Skip to content

One range-table parser; derive the four specialised views from it #278

Description

Depends on: #274 (so there is a single call site that needs the derived views).

Why

generator/src/main/kotlin/norm/generator/PgNodeTreeParser.kt scans :rtable five times with five
near-identical loops:

Method Lines Yields
parseRangeTable 109-119 varno → relid for rtekind 0
parseGroupRteMap 137-154 (groupVarno, attr) → (baseVarno, baseAttno) for rtekind 9, by textual :varno/:varattno scan of each :groupexprs block
parseGroupRteExpressions 181-193 groupVarno → List<PgNodeExpression> for rtekind 9, by parseExpression of each block
parseSubqueryRangeTable 386-400 varno → {QUERY} text for rtekind 1
parseCteRangeTableEntries 452-463 varno → NodeTreeCteReference for rtekind 6
parseRangeTableEntries 481-511 varno → RangeTableEntry for every kind

The last one already subsumes the first, fourth, and fifth exactly (same field extraction, same
1-based indexing). Only the two GROUP-RTE methods read a field (:groupexprs) that
RangeTableEntry.Other(9) does not carry.

Target state

Model

Add to RangeTableEntry in PgNodeExpression.kt:

/**
 * `rtekind 9`: a PostgreSQL 18+ `*GROUP*` RTE. [groupExpressionBlocks] are the raw `{...}` blocks of
 * `:groupexprs`, in order, so `varattno - 1` indexes into them.
 */
data class Group(val groupExpressionBlocks: List<String>) : RangeTableEntry

parseRangeTableEntries gains a 9 -> branch producing Group(splitBraceBlocks(groupExprsContent)),
falling back to Other(9) when :groupexprs is absent (matching today's return@forEachIndexed,
which skips the entry entirely; skipping vs Other(9) is unobservable because no caller reads
Other).

Derived views

In PgNodeExpression.kt, next to RangeTableEntry, as extension functions on
Map<Int, RangeTableEntry>:

internal fun Map<Int, RangeTableEntry>.baseRelations(): Map<Int, Int>
internal fun Map<Int, RangeTableEntry>.subqueryBlocks(): Map<Int, String>
internal fun Map<Int, RangeTableEntry>.cteReferences(): Map<Int, NodeTreeCteReference>
internal fun Map<Int, RangeTableEntry>.groupExpressions(parser: PgNodeTreeParser): Map<Int, List<PgNodeExpression>>
internal fun Map<Int, RangeTableEntry>.groupRteMap(): Map<Pair<Int, Int>, Pair<Int, Int>>

Each is a mapNotNull/filterIsInstance over the entries. groupRteMap must preserve today's
semantics exactly: for each block, the first textual :varno and :varattno (today's
extractIntField), not parseExpression(block) as? Var. The two differ when a group expression is
not a bare VAR (a FUNCEXPR containing a VAR, or an unmodelled node type whose VAR child is
lost to Unknown). Whether that textual rule is right is a separate question; this issue keeps it.
To do that, extractIntField (currently private on the parser) is either made internal or the
derivation is expressed as parser.firstVarnoAndVarattno(block); pick whichever keeps the parser's
surface smaller.

Deletions

Delete parseRangeTable, parseGroupRteMap, parseGroupRteExpressions, parseSubqueryRangeTable,
parseCteRangeTableEntries. Update the KDoc on RangeTableEntry (currently lines 292-301 of
PgNodeExpression.kt) which lists the deleted methods as contrast.

Call sites

After #274 the derivations are consumed in buildQueryBlockScope (one parseRangeTableEntries
call, then .baseRelations(), .subqueryBlocks(), .cteReferences(), .groupRteMap()),
analyzeCteBodyNullability / analyzeSetOperationBranches (.baseRelations(), .subqueryBlocks()),
mergeAbsentVarnos (already uses parseRangeTableEntries), and
NodeTreeNullabilityAnalyzer.extractColumnNullability:141 (.groupExpressions(parser)).
NodeTreeProvenanceResolver already uses parseRangeTableEntries.

Test design

  • PgNodeTreeParserTest.kt has tests for each deleted method. Rewrite each to call
    parseRangeTableEntries(...) plus the corresponding derivation and assert the same result. Do not
    drop any assertion.
  • Add one test for Group: a hand-written :rtable with an rtekind 9 entry whose :groupexprs
    holds a {FUNCEXPR ... :args ({VAR :varno 1 :varattno 2 ...})} block. Assert groupRteMap() maps
    (groupVarno, 1) → (1, 2) (textual rule) and groupExpressions(parser)[groupVarno][0] is a
    FuncExpr. This pins that the two derivations intentionally disagree on non-Var expressions.
  • QueryAnalysisTest Grouping nested class (about 1200 tests-lines, PG18 GROUP RTE coverage) and
    goldens are the end-to-end pin.

Acceptance criteria

  1. PgNodeTreeParser has exactly one method that iterates :rtable.
  2. The five deleted method names appear nowhere in generator/.
  3. Tests in "Test design" exist and pass; ./gradlew :generator:check :gradle-plugin:test passes;
    golden regeneration leaves test-scenarios* unchanged.

Files

  • generator/src/main/kotlin/norm/generator/PgNodeTreeParser.kt
  • generator/src/main/kotlin/norm/generator/PgNodeExpression.kt
  • generator/src/main/kotlin/norm/generator/ColumnNullabilityAnalyzer.kt
  • generator/src/main/kotlin/norm/generator/NodeTreeNullabilityAnalyzer.kt
  • generator/src/test/kotlin/norm/generator/PgNodeTreeParserTest.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