Skip to content

Replace query internals with canonical backend-agnostic IR AST#9

Merged
flyon merged 23 commits intodevfrom
refactor-intermediate-representation
Feb 24, 2026
Merged

Replace query internals with canonical backend-agnostic IR AST#9
flyon merged 23 commits intodevfrom
refactor-intermediate-representation

Conversation

@flyon
Copy link
Copy Markdown
Member

@flyon flyon commented Feb 24, 2026

Summary

Replaces the internal query representation with a canonical backend-agnostic IR AST. The public Shape DSL (Shape.select(), .create(), .update(), .delete()) is unchanged — what changed is what IQuadStore implementations receive.

Why

Stores previously received ad-hoc nested arrays and objects (query.select[0][0].property.label). This made downstream store implementations brittle, hard to reason about, and tightly coupled to internal DSL structure. A normalized IR with typed discriminated unions gives store implementers a stable, well-documented contract to build against.

What

  • SelectQuery, CreateQuery, UpdateQuery, DeleteQuery ARE the IR types. No separate "IR" vs "query" type hierarchy — factories emit IR directly via build().
  • Full IR AST for selects: root (shape scan pattern), projection (expression-based), where (expression tree), orderBy, limit, subjectId, singleResult, resultMap.
  • Typed mutations: Same kind discriminators (create, update, delete) with IRFieldUpdate[], IRNodeDescription, and set modification support (add/remove).
  • Compact references: IR carries shapeId and propertyShapeId strings only — no embedded shape/property objects.
  • Early normalization: some()exists(), every()not exists(not ...) during canonicalization. Boolean trees flattened.
  • Explicit aliasing: Deterministic alias generation (a0, a1, ...) with lexical scope validation.

How

A multi-stage pipeline converts DSL traces to IR:

SelectQueryFactory.build()
  → toRawInput()             (extract factory state)
  → desugarSelectQuery()     (IRDesugar — flatten DSL tree)
  → canonicalize()           (IRCanonicalize — normalize booleans, rewrite quantifiers)
  → lowerSelectQuery()       (IRLower — produce full AST with aliases, patterns, expressions)
  → SelectQuery

Mutations convert directly via buildCanonical*MutationIR() in IRMutation.ts.

New modules: IRDesugar.ts, IRCanonicalize.ts, IRLower.ts, IRPipeline.ts, IRProjection.ts, IRAliasScope.ts, IRMutation.ts. All exported functions have JSDoc documentation.

Breaking changes for downstream stores

IQuadStore method signatures are unchanged, but the objects they receive are structurally different. Select queries are completely new (query.root, query.projection, query.where). Mutation queries are structurally similar but properly typed. Store result types are now exported: ResultRow, SelectResult, CreateResult, UpdateResult, SetOverwriteResult, SetModificationResult.

Full IR reference: documentation/intermediate-representation.md.

Other improvements

  • QueryParser simplified: Deleted IQueryParser interface. Shape calls QueryParser directly — no swappable indirection.
  • Dead code removed: ~50 lines of commented-out type variants cleaned up.
  • Test coverage: 147 tests across 9 suites. Every select pattern (56) and mutation pattern (18) from the original query.test.ts has IR assertions. Old legacy test file deleted.
  • Docs consolidated: Four plan docs merged into docs/003-ir-refactoring.md.

Test plan

  • All 147 tests pass (npx jest --config jest.config.js)
  • TypeScript compiles cleanly (npx tsc --noEmit)
  • No stale references to deleted APIs (IQueryParser, getLegacyQueryObject, getCanonicalIR, etc.)
  • IR golden fixtures cover all select/mutation patterns
  • Compile-time type inference assertions green (query.types.test.ts)

🤖 Generated with Claude Code

René Verheij and others added 23 commits February 23, 2026 15:32
Refactored intermediate representation

Enhance Jest configuration and documentation for Intermediate Representation

- Added new test patterns for Intermediate Representation related tests in jest.config.js.
- Updated README to include a section on Intermediate Representation with a link to the documentation.
Refactored intermediate representation

Enhance Jest configuration and documentation for Intermediate Representation

- Added new test patterns for Intermediate Representation related tests in jest.config.js.
- Updated README to include a section on Intermediate Representation with a link to the documentation.
Phase 0: plan document covering 8 phases to complete the IR refactor —
expand desugar, lift to full AST, test parity, production wiring,
cleanup, and documentation sync.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Added 6 new desugared types (DesugaredCountStep, DesugaredTypeCastStep,
DesugaredSubSelect, DesugaredCustomObjectSelect, DesugaredEvaluationSelect,
DesugaredMultiSelection) to handle sub-selects, custom result objects, type
casting, preload, and count/aggregation patterns. Expanded desugar tests from
3 to 35 covering all query patterns in query.test.ts. Updated downstream
consumers (IRPipeline, IRProjection) for new union types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract shared QueryCaptureStore to src/test-helpers/query-capture-store.ts,
merge ir-pipeline-parity tests into ir-select-golden, document pipeline
architecture in documentation/intermediate-representation.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewrite IR docs with comprehensive examples for all variant types,
add migration section to README, mark docs 002/003 as superseded.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…/DeleteQuery are now IR types

