Skip to content

Unify WHERE pipeline on ExpressionNode, retire Evaluation, add .none()#53

Merged
flyon merged 17 commits intomainfrom
claude/compare-orm-libraries-Qk11R
Apr 3, 2026
Merged

Unify WHERE pipeline on ExpressionNode, retire Evaluation, add .none()#53
flyon merged 17 commits intomainfrom
claude/compare-orm-libraries-Qk11R

Conversation

@flyon
Copy link
Copy Markdown
Member

@flyon flyon commented Apr 3, 2026

Summary

This PR completes negation support in the query DSL by unifying all WHERE conditions on ExpressionNode and retiring the legacy Evaluation class. Added .none() collection quantifier for filtering where no elements match a condition.

Key Changes

WHERE Pipeline Unification

  • Removed Evaluation class and its 4-stage pipeline (Evaluation → WherePath → DesugaredWhere → CanonicalWhere → IR)
  • .equals() now returns ExpressionNode instead of Evaluation, enabling .not() chaining
  • Simplified WhereClause type to accept only ExpressionNode or ExistsCondition (or callbacks returning them)
  • Removed WhereEvaluationPath, DesugaredWhereComparison, DesugaredWhereBoolean, CanonicalWhereComparison, CanonicalWhereLogical, CanonicalWhereExists, CanonicalWhereNot types

New Collection Quantifier

  • Added .none(fn) method on QueryShapeSet for "no elements match" filtering
  • Generates FILTER(NOT EXISTS { ... }) in SPARQL
  • Equivalent to .some(fn).not() but reads more naturally

Expression System Enhancements

  • Added ExistsCondition type to represent collection quantifiers (.some(), .every(), .none())
  • Added isExistsCondition() guard function
  • Added tracedAliasExpression() for root shape comparisons
  • Updated toExpressionNode() to handle context references and alias expressions

IR Simplification

  • DesugaredWhere now only contains DesugaredExpressionWhere and DesugaredExistsWhere
  • CanonicalWhereExpression simplified to only DesugaredExpressionWhere and DesugaredExistsWhere
  • Removed lowerWhereArg() function (no longer needed)
  • lowerWhere() now handles only expression and exists conditions

Test Coverage

  • Added golden tests for .none(), .some().not(), .equals().not(), and chained negations
  • Updated IR canonicalization tests to reflect new WHERE structure
  • Updated IR desugar tests to remove Evaluation-related assertions

Implementation Details

The unification works by recognizing that ExpressionNode is already an IR representation, eliminating the need for intermediate transformation stages. Collection quantifiers (.some(), .every(), .none()) are handled separately via ExistsCondition, which lowers to IRExistsExpression with appropriate negation flags.

All negation now flows through two mechanisms:

  1. Expression-level: .not() method on ExpressionNode for boolean negation
  2. Pattern-level: ExistsCondition with negation for collection quantifiers

This enables natural chaining like p.name.equals('Alice').not() and p.friends.none(f => f.hobby.equals('Chess')).

https://claude.ai/code/session_019MDcpswuKKNx9YhC2S2vT7

claude added 17 commits April 1, 2026 20:34
Compare Linked with popular JS/TS libraries (Prisma, Drizzle, TypeORM,
MikroORM, Sequelize, Kysely, Knex, Neo4j OGM, Neogma) across type safety,
query style, schema definition, relationships, and SQLAlchemy pattern alignment.

https://claude.ai/code/session_019MDcpswuKKNx9YhC2S2vT7
Use the public npm package for skill installation instead of a custom
script that required SSH access to the private agents repo.

https://claude.ai/code/session_019MDcpswuKKNx9YhC2S2vT7
Each doc covers current codebase state, how other libraries handle it,
open questions, and potential API designs:

- 016: Aggregations (sum/avg/min/max/groupBy)
- 017: Upsert (create-or-update semantics)
- 018: Transactions (mutation batching)
- 019: Multi-column sorting (per-key direction)
- 020: Distinct (explicit control)
- 021: Computed properties (reusable expression fragments on shapes)
- 022: Negation (NOT/notEquals/none)

https://claude.ai/code/session_019MDcpswuKKNx9YhC2S2vT7
Clarify what Linked is in relation to the ORM/query builder landscape
(like Drizzle/Prisma but for RDF/SPARQL). Update setup instructions
to reflect the new npx-based setup command.

https://claude.ai/code/session_019MDcpswuKKNx9YhC2S2vT7
Resolved all open questions through interactive ideation:
- Retire Evaluation class, unify on ExpressionNode for all WHERE paths
- Add .none() on collections (sugar for .some().not())
- Expr.not() prefix and .not() postfix suffice for general negation
- Skip prefix f.age.not().gt() — ambiguous scope
- Keep .minus() alongside .none() — different SPARQL semantics

https://claude.ai/code/session_019MDcpswuKKNx9YhC2S2vT7
4-phase plan: migrate .equals() to ExpressionNode, migrate .some()/.every(),
add .none(), then remove Evaluation class and old WHERE infrastructure.

