Unify WHERE pipeline on ExpressionNode, retire Evaluation, add .none()#53
Merged
Unify WHERE pipeline on ExpressionNode, retire Evaluation, add .none()#53
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR completes negation support in the query DSL by unifying all WHERE conditions on
ExpressionNodeand retiring the legacyEvaluationclass. Added.none()collection quantifier for filtering where no elements match a condition.Key Changes
WHERE Pipeline Unification
Evaluationclass and its 4-stage pipeline (Evaluation → WherePath → DesugaredWhere → CanonicalWhere → IR).equals()now returnsExpressionNodeinstead ofEvaluation, enabling.not()chainingWhereClausetype to accept onlyExpressionNodeorExistsCondition(or callbacks returning them)WhereEvaluationPath,DesugaredWhereComparison,DesugaredWhereBoolean,CanonicalWhereComparison,CanonicalWhereLogical,CanonicalWhereExists,CanonicalWhereNottypesNew Collection Quantifier
.none(fn)method onQueryShapeSetfor "no elements match" filteringFILTER(NOT EXISTS { ... })in SPARQL.some(fn).not()but reads more naturallyExpression System Enhancements
ExistsConditiontype to represent collection quantifiers (.some(),.every(),.none())isExistsCondition()guard functiontracedAliasExpression()for root shape comparisonstoExpressionNode()to handle context references and alias expressionsIR Simplification
DesugaredWherenow only containsDesugaredExpressionWhereandDesugaredExistsWhereCanonicalWhereExpressionsimplified to onlyDesugaredExpressionWhereandDesugaredExistsWherelowerWhereArg()function (no longer needed)lowerWhere()now handles only expression and exists conditionsTest Coverage
.none(),.some().not(),.equals().not(), and chained negationsImplementation Details
The unification works by recognizing that
ExpressionNodeis already an IR representation, eliminating the need for intermediate transformation stages. Collection quantifiers (.some(),.every(),.none()) are handled separately viaExistsCondition, which lowers toIRExistsExpressionwith appropriate negation flags.All negation now flows through two mechanisms:
.not()method onExpressionNodefor boolean negationExistsConditionwith negation for collection quantifiersThis enables natural chaining like
p.name.equals('Alice').not()andp.friends.none(f => f.hobby.equals('Chess')).https://claude.ai/code/session_019MDcpswuKKNx9YhC2S2vT7