Old interfaces renamed to LegacySelectQuery/LegacyCreateQuery/LegacyUpdateQuery/LegacyDeleteQuery.
IQuadStore and LinkedStorage now import canonical query types from their query files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Object/getIR

All four query factories now expose build() as the primary method.
getQueryObject() and getIR() are deprecated aliases. QueryParser and
internal callers (getPropertyPath, isValidResult) updated.
getLegacyQueryObject() remains internally for pipeline input (Phase 11)
and test helpers (Phase 12).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…peline

Define RawSelectInput as the internal pipeline input type capturing exactly
what the desugar pass needs. SelectQueryFactory.build() now constructs
RawSelectInput directly from factory state, bypassing getLegacyQueryObject().
IRDesugar.ts and IRPipeline.ts no longer import LegacySelectQuery.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ery.test.ts

- Delete getLegacyQueryObject() from all four factories
- Add toRawInput() on SelectQueryFactory for pipeline/test access
- Delete LegacySelectQuery, LegacyCreateQuery, LegacyUpdateQuery, LegacyDeleteQuery
- Define mutation input types locally in IRMutation.ts
- Update query-capture-store to use toRawInput() for select, build() for mutations
- Simplify ir-mutation-parity.test.ts (capture store now returns IR directly)
- Update core-utils.test.ts to use toRawInput()
- Delete query.test.ts (superseded by ir-select-golden.test.ts)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…SelectQuery, update docs

- Delete SelectQueryIR type alias from IRPipeline.ts
- Rename buildSelectQueryIR to buildSelectQuery
- Update ir-select-golden.test.ts to use SelectQuery type and build()
- Update documentation/intermediate-representation.md for current pipeline
- Update README.md migration section for build() API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ion type discriminators

- Delete getQueryObject() from all 4 factories and QueryFactory base class
- Delete getIR() from SelectQueryFactory
- Delete LinkedQuery interface (no longer needed)
- Remove dead buildCanonicalMutationIR dispatcher and CanonicalMutationIR type
- Remove type:'create'|'update'|'delete' discriminators from mutation input types
- Fix as any in BoundComponent.getPropertyPath() with proper Array.isArray narrowing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…p redundant structure

- Top-level kinds: select_query→select, create_mutation→create, update_mutation→update, delete_mutation→delete
- Graph pattern: exists_pattern→exists
- Flatten IRShapeRef {shapeId: string} and IRPropertyRef {propertyShapeId: string} to plain strings
- Rename description→data and updates→data on mutations
- Rename IRNodeDescription→IRNodeData, IRNodeFieldUpdate→IRFieldUpdate
- Drop kind from projection items, order by items (unambiguous from context)
- Flatten resultMap from {kind: 'result_map', entries: [...]} to just [...]
- Remove IRResultMap wrapper type
- Update all pipeline stages, tests, documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…arser

This commit completes the IR refactoring branch with final improvements for downstream implementers:

### Dead code removal
- QueryFactory.ts: removed ~50 lines of commented-out type variants
- SelectQuery.ts: removed commented-out QueryBoolean stub

### Documentation improvements
- Added JSDoc comments to all 13 exported IR pipeline functions
- Functions documented: buildSelectQuery, desugarSelectQuery, canonicalizeWhere, canonicalizeDesugaredSelectQuery, lowerSelectQuery, projectionKeyFromPath, lowerSelectionPathExpression, buildCanonicalProjection, and mutation IR builders
- Added JSDoc for IRAliasBinding type and validateAliasReference function

### QueryParser simplification
- Deleted IQueryParser interface (existed only for swappability with one impl)
- Shape now calls QueryParser.selectQuery/updateQuery/createQuery/deleteQuery directly
- Removed Shape.queryParser static property and queryParser from method this constraints
- Updated QueryCaptureStore to use jest.spyOn instead of implementing IQueryParser
- Tests updated to use new captureQuery API (6 test files updated)
- Result: cleaner module boundaries, one less abstraction, no runtime circular dependency

### Consolidation
- Merged docs 002, 003, 004 into single docs/003-ir-refactoring.md
- All 26 implementation phases summarized with key decisions and final state

All 147 tests pass. TypeScript clean. Ready for production.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@flyon flyon changed the title IR refactor: final review and cleanup Replace query internals with canonical backend-agnostic IR AST Feb 24, 2026
@flyon flyon merged commit 5c8df9e into dev Feb 24, 2026
3 checks passed
@flyon flyon deleted the refactor-intermediate-representation branch February 24, 2026 08:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant