Skip to content

Build :many signatures from mapperFunction #279

Description

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

  1. addManyImplementation contains no inline for ((index, parameter) in statement.parameters...)
    loop and no inline construction of the mapper ParameterSpec.
  2. ./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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions