Why
generator/src/main/kotlin/norm/generator/ImplementationBuilder.kt already has sqlFunction and
mapperFunction (lines 28-70) that build a query method's signature (name, @Throws, one parameter
per statement parameter, the T type variable, the trailing mapper lambda). addManyImplementation
(lines 113-216) ignores them and rebuilds the same parameter list and mapper parameter three times
inline: once for the private helper (118-141), once for the public Many override (163-180), once
for the Dynamically override (195-207). Any change to how a parameter is named or typed must be
made in four places instead of one.
Target state
private fun TypeSpec.Builder.addManyImplementation(statement: SqlStatement) {
val resultRowShape = statement.resultRowShape
val returnTypeVariable = TypeVariableName("Return")
val helperFunction = mapperFunction(statement)
.addModifiers(KModifier.PRIVATE)
.addTypeVariable(returnTypeVariable)
.addParameter("processor", MANY_PROCESSOR.parameterizedBy(resultRowShape.mapperReturnType, returnTypeVariable))
.returns(returnTypeVariable)
.apply { /* existing body: val sql, rowReader, queryBinder, return processor.invoke(...) */ }
.build()
addFunction(helperFunction)
val manyFunction = mapperFunction(statement)
.addModifiers(KModifier.OVERRIDE)
.apply { /* existing one-line body delegating to helper with driver::queryMany */ }
.build()
addFunction(manyFunction)
if (statement.canBeDynamic) {
val dynamicFunction = mapperFunction(statement).build()
.toBuilder("${statement.name}Dynamically")
.addModifiers(KModifier.OVERRIDE)
.returns(Command.NORM_QUERY.parameterizedBy(resultRowShape.mapperReturnType))
.apply { /* existing one-line body */ }
.build()
addFunction(dynamicFunction)
}
}
Notes for equivalence:
sqlFunction adds @Throws(SQLException) only when command != MANY, so mapperFunction on a
:many statement adds no throws annotation, matching today's three builders (none of which call
throws).
mapperFunction calls .returns(Many<T>); the helper and dynamic variants override with their own
.returns(...). KotlinPoet's FunSpec.Builder.returns replaces the previous value.
canBeDynamic implies parameters.isEmpty(), so the dynamic signature from mapperFunction is
(mapper) only, identical to today's hand-built one. FunSpec.toBuilder(name) keeps every other
attribute.
- Extract
ClassName("norm", "ManyProcessor") into a file-level private val MANY_PROCESSOR next to
PROCESS_EXEC_RESULTS.
Test design
The generated text for every :many query in test-scenarios*/**/PostgresQueries.kt is the pin; the
ctes scenario alone has dozens. SqlStatementTest / InterfaceBuilderKdocTest do not need changes.
No new unit test is required beyond golden invariance, but if the goldens change in any way the
refactor is wrong, not the goldens.
Acceptance criteria
addManyImplementation contains no inline for ((index, parameter) in statement.parameters...)
loop and no inline construction of the mapper ParameterSpec.
./gradlew :generator:check :gradle-plugin:test passes; golden regeneration leaves
test-scenarios* byte-identical.
Files
generator/src/main/kotlin/norm/generator/ImplementationBuilder.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.
Why
generator/src/main/kotlin/norm/generator/ImplementationBuilder.ktalready hassqlFunctionandmapperFunction(lines 28-70) that build a query method's signature (name,@Throws, one parameterper statement parameter, the
Ttype variable, the trailingmapperlambda).addManyImplementation(lines 113-216) ignores them and rebuilds the same parameter list and mapper parameter three times
inline: once for the private helper (118-141), once for the public
Manyoverride (163-180), oncefor the
Dynamicallyoverride (195-207). Any change to how a parameter is named or typed must bemade in four places instead of one.
Target state
Notes for equivalence:
sqlFunctionadds@Throws(SQLException)only whencommand != MANY, somapperFunctionon a:manystatement adds no throws annotation, matching today's three builders (none of which callthrows).mapperFunctioncalls.returns(Many<T>); the helper and dynamic variants override with their own.returns(...). KotlinPoet'sFunSpec.Builder.returnsreplaces the previous value.canBeDynamicimpliesparameters.isEmpty(), so the dynamic signature frommapperFunctionis(mapper)only, identical to today's hand-built one.FunSpec.toBuilder(name)keeps every otherattribute.
ClassName("norm", "ManyProcessor")into a file-levelprivate val MANY_PROCESSORnext toPROCESS_EXEC_RESULTS.Test design
The generated text for every
:manyquery intest-scenarios*/**/PostgresQueries.ktis the pin; thectesscenario alone has dozens.SqlStatementTest/InterfaceBuilderKdocTestdo not need changes.No new unit test is required beyond golden invariance, but if the goldens change in any way the
refactor is wrong, not the goldens.
Acceptance criteria
addManyImplementationcontains no inlinefor ((index, parameter) in statement.parameters...)loop and no inline construction of the
mapperParameterSpec../gradlew :generator:check :gradle-plugin:testpasses; golden regeneration leavestest-scenarios*byte-identical.Files
generator/src/main/kotlin/norm/generator/ImplementationBuilder.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.