Depends on: #276 (moves quoteSqlIdentifierIfNeeded out of this file first).
Why
generator/src/main/kotlin/norm/generator/TypeRepository.kt is 1145 lines holding four unrelated
things:
| Lines |
Concern |
| 26-552 |
TypeRepository class: column → SqlMappable resolution and data-class projection building |
| 554-795 |
Two type tables: BASE_TYPE_RESOLVERS (35 keys → SqlMappable factory) and resolveJdbcTypeInfo (the same 35 keys → JdbcTypeInfo), plus postgresArrayElementTypeName |
| 797-1100 |
KDoc rendering: PropertySource, addClassKdoc, formatAsKdocPropertyReference, needsKotlinPoetDeclarationBackticks, sourceReference, and the Markdown helpers (longestBacktickRun, markdownInlineCodeSpan, containsUnescapableBlockCommentDelimiter, escapeMarkdownBacktick, wrapInBacktickDelimiter, markdownFenceDelimiter) |
| 1102-1145 |
collapseCosmeticWhitespace (a lexer-aware whitespace normaliser used by the provenance path) |
The two type tables are kept in lockstep by a test sweep (ColumnTypeMappingTest.kt, the
DomainBaseTypes parity check around lines 2185-2200) rather than by construction. A type added to
one and forgotten in the other compiles and fails only at test time.
Target state
Files
TypeRepository.kt: the class only (lines 26-552 today), plus ENUM_JDBC_TYPE_INFO if only the
class uses it.
PostgresBaseTypes.kt: the merged table (below) and postgresArrayElementTypeName.
KdocRendering.kt: PropertySource, TypeSpec.Builder.addClassKdoc, every private helper it
calls, and the Markdown helpers. InterfaceBuilder.kt already imports markdownFenceDelimiter,
escapeMarkdownBacktick, containsUnescapableBlockCommentDelimiter; those stay internal.
collapseCosmeticWhitespace moves to SqlLexer.kt: it is a skipLexicalToken-based text
normaliser and its only callers are TypeRepository.buildTypeProjectionForQuery and
resolveNodeTreeProvenanceExpression.
Pure moves: no function body changes in this step. Do the split as its own commit so the diff is
reviewable as renames.
Merged table
/**
* One Postgres base type Norm maps to Kotlin, keyed by its canonical or SQL-standard spelling.
*
* @property mappable Builds the [SqlMappable] for a plain (adapterless) column of this type.
* @property jdbcTypeInfo The wire-level getter/setter description used when the same type sits behind a
* [ColumnAdapter] (an enum, a domain, or a user type mapping).
*/
internal class PostgresBaseType(
val mappable: (notNull: Boolean) -> SqlMappable,
val jdbcTypeInfo: JdbcTypeInfo,
)
internal val POSTGRES_BASE_TYPES: Map<String, PostgresBaseType> = buildMap {
fun register(jdbcTypeInfo: JdbcTypeInfo, vararg names: String, mappable: (Boolean) -> SqlMappable) {
for (name in names) put(name, PostgresBaseType(mappable, jdbcTypeInfo))
}
register(JdbcTypeInfo("getShort", "setShort", true, "SMALLINT", kotlinType = SHORT), "smallserial", "serial2", "smallint", "int2") {
JdbcTypes.SHORT.decorateForNullable(it)
}
// ... one register call per row of today's resolveJdbcTypeInfo `when`, carrying the matching
// BASE_TYPE_RESOLVERS lambda ...
}
internal fun resolveJdbcTypeInfo(baseTypeName: String): JdbcTypeInfo? = POSTGRES_BASE_TYPES[baseTypeName]?.jdbcTypeInfo
TypeRepository.resolveBaseType becomes
POSTGRES_BASE_TYPES[typeName.removePrefix("pg_catalog.")]?.mappable?.invoke(notNull).
Keep every existing comment that carries a verified fact (pgjdbc getter behaviour for java.time
types, oid → Blob, json/jsonb needing Types.OTHER) attached to the row it describes. Drop
the paragraphs whose only content is "the two tables must be kept in sync"; there is now one table.
Tests
The parity sweep in ColumnTypeMappingTest that asserts every BASE_TYPE_RESOLVERS key has a
resolveJdbcTypeInfo entry becomes a tautology (x in map.keys → map[x] != null). Delete it. Keep
the alias-classification test around line 1025 (it asserts something about
postgresArrayElementTypeName, not table parity); rewrite its BASE_TYPE_RESOLVERS.keys reference to
POSTGRES_BASE_TYPES.keys. Every other test that names BASE_TYPE_RESOLVERS is updated the same way.
TypeRepositoryTest.kt, DomainBuilderTest.kt, and the golden scenarios (all_types, domains,
type_mappings) pin the per-type output.
Acceptance criteria
- No file under
generator/src/main exceeds 1000 lines after this change except
PgNodeTreeParser.kt and NodeTreeNullabilityAnalyzer.kt (addressed elsewhere or accepted).
git grep -n "BASE_TYPE_RESOLVERS" generator/ returns nothing; exactly one table maps type names.
./gradlew :generator:check :gradle-plugin:test passes; golden regeneration leaves
test-scenarios* byte-identical.
Files
generator/src/main/kotlin/norm/generator/TypeRepository.kt (shrinks)
- new
generator/src/main/kotlin/norm/generator/PostgresBaseTypes.kt
- new
generator/src/main/kotlin/norm/generator/KdocRendering.kt
generator/src/main/kotlin/norm/generator/SqlLexer.kt (receives collapseCosmeticWhitespace)
generator/src/main/kotlin/norm/generator/DomainBuilder.kt, Main.kt (import-only changes)
generator/src/test/kotlin/norm/generator/ColumnTypeMappingTest.kt
Conventions every issue inherits
- Repo root:
/Volumes/Code/3rd-party/norm. Module under change is almost always generator/.
- Style: 2-space indent, 120 columns, full words in identifiers (
parameter, not param), no
section-separator comments, @Nested classes group tests. Format with ./gradlew spotlessApply.
- TDD: tests are written first or alongside. For a behavior-preserving refactor the existing suite is
the pin; each issue says which tests must also be added.
- Golden files under
test-scenarios*/ are never hand-edited. A refactor is behavior-preserving only
if ./gradlew :gradle-plugin:generateGoldenFiles leaves git status --porcelain test-scenarios test-scenarios-frameworks empty.
- Verification for a
generator/ change: ./gradlew :generator:check :gradle-plugin:test (Docker
required). Then the golden regeneration check above.
- Commit message explains the design decision, not the diff.
- Never run
./gradlew clean or disable the configuration/build cache to "fix" a build problem.
Depends on: #276 (moves
quoteSqlIdentifierIfNeededout of this file first).Why
generator/src/main/kotlin/norm/generator/TypeRepository.ktis 1145 lines holding four unrelatedthings:
TypeRepositoryclass: column →SqlMappableresolution and data-class projection buildingBASE_TYPE_RESOLVERS(35 keys →SqlMappablefactory) andresolveJdbcTypeInfo(the same 35 keys →JdbcTypeInfo), pluspostgresArrayElementTypeNamePropertySource,addClassKdoc,formatAsKdocPropertyReference,needsKotlinPoetDeclarationBackticks,sourceReference, and the Markdown helpers (longestBacktickRun,markdownInlineCodeSpan,containsUnescapableBlockCommentDelimiter,escapeMarkdownBacktick,wrapInBacktickDelimiter,markdownFenceDelimiter)collapseCosmeticWhitespace(a lexer-aware whitespace normaliser used by the provenance path)The two type tables are kept in lockstep by a test sweep (
ColumnTypeMappingTest.kt, theDomainBaseTypesparity check around lines 2185-2200) rather than by construction. A type added toone and forgotten in the other compiles and fails only at test time.
Target state
Files
TypeRepository.kt: the class only (lines 26-552 today), plusENUM_JDBC_TYPE_INFOif only theclass uses it.
PostgresBaseTypes.kt: the merged table (below) andpostgresArrayElementTypeName.KdocRendering.kt:PropertySource,TypeSpec.Builder.addClassKdoc, every private helper itcalls, and the Markdown helpers.
InterfaceBuilder.ktalready importsmarkdownFenceDelimiter,escapeMarkdownBacktick,containsUnescapableBlockCommentDelimiter; those stayinternal.collapseCosmeticWhitespacemoves toSqlLexer.kt: it is askipLexicalToken-based textnormaliser and its only callers are
TypeRepository.buildTypeProjectionForQueryandresolveNodeTreeProvenanceExpression.Pure moves: no function body changes in this step. Do the split as its own commit so the diff is
reviewable as renames.
Merged table
TypeRepository.resolveBaseTypebecomesPOSTGRES_BASE_TYPES[typeName.removePrefix("pg_catalog.")]?.mappable?.invoke(notNull).Keep every existing comment that carries a verified fact (pgjdbc getter behaviour for
java.timetypes,
oid→Blob,json/jsonbneedingTypes.OTHER) attached to the row it describes. Dropthe paragraphs whose only content is "the two tables must be kept in sync"; there is now one table.
Tests
The parity sweep in
ColumnTypeMappingTestthat asserts everyBASE_TYPE_RESOLVERSkey has aresolveJdbcTypeInfoentry becomes a tautology (x in map.keys → map[x] != null). Delete it. Keepthe alias-classification test around line 1025 (it asserts something about
postgresArrayElementTypeName, not table parity); rewrite itsBASE_TYPE_RESOLVERS.keysreference toPOSTGRES_BASE_TYPES.keys. Every other test that namesBASE_TYPE_RESOLVERSis updated the same way.TypeRepositoryTest.kt,DomainBuilderTest.kt, and the golden scenarios (all_types,domains,type_mappings) pin the per-type output.Acceptance criteria
generator/src/mainexceeds 1000 lines after this change exceptPgNodeTreeParser.ktandNodeTreeNullabilityAnalyzer.kt(addressed elsewhere or accepted).git grep -n "BASE_TYPE_RESOLVERS" generator/returns nothing; exactly one table maps type names../gradlew :generator:check :gradle-plugin:testpasses; golden regeneration leavestest-scenarios*byte-identical.Files
generator/src/main/kotlin/norm/generator/TypeRepository.kt(shrinks)generator/src/main/kotlin/norm/generator/PostgresBaseTypes.ktgenerator/src/main/kotlin/norm/generator/KdocRendering.ktgenerator/src/main/kotlin/norm/generator/SqlLexer.kt(receivescollapseCosmeticWhitespace)generator/src/main/kotlin/norm/generator/DomainBuilder.kt,Main.kt(import-only changes)generator/src/test/kotlin/norm/generator/ColumnTypeMappingTest.ktConventions every issue inherits
/Volumes/Code/3rd-party/norm. Module under change is almost alwaysgenerator/.parameter, notparam), nosection-separator comments,
@Nestedclasses group tests. Format with./gradlew spotlessApply.the pin; each issue says which tests must also be added.
test-scenarios*/are never hand-edited. A refactor is behavior-preserving onlyif
./gradlew :gradle-plugin:generateGoldenFilesleavesgit status --porcelain test-scenarios test-scenarios-frameworksempty.generator/change:./gradlew :generator:check :gradle-plugin:test(Dockerrequired). Then the golden regeneration check above.
./gradlew cleanor disable the configuration/build cache to "fix" a build problem.