https://claude.ai/code/session_019MDcpswuKKNx9YhC2S2vT7
Phase 1+2+3 of negation implementation (022):
- Add 'equals' to EXPRESSION_METHODS for proxy interception
- Rewrite QueryShape/QueryPrimitive/QueryPrimitiveSet .equals() to return ExpressionNode
- Rewrite QueryShapeSet .some()/.every() to return ExistsCondition
- Add .none() on QueryShapeSet
- Add ExistsCondition class for EXISTS quantifier conditions
- Add lowerExistsCondition in IRLower for ExistsCondition → IRExistsExpression
- Add toExpressionNode helper for QueryBuilderObject → ExpressionNode
- Update tests for new pipeline paths

Status: 68/68 golden IR tests pass. 5 edge case failures remain
(inline where on primitives, root shape equality, size equality,
context values). These need special handling during migration.

https://claude.ai/code/session_019MDcpswuKKNx9YhC2S2vT7
Handle cases where toExpressionNode can't resolve a property path:
- Root shape self-reference (p.equals(...))
- Inline where on primitives (h.equals(...))
- Shape reference comparison (p.bestFriend.equals({id}))

Falls back to old Evaluation path when segments are empty.
Down to 2 remaining edge case failures (context paths, count equals).

https://claude.ai/code/session_019MDcpswuKKNx9YhC2S2vT7
- Override .equals() on SetSize to always use Evaluation path (preserves
  count/GROUP BY/HAVING semantics in SPARQL)
- Handle query context references in toExpressionNode — detect context
  origin and produce context_property_expr IR
- findContextId walks the QBO chain to find __queryContextId markers

All 927 non-Fuseki tests pass (0 failures).

https://claude.ai/code/session_019MDcpswuKKNx9YhC2S2vT7
All .equals() calls now produce ExpressionNode without Evaluation:
- Root shape: tracedAliasExpression([]) resolves to root alias
- Inline where: InlineWhereProxy produces alias expression for bound value
- SetSize: builds aggregate_expr(count, ...) ExpressionNode directly
- Context root: reference_expr with context IRI
- Context property: context_property_expr

Added tracedAliasExpression() and alias_expr resolution in
resolveExpressionRefs. Added aggregate_expr resolution too.

All 927 non-Fuseki tests pass.

https://claude.ai/code/session_019MDcpswuKKNx9YhC2S2vT7
- Gut Evaluation class (empty deprecated stub remains for type compat)
- Remove dead code: DesugaredWhereComparison, DesugaredWhereBoolean,
  CanonicalWhereComparison, CanonicalWhereLogical, CanonicalWhereExists,
  CanonicalWhereNot, toWhereComparison, toWhereArg, toExists, toComparison,
  canonicalizeComparison, flattenLogical, isDesugaredWhere
- Simplify canonicalizeWhere to passthrough for expression + exists
- Simplify lowerWhere to only handle expression + exists_condition
- Remove isEvaluation from FieldSet
- Remove Evaluation from WhereClause type
- Fix type inference: use structural check for ExpressionNode→boolean
  mapping to avoid false matches on other classes
- Add 15 negation type inference tests (compile-only)
- Update ir-canonicalize tests for new pipeline

All 927 non-Fuseki tests pass. TypeScript compiles clean.

https://claude.ai/code/session_019MDcpswuKKNx9YhC2S2vT7
- Remove remaining dead code: Evaluation stub, WhereMethods enum,
  isWhereEvaluationPath, WhereEvaluationPath, DesugaredEvaluationSelect,
  evaluation_select handler, collectRefs, isEvaluation
- Add .none() query fixture (whereNone) + IR golden test + SPARQL golden test
- Write final report (docs/reports/013-negation-and-evaluation-retirement.md)
- Remove plan doc (docs/plans/022-negation.md)
- Add changeset for minor version bump

929 tests pass, 0 failures.

https://claude.ai/code/session_019MDcpswuKKNx9YhC2S2vT7
Three new query fixtures with IR golden + SPARQL golden tests:
- whereSomeNot: .some().not() produces identical SPARQL to .none()
- whereEqualsNot: .equals().not() produces FILTER(!(?var = "value"))
- whereNoneAndEquals: .none().and() chains NOT EXISTS with equality

935 tests pass, 0 failures.

https://claude.ai/code/session_019MDcpswuKKNx9YhC2S2vT7
Complete the SPARQL golden coverage for all negation patterns:
- whereNeq: .neq() produces FILTER(?var != "value")
- whereExprNot: Expr.not() wrapping compound AND produces
  FILTER(!(?a && ?b))

939 tests pass, 0 failures.

https://claude.ai/code/session_019MDcpswuKKNx9YhC2S2vT7
Gap 4: Replace fragile structural check with direct T extends ExpressionNode
  for boolean type mapping — works now that Evaluation is fully removed
Gap 1: toExpressionNode() return type is always ExpressionNode (remove | null)
Gap 3: Update error messages in buildPredicateExpression

https://claude.ai/code/session_019MDcpswuKKNx9YhC2S2vT7
Gap 1: Remove stale | null from toExpressionNode return type
Gap 2: Update stale Evaluation references in report 010
Gap 3: Fix error message in buildPredicateExpression

939 tests pass, 0 failures.

https://claude.ai/code/session_019MDcpswuKKNx9YhC2S2vT7
@flyon flyon merged commit eeef5f8 into main Apr 3, 2026
3 checks passed
@flyon flyon deleted the claude/compare-orm-libraries-Qk11R branch April 3, 2026 15:04
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.

2 participants