@nicia-ai/typegraph 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -313,72 +313,6 @@ var core = {
313
313
  implies
314
314
  };
315
315
 
316
- // src/query/compiler/passes/recursive.ts
317
- function runRecursiveTraversalSelectionPass(ast) {
318
- const variableLengthTraversal = ast.traversals.find(
319
- (traversal) => traversal.variableLength !== void 0
320
- );
321
- if (variableLengthTraversal === void 0) {
322
- throw new CompilerInvariantError("No variable-length traversal found");
323
- }
324
- if (ast.traversals.length > 1) {
325
- throw new UnsupportedPredicateError(
326
- "Variable-length traversals with multiple traversals are not yet supported. Please use a single variable-length traversal."
327
- );
328
- }
329
- return variableLengthTraversal;
330
- }
331
-
332
- // src/query/compiler/passes/runner.ts
333
- function runCompilerPass(state, pass) {
334
- const output = pass.execute(state);
335
- return {
336
- state: pass.update(state, output)
337
- };
338
- }
339
- function compileTemporalFilter(options) {
340
- const { mode, asOf, tableAlias, currentTimestamp } = options;
341
- const prefix = tableAlias ? sql.raw(`${tableAlias}.`) : sql.raw("");
342
- const deletedAt = sql`${prefix}deleted_at`;
343
- const validFrom = sql`${prefix}valid_from`;
344
- const validTo = sql`${prefix}valid_to`;
345
- switch (mode) {
346
- case "current": {
347
- const now = currentTimestamp ?? sql`CURRENT_TIMESTAMP`;
348
- return sql`${deletedAt} IS NULL AND (${validFrom} IS NULL OR ${validFrom} <= ${now}) AND (${validTo} IS NULL OR ${validTo} > ${now})`;
349
- }
350
- case "asOf": {
351
- const timestamp = asOf;
352
- return sql`${deletedAt} IS NULL AND (${validFrom} IS NULL OR ${validFrom} <= ${timestamp}) AND (${validTo} IS NULL OR ${validTo} > ${timestamp})`;
353
- }
354
- case "includeEnded": {
355
- return sql`${deletedAt} IS NULL`;
356
- }
357
- case "includeTombstones": {
358
- return sql.raw("1=1");
359
- }
360
- }
361
- }
362
- function extractTemporalOptions(ast, tableAlias) {
363
- return {
364
- mode: ast.temporalMode.mode,
365
- asOf: ast.temporalMode.asOf,
366
- tableAlias
367
- };
368
- }
369
-
370
- // src/query/compiler/passes/temporal.ts
371
- function createTemporalFilterPass(ast, currentTimestamp) {
372
- return {
373
- forAlias(tableAlias) {
374
- return compileTemporalFilter({
375
- ...extractTemporalOptions(ast, tableAlias),
376
- currentTimestamp
377
- });
378
- }
379
- };
380
- }
381
-
382
316
  // src/query/subquery-utils.ts
383
317
  function normalizeValueType(valueType) {
384
318
  if (valueType === void 0 || valueType === "unknown") {
@@ -1465,1179 +1399,1876 @@ function extractVectorSimilarityPredicates(predicates) {
1465
1399
  return results;
1466
1400
  }
1467
1401
 
1468
- // src/query/compiler/passes/vector.ts
1469
- function runVectorPredicatePass(ast, dialect) {
1470
- const vectorPredicates = extractVectorSimilarityPredicates(ast.predicates);
1471
- if (vectorPredicates.length > 1) {
1472
- throw new UnsupportedPredicateError(
1473
- "Multiple vector similarity predicates in a single query are not supported"
1402
+ // src/query/compiler/predicate-utils.ts
1403
+ var EMPTY_PREDICATES = [];
1404
+ function buildPredicateIndexKey(alias, targetType) {
1405
+ return `${alias}\0${targetType}`;
1406
+ }
1407
+ function resolvePredicateTargetType(predicate2) {
1408
+ return predicate2.targetType === "edge" ? "edge" : "node";
1409
+ }
1410
+ function buildPredicateIndex(ast) {
1411
+ const byAliasAndType = /* @__PURE__ */ new Map();
1412
+ for (const predicate2 of ast.predicates) {
1413
+ const key = buildPredicateIndexKey(
1414
+ predicate2.targetAlias,
1415
+ resolvePredicateTargetType(predicate2)
1474
1416
  );
1417
+ const existing = byAliasAndType.get(key);
1418
+ if (existing === void 0) {
1419
+ byAliasAndType.set(key, [predicate2]);
1420
+ } else {
1421
+ existing.push(predicate2);
1422
+ }
1475
1423
  }
1476
- const vectorPredicate = vectorPredicates[0];
1477
- if (vectorPredicate === void 0) {
1478
- return { vectorPredicate: void 0 };
1424
+ return { byAliasAndType };
1425
+ }
1426
+ function getPredicatesForAlias(predicateIndex, alias, targetType) {
1427
+ return predicateIndex.byAliasAndType.get(
1428
+ buildPredicateIndexKey(alias, targetType)
1429
+ ) ?? EMPTY_PREDICATES;
1430
+ }
1431
+ function compilePredicateClauses(predicates, predicateContext) {
1432
+ return predicates.map(
1433
+ (predicate2) => compilePredicateExpression(predicate2.expression, predicateContext)
1434
+ );
1435
+ }
1436
+ function compileKindFilter(column, kinds) {
1437
+ if (kinds.length === 0) {
1438
+ return sql`1 = 0`;
1479
1439
  }
1480
- const vectorStrategy = dialect.capabilities.vectorPredicateStrategy;
1481
- if (vectorStrategy === "unsupported" || !dialect.supportsVectors) {
1482
- throw new UnsupportedPredicateError(
1483
- `Vector similarity predicates are not supported for dialect "${dialect.name}"`
1484
- );
1440
+ if (kinds.length === 1) {
1441
+ return sql`${column} = ${kinds[0]}`;
1485
1442
  }
1486
- if (!dialect.capabilities.vectorMetrics.includes(vectorPredicate.metric)) {
1487
- throw new UnsupportedPredicateError(
1488
- `Vector metric "${vectorPredicate.metric}" is not supported for dialect "${dialect.name}"`
1489
- );
1443
+ return sql`${column} IN (${sql.join(
1444
+ kinds.map((kind) => sql`${kind}`),
1445
+ sql`, `
1446
+ )})`;
1447
+ }
1448
+ function getNodeKindsForAlias(ast, alias) {
1449
+ if (alias === ast.start.alias) {
1450
+ return ast.start.kinds;
1490
1451
  }
1491
- if (!Number.isFinite(vectorPredicate.limit) || vectorPredicate.limit <= 0) {
1492
- throw new UnsupportedPredicateError(
1493
- `Vector predicate limit must be a positive finite number, got ${String(vectorPredicate.limit)}`
1494
- );
1452
+ for (const traversal of ast.traversals) {
1453
+ if (traversal.nodeAlias === alias) {
1454
+ return traversal.nodeKinds;
1455
+ }
1495
1456
  }
1496
- const { minScore } = vectorPredicate;
1497
- if (minScore !== void 0) {
1498
- if (!Number.isFinite(minScore)) {
1499
- throw new UnsupportedPredicateError(
1500
- `Vector minScore must be a finite number, got ${String(minScore)}`
1501
- );
1457
+ throw new CompilerInvariantError(`Unknown traversal source alias: ${alias}`);
1458
+ }
1459
+
1460
+ // src/query/compiler/emitter/plan-inspector.ts
1461
+ function collectPlanOperations(node, ops) {
1462
+ ops.add(node.op);
1463
+ switch (node.op) {
1464
+ case "aggregate":
1465
+ case "filter":
1466
+ case "join":
1467
+ case "limit_offset":
1468
+ case "project":
1469
+ case "recursive_expand":
1470
+ case "sort":
1471
+ case "vector_knn": {
1472
+ collectPlanOperations(node.input, ops);
1473
+ return;
1502
1474
  }
1503
- if (vectorPredicate.metric === "cosine" && (minScore < -1 || minScore > 1)) {
1504
- throw new UnsupportedPredicateError(
1505
- `Cosine minScore must be between -1 and 1, got ${String(minScore)}`
1506
- );
1475
+ case "set_op": {
1476
+ collectPlanOperations(node.left, ops);
1477
+ collectPlanOperations(node.right, ops);
1478
+ return;
1479
+ }
1480
+ case "scan": {
1481
+ return;
1507
1482
  }
1508
1483
  }
1509
- return { vectorPredicate };
1510
1484
  }
1511
- function resolveVectorAwareLimit(astLimit, vectorPredicate) {
1512
- if (vectorPredicate === void 0) {
1513
- return astLimit;
1514
- }
1515
- if (astLimit === void 0) {
1516
- return vectorPredicate.limit;
1485
+ function findUnaryNodeInProjectChain(rootNode, op) {
1486
+ let currentNode = rootNode.input;
1487
+ for (; ; ) {
1488
+ if (currentNode.op === op) {
1489
+ return currentNode;
1490
+ }
1491
+ switch (currentNode.op) {
1492
+ case "aggregate":
1493
+ case "filter":
1494
+ case "join":
1495
+ case "limit_offset":
1496
+ case "recursive_expand":
1497
+ case "sort":
1498
+ case "vector_knn": {
1499
+ currentNode = currentNode.input;
1500
+ continue;
1501
+ }
1502
+ case "project":
1503
+ case "scan":
1504
+ case "set_op": {
1505
+ return void 0;
1506
+ }
1507
+ }
1517
1508
  }
1518
- return Math.min(astLimit, vectorPredicate.limit);
1519
1509
  }
1520
-
1521
- // src/query/compiler/plan/lowering.ts
1522
- function createPlanNodeIdFactory() {
1523
- let current = 0;
1524
- return function nextPlanNodeId() {
1525
- current += 1;
1526
- return `plan_${current.toString(36)}`;
1510
+ function inspectProjectPlan(logicalPlan) {
1511
+ if (logicalPlan.root.op !== "project") {
1512
+ throw new CompilerInvariantError(
1513
+ `SQL emitter expected logical plan root to be "project", got "${logicalPlan.root.op}"`,
1514
+ { component: "plan-inspector" }
1515
+ );
1516
+ }
1517
+ const operations = /* @__PURE__ */ new Set();
1518
+ collectPlanOperations(logicalPlan.root, operations);
1519
+ const limitOffsetNode = findUnaryNodeInProjectChain(
1520
+ logicalPlan.root,
1521
+ "limit_offset"
1522
+ );
1523
+ const sortNode = findUnaryNodeInProjectChain(
1524
+ logicalPlan.root,
1525
+ "sort"
1526
+ );
1527
+ return {
1528
+ hasAggregate: operations.has("aggregate"),
1529
+ hasLimitOffset: operations.has("limit_offset"),
1530
+ hasRecursiveExpand: operations.has("recursive_expand"),
1531
+ hasSetOperation: operations.has("set_op"),
1532
+ hasSort: operations.has("sort"),
1533
+ hasVectorKnn: operations.has("vector_knn"),
1534
+ limitOffsetNode,
1535
+ rootProjectNode: logicalPlan.root,
1536
+ sortNode
1527
1537
  };
1528
1538
  }
1529
- function extractAggregateExpressions(ast) {
1530
- const aggregates = [];
1531
- for (const field2 of ast.projection.fields) {
1532
- if ("__type" in field2.source && field2.source.__type === "aggregate") {
1533
- aggregates.push(field2.source);
1534
- }
1539
+ function inspectStandardProjectPlan(logicalPlan) {
1540
+ const shape = inspectProjectPlan(logicalPlan);
1541
+ if (shape.hasSetOperation) {
1542
+ throw new CompilerInvariantError(
1543
+ 'Standard SQL emitter does not support plans containing "set_op" nodes',
1544
+ { component: "plan-inspector" }
1545
+ );
1535
1546
  }
1536
- return aggregates;
1547
+ if (shape.hasRecursiveExpand) {
1548
+ throw new CompilerInvariantError(
1549
+ 'Standard SQL emitter does not support plans containing "recursive_expand" nodes',
1550
+ { component: "plan-inspector" }
1551
+ );
1552
+ }
1553
+ return shape;
1537
1554
  }
1538
- function getAliasPredicates(ast, alias, predicateTargetType) {
1539
- return ast.predicates.filter((predicate2) => {
1540
- const targetType = predicate2.targetType ?? "node";
1541
- return predicate2.targetAlias === alias && targetType === predicateTargetType;
1542
- });
1555
+ function inspectRecursiveProjectPlan(logicalPlan) {
1556
+ const shape = inspectProjectPlan(logicalPlan);
1557
+ if (!shape.hasRecursiveExpand) {
1558
+ throw new CompilerInvariantError(
1559
+ 'Recursive SQL emitter expected logical plan to contain a "recursive_expand" node',
1560
+ { component: "plan-inspector" }
1561
+ );
1562
+ }
1563
+ if (shape.hasSetOperation) {
1564
+ throw new CompilerInvariantError(
1565
+ 'Recursive SQL emitter does not support plans containing "set_op" nodes',
1566
+ { component: "plan-inspector" }
1567
+ );
1568
+ }
1569
+ return shape;
1543
1570
  }
1544
- function wrapWithAliasFilterNode(currentNode, ast, alias, predicateTargetType, nextPlanNodeId) {
1545
- const aliasPredicates = getAliasPredicates(ast, alias, predicateTargetType);
1546
- if (aliasPredicates.length === 0) {
1547
- return currentNode;
1571
+ function findTopLevelLimitOffsetNode(rootNode) {
1572
+ let currentNode = rootNode;
1573
+ for (; ; ) {
1574
+ if (currentNode.op === "limit_offset") {
1575
+ return currentNode;
1576
+ }
1577
+ switch (currentNode.op) {
1578
+ case "aggregate":
1579
+ case "filter":
1580
+ case "join":
1581
+ case "project":
1582
+ case "recursive_expand":
1583
+ case "sort":
1584
+ case "vector_knn": {
1585
+ currentNode = currentNode.input;
1586
+ continue;
1587
+ }
1588
+ case "scan":
1589
+ case "set_op": {
1590
+ return void 0;
1591
+ }
1592
+ }
1593
+ }
1594
+ }
1595
+ function findTopLevelSortNode(rootNode) {
1596
+ let currentNode = rootNode;
1597
+ for (; ; ) {
1598
+ if (currentNode.op === "sort") {
1599
+ return currentNode;
1600
+ }
1601
+ switch (currentNode.op) {
1602
+ case "aggregate":
1603
+ case "filter":
1604
+ case "join":
1605
+ case "limit_offset":
1606
+ case "project":
1607
+ case "recursive_expand":
1608
+ case "vector_knn": {
1609
+ currentNode = currentNode.input;
1610
+ continue;
1611
+ }
1612
+ case "scan":
1613
+ case "set_op": {
1614
+ return void 0;
1615
+ }
1616
+ }
1617
+ }
1618
+ }
1619
+ function inspectSetOperationPlan(logicalPlan) {
1620
+ const operations = /* @__PURE__ */ new Set();
1621
+ collectPlanOperations(logicalPlan.root, operations);
1622
+ if (!operations.has("set_op")) {
1623
+ throw new CompilerInvariantError(
1624
+ 'Set-operation SQL emitter expected logical plan to contain a "set_op" node',
1625
+ { component: "plan-inspector" }
1626
+ );
1548
1627
  }
1628
+ const limitOffsetNode = findTopLevelLimitOffsetNode(logicalPlan.root);
1629
+ const sortNode = findTopLevelSortNode(logicalPlan.root);
1549
1630
  return {
1550
- alias,
1551
- id: nextPlanNodeId(),
1552
- input: currentNode,
1553
- op: "filter",
1554
- predicateTargetType,
1555
- predicates: aliasPredicates.map((predicate2) => predicate2.expression)
1631
+ hasLimitOffset: limitOffsetNode !== void 0,
1632
+ hasSetOperation: true,
1633
+ hasSort: sortNode !== void 0,
1634
+ limitOffsetNode,
1635
+ sortNode
1556
1636
  };
1557
1637
  }
1558
- function appendAggregateSortLimitAndProjectNodes(currentNode, ast, nextPlanNodeId, limit, collapsedTraversalCteAlias) {
1559
- let node = currentNode;
1560
- const aggregateExpressions = extractAggregateExpressions(ast);
1561
- if (aggregateExpressions.length > 0 || ast.groupBy !== void 0 || ast.having !== void 0) {
1562
- const aggregateNode = {
1563
- aggregates: aggregateExpressions,
1564
- groupBy: ast.groupBy?.fields ?? [],
1565
- id: nextPlanNodeId(),
1566
- input: node,
1567
- op: "aggregate"
1568
- };
1569
- node = ast.having === void 0 ? aggregateNode : { ...aggregateNode, having: ast.having };
1638
+ function assertRecursiveEmitterClauseAlignment(logicalPlan, input) {
1639
+ const planShape = inspectRecursiveProjectPlan(logicalPlan);
1640
+ if (planShape.hasSort && input.orderBy === void 0) {
1641
+ throw new CompilerInvariantError(
1642
+ "Recursive SQL emitter expected ORDER BY clause for plan containing a sort node",
1643
+ { component: "recursive-emitter" }
1644
+ );
1570
1645
  }
1571
- if (ast.orderBy !== void 0 && ast.orderBy.length > 0) {
1572
- node = {
1573
- id: nextPlanNodeId(),
1574
- input: node,
1575
- op: "sort",
1576
- orderBy: ast.orderBy
1577
- };
1646
+ if (!planShape.hasSort && input.orderBy !== void 0) {
1647
+ throw new CompilerInvariantError(
1648
+ "Recursive SQL emitter received ORDER BY clause for a plan without sort nodes",
1649
+ { component: "recursive-emitter" }
1650
+ );
1578
1651
  }
1579
- if (limit !== void 0 || ast.offset !== void 0) {
1580
- const limitOffsetNodeBase = {
1581
- id: nextPlanNodeId(),
1582
- input: node,
1583
- op: "limit_offset"
1584
- };
1585
- const hasLimit = limit !== void 0;
1586
- const hasOffset = ast.offset !== void 0;
1587
- if (hasLimit && hasOffset) {
1588
- node = {
1589
- ...limitOffsetNodeBase,
1590
- limit,
1591
- offset: ast.offset
1592
- };
1593
- } else if (hasLimit) {
1594
- node = { ...limitOffsetNodeBase, limit };
1595
- } else if (hasOffset) {
1596
- node = { ...limitOffsetNodeBase, offset: ast.offset };
1597
- } else {
1652
+ if (planShape.hasLimitOffset && input.limitOffset === void 0) {
1653
+ throw new CompilerInvariantError(
1654
+ "Recursive SQL emitter expected LIMIT/OFFSET clause for plan containing a limit_offset node",
1655
+ { component: "recursive-emitter" }
1656
+ );
1657
+ }
1658
+ if (!planShape.hasLimitOffset && input.limitOffset !== void 0) {
1659
+ throw new CompilerInvariantError(
1660
+ "Recursive SQL emitter received LIMIT/OFFSET clause for a plan without limit_offset nodes",
1661
+ { component: "recursive-emitter" }
1662
+ );
1663
+ }
1664
+ }
1665
+ function emitRecursiveQuerySql(input) {
1666
+ assertRecursiveEmitterClauseAlignment(input.logicalPlan, input);
1667
+ const parts = [
1668
+ sql`WITH RECURSIVE`,
1669
+ input.recursiveCte,
1670
+ sql`SELECT ${input.projection}`,
1671
+ sql`FROM recursive_cte`,
1672
+ input.depthFilter
1673
+ ];
1674
+ if (input.orderBy !== void 0) {
1675
+ parts.push(input.orderBy);
1676
+ }
1677
+ if (input.limitOffset !== void 0) {
1678
+ parts.push(input.limitOffset);
1679
+ }
1680
+ return sql.join(parts, sql` `);
1681
+ }
1682
+ function assertSetOperationEmitterClauseAlignment(logicalPlan, suffixClauses) {
1683
+ const shape = inspectSetOperationPlan(logicalPlan);
1684
+ const hasSuffixClauses = suffixClauses !== void 0 && suffixClauses.length > 0;
1685
+ if (!shape.hasSort && !shape.hasLimitOffset && hasSuffixClauses) {
1686
+ throw new CompilerInvariantError(
1687
+ "Set-operation SQL emitter received suffix clauses for a plan without top-level sort or limit_offset nodes",
1688
+ { component: "set-operation-emitter" }
1689
+ );
1690
+ }
1691
+ if (!hasSuffixClauses) {
1692
+ if (shape.hasSort || shape.hasLimitOffset) {
1598
1693
  throw new CompilerInvariantError(
1599
- "limit_offset node requires limit or offset to be present"
1694
+ "Set-operation SQL emitter expected suffix clauses for plan containing top-level sort or limit_offset nodes",
1695
+ { component: "set-operation-emitter" }
1600
1696
  );
1601
1697
  }
1698
+ return;
1602
1699
  }
1603
- const projectNodeBase = {
1604
- fields: ast.projection.fields,
1605
- id: nextPlanNodeId(),
1606
- input: node,
1607
- op: "project"
1608
- };
1609
- return collapsedTraversalCteAlias === void 0 ? projectNodeBase : {
1610
- ...projectNodeBase,
1611
- collapsedTraversalAlias: collapsedTraversalCteAlias
1612
- };
1613
- }
1614
- function lowerStandardQueryToLogicalPlanNode(input) {
1615
- const { ast, nextPlanNodeId } = input;
1616
- let currentNode = {
1617
- alias: ast.start.alias,
1618
- graphId: input.graphId,
1619
- id: nextPlanNodeId(),
1620
- kinds: ast.start.kinds,
1621
- op: "scan",
1622
- source: "nodes"
1623
- };
1624
- currentNode = wrapWithAliasFilterNode(
1625
- currentNode,
1626
- ast,
1627
- ast.start.alias,
1628
- "node",
1629
- nextPlanNodeId
1630
- );
1631
- for (const traversal of ast.traversals) {
1632
- currentNode = {
1633
- direction: traversal.direction,
1634
- edgeAlias: traversal.edgeAlias,
1635
- edgeKinds: traversal.edgeKinds,
1636
- id: nextPlanNodeId(),
1637
- input: currentNode,
1638
- inverseEdgeKinds: traversal.inverseEdgeKinds ?? [],
1639
- joinFromAlias: traversal.joinFromAlias,
1640
- joinType: traversal.optional ? "left" : "inner",
1641
- nodeAlias: traversal.nodeAlias,
1642
- nodeKinds: traversal.nodeKinds,
1643
- op: "join"
1644
- };
1645
- currentNode = wrapWithAliasFilterNode(
1646
- currentNode,
1647
- ast,
1648
- traversal.edgeAlias,
1649
- "edge",
1650
- nextPlanNodeId
1651
- );
1652
- currentNode = wrapWithAliasFilterNode(
1653
- currentNode,
1654
- ast,
1655
- traversal.nodeAlias,
1656
- "node",
1657
- nextPlanNodeId
1700
+ const limitOffsetClauseCount = shape.limitOffsetNode === void 0 ? 0 : (shape.limitOffsetNode.limit === void 0 ? 0 : 1) + (shape.limitOffsetNode.offset === void 0 ? 0 : 1);
1701
+ const expectedClauseCount = (shape.sortNode === void 0 ? 0 : 1) + limitOffsetClauseCount;
1702
+ if (suffixClauses.length !== expectedClauseCount) {
1703
+ throw new CompilerInvariantError(
1704
+ `Set-operation SQL emitter expected ${String(expectedClauseCount)} top-level suffix clause(s) from logical plan, got ${String(suffixClauses.length)}`,
1705
+ { component: "set-operation-emitter" }
1658
1706
  );
1659
1707
  }
1660
- if (input.vectorPredicate !== void 0) {
1661
- currentNode = {
1662
- id: nextPlanNodeId(),
1663
- input: currentNode,
1664
- op: "vector_knn",
1665
- predicate: input.vectorPredicate
1666
- };
1667
- }
1668
- return appendAggregateSortLimitAndProjectNodes(
1669
- currentNode,
1670
- ast,
1671
- nextPlanNodeId,
1672
- input.effectiveLimit,
1673
- input.collapsedTraversalCteAlias
1674
- );
1675
1708
  }
1676
- function lowerRecursiveQueryToLogicalPlanNode(input) {
1677
- const { ast, nextPlanNodeId } = input;
1678
- const traversal = input.traversal ?? runRecursiveTraversalSelectionPass(input.ast);
1679
- let currentNode = {
1680
- alias: ast.start.alias,
1681
- graphId: input.graphId,
1682
- id: nextPlanNodeId(),
1683
- kinds: ast.start.kinds,
1684
- op: "scan",
1685
- source: "nodes"
1686
- };
1687
- currentNode = wrapWithAliasFilterNode(
1688
- currentNode,
1689
- ast,
1690
- ast.start.alias,
1691
- "node",
1692
- nextPlanNodeId
1693
- );
1694
- currentNode = {
1695
- edgeAlias: traversal.edgeAlias,
1696
- edgeKinds: traversal.edgeKinds,
1697
- id: nextPlanNodeId(),
1698
- input: currentNode,
1699
- inverseEdgeKinds: traversal.inverseEdgeKinds ?? [],
1700
- nodeAlias: traversal.nodeAlias,
1701
- nodeKinds: traversal.nodeKinds,
1702
- op: "recursive_expand",
1703
- traversal: traversal.variableLength
1704
- };
1705
- currentNode = wrapWithAliasFilterNode(
1706
- currentNode,
1707
- ast,
1708
- traversal.edgeAlias,
1709
- "edge",
1710
- nextPlanNodeId
1711
- );
1712
- currentNode = wrapWithAliasFilterNode(
1713
- currentNode,
1714
- ast,
1715
- traversal.nodeAlias,
1716
- "node",
1717
- nextPlanNodeId
1718
- );
1719
- return appendAggregateSortLimitAndProjectNodes(
1720
- currentNode,
1721
- ast,
1722
- nextPlanNodeId,
1723
- ast.limit
1709
+ function emitSetOperationQuerySql(input) {
1710
+ assertSetOperationEmitterClauseAlignment(
1711
+ input.logicalPlan,
1712
+ input.suffixClauses
1724
1713
  );
1714
+ const parts = [];
1715
+ if (input.ctes !== void 0 && input.ctes.length > 0) {
1716
+ parts.push(sql`WITH ${sql.join([...input.ctes], sql`, `)}`);
1717
+ }
1718
+ parts.push(input.baseQuery);
1719
+ if (input.suffixClauses !== void 0 && input.suffixClauses.length > 0) {
1720
+ parts.push(...input.suffixClauses);
1721
+ }
1722
+ return sql.join(parts, sql` `);
1725
1723
  }
1726
- function lowerComposableQueryToLogicalPlanNode(query, dialect, graphId, nextPlanNodeId) {
1727
- if ("__type" in query) {
1728
- return lowerSetOperationToLogicalPlanNode(
1729
- query,
1730
- graphId,
1731
- dialect,
1732
- nextPlanNodeId
1724
+ function assertStandardEmitterClauseAlignment(logicalPlan, input) {
1725
+ const planShape = inspectStandardProjectPlan(logicalPlan);
1726
+ if (input.groupBy !== void 0 && !planShape.hasAggregate) {
1727
+ throw new CompilerInvariantError(
1728
+ "Standard SQL emitter received GROUP BY clause for a plan without aggregate nodes",
1729
+ { component: "standard-emitter" }
1733
1730
  );
1734
1731
  }
1735
- const hasVariableLengthTraversal2 = query.traversals.some(
1736
- (traversal) => traversal.variableLength !== void 0
1737
- );
1738
- if (hasVariableLengthTraversal2) {
1739
- return lowerRecursiveQueryToLogicalPlanNode({
1740
- ast: query,
1741
- graphId,
1742
- nextPlanNodeId
1743
- });
1732
+ if (input.having !== void 0 && !planShape.hasAggregate) {
1733
+ throw new CompilerInvariantError(
1734
+ "Standard SQL emitter received HAVING clause for a plan without aggregate nodes",
1735
+ { component: "standard-emitter" }
1736
+ );
1744
1737
  }
1745
- const vectorPredicate = runVectorPredicatePass(
1746
- query,
1747
- getDialect(dialect)
1748
- ).vectorPredicate;
1749
- const effectiveLimit = resolveVectorAwareLimit(query.limit, vectorPredicate);
1750
- const loweringInput = {
1751
- ast: query,
1752
- graphId,
1753
- nextPlanNodeId,
1754
- ...effectiveLimit === void 0 ? {} : { effectiveLimit },
1755
- ...vectorPredicate === void 0 ? {} : { vectorPredicate }
1756
- };
1757
- return lowerStandardQueryToLogicalPlanNode(loweringInput);
1758
- }
1759
- function lowerSetOperationToLogicalPlanNode(op, graphId, dialect, nextPlanNodeId) {
1760
- let currentNode = {
1761
- id: nextPlanNodeId(),
1762
- left: lowerComposableQueryToLogicalPlanNode(
1763
- op.left,
1764
- dialect,
1765
- graphId,
1766
- nextPlanNodeId
1767
- ),
1768
- op: "set_op",
1769
- operator: op.operator,
1770
- right: lowerComposableQueryToLogicalPlanNode(
1771
- op.right,
1772
- dialect,
1773
- graphId,
1774
- nextPlanNodeId
1775
- )
1776
- };
1777
- if (op.orderBy !== void 0 && op.orderBy.length > 0) {
1778
- currentNode = {
1779
- id: nextPlanNodeId(),
1780
- input: currentNode,
1781
- op: "sort",
1782
- orderBy: op.orderBy
1783
- };
1738
+ const expectsOrderBy = planShape.hasSort || planShape.hasVectorKnn;
1739
+ if (expectsOrderBy && input.orderBy === void 0) {
1740
+ throw new CompilerInvariantError(
1741
+ "Standard SQL emitter expected ORDER BY clause for plan containing a sort or vector_knn node",
1742
+ { component: "standard-emitter" }
1743
+ );
1784
1744
  }
1785
- if (op.limit !== void 0 || op.offset !== void 0) {
1786
- const limitOffsetBase = {
1787
- id: nextPlanNodeId(),
1788
- input: currentNode,
1789
- op: "limit_offset"
1790
- };
1791
- if (op.limit !== void 0 && op.offset !== void 0) {
1792
- currentNode = { ...limitOffsetBase, limit: op.limit, offset: op.offset };
1793
- } else if (op.limit === void 0) {
1794
- currentNode = { ...limitOffsetBase, offset: op.offset };
1795
- } else {
1796
- currentNode = { ...limitOffsetBase, limit: op.limit };
1797
- }
1745
+ if (!expectsOrderBy && input.orderBy !== void 0) {
1746
+ throw new CompilerInvariantError(
1747
+ "Standard SQL emitter received ORDER BY clause for a plan without sort or vector_knn nodes",
1748
+ { component: "standard-emitter" }
1749
+ );
1750
+ }
1751
+ if (planShape.hasLimitOffset && input.limitOffset === void 0) {
1752
+ throw new CompilerInvariantError(
1753
+ "Standard SQL emitter expected LIMIT/OFFSET clause for plan containing a limit_offset node",
1754
+ { component: "standard-emitter" }
1755
+ );
1756
+ }
1757
+ if (!planShape.hasLimitOffset && input.limitOffset !== void 0) {
1758
+ throw new CompilerInvariantError(
1759
+ "Standard SQL emitter received LIMIT/OFFSET clause for a plan without limit_offset nodes",
1760
+ { component: "standard-emitter" }
1761
+ );
1798
1762
  }
1799
- return currentNode;
1800
- }
1801
- function lowerStandardQueryToLogicalPlan(input) {
1802
- const nextPlanNodeId = createPlanNodeIdFactory();
1803
- return {
1804
- metadata: {
1805
- dialect: input.dialect,
1806
- graphId: input.graphId
1807
- },
1808
- root: lowerStandardQueryToLogicalPlanNode({
1809
- ...input,
1810
- nextPlanNodeId
1811
- })
1812
- };
1813
- }
1814
- function lowerRecursiveQueryToLogicalPlan(input) {
1815
- const nextPlanNodeId = createPlanNodeIdFactory();
1816
- return {
1817
- metadata: {
1818
- dialect: input.dialect,
1819
- graphId: input.graphId
1820
- },
1821
- root: lowerRecursiveQueryToLogicalPlanNode({
1822
- ...input,
1823
- nextPlanNodeId
1824
- })
1825
- };
1826
1763
  }
1827
- function lowerSetOperationToLogicalPlan(input) {
1828
- const nextPlanNodeId = createPlanNodeIdFactory();
1829
- return {
1830
- metadata: {
1831
- dialect: input.dialect,
1832
- graphId: input.graphId
1833
- },
1834
- root: lowerSetOperationToLogicalPlanNode(
1835
- input.op,
1836
- input.graphId,
1837
- input.dialect,
1838
- nextPlanNodeId
1839
- )
1840
- };
1764
+ function emitStandardQuerySql(input) {
1765
+ assertStandardEmitterClauseAlignment(input.logicalPlan, input);
1766
+ const parts = [];
1767
+ if (input.ctes.length > 0) {
1768
+ parts.push(sql`WITH ${sql.join([...input.ctes], sql`, `)}`);
1769
+ }
1770
+ parts.push(sql`SELECT ${input.projection}`, input.fromClause);
1771
+ if (input.groupBy !== void 0) {
1772
+ parts.push(input.groupBy);
1773
+ }
1774
+ if (input.having !== void 0) {
1775
+ parts.push(input.having);
1776
+ }
1777
+ if (input.orderBy !== void 0) {
1778
+ parts.push(input.orderBy);
1779
+ }
1780
+ if (input.limitOffset !== void 0) {
1781
+ parts.push(input.limitOffset);
1782
+ }
1783
+ return sql.join(parts, sql` `);
1841
1784
  }
1842
1785
 
1843
- // src/query/compiler/emitter/plan-inspector.ts
1844
- function collectPlanOperations(node, ops) {
1845
- ops.add(node.op);
1846
- switch (node.op) {
1847
- case "aggregate":
1848
- case "filter":
1849
- case "join":
1850
- case "limit_offset":
1851
- case "project":
1852
- case "recursive_expand":
1853
- case "sort":
1854
- case "vector_knn": {
1855
- collectPlanOperations(node.input, ops);
1856
- return;
1786
+ // src/query/compiler/typed-json-extract.ts
1787
+ function compileTypedJsonExtract(input) {
1788
+ const { column, dialect, pointer, valueType } = input;
1789
+ const fallback = input.fallback ?? "json";
1790
+ switch (valueType) {
1791
+ case "string": {
1792
+ return dialect.jsonExtractText(column, pointer);
1857
1793
  }
1858
- case "set_op": {
1859
- collectPlanOperations(node.left, ops);
1860
- collectPlanOperations(node.right, ops);
1861
- return;
1794
+ case "number": {
1795
+ return dialect.jsonExtractNumber(column, pointer);
1862
1796
  }
1863
- case "scan": {
1864
- return;
1797
+ case "boolean": {
1798
+ return dialect.jsonExtractBoolean(column, pointer);
1865
1799
  }
1866
- }
1867
- }
1868
- function findUnaryNodeInProjectChain(rootNode, op) {
1869
- let currentNode = rootNode.input;
1870
- for (; ; ) {
1871
- if (currentNode.op === op) {
1872
- return currentNode;
1800
+ case "date": {
1801
+ return dialect.jsonExtractDate(column, pointer);
1873
1802
  }
1874
- switch (currentNode.op) {
1875
- case "aggregate":
1876
- case "filter":
1877
- case "join":
1878
- case "limit_offset":
1879
- case "recursive_expand":
1880
- case "sort":
1881
- case "vector_knn": {
1882
- currentNode = currentNode.input;
1883
- continue;
1884
- }
1885
- case "project":
1886
- case "scan":
1887
- case "set_op": {
1888
- return void 0;
1889
- }
1803
+ case "array":
1804
+ case "object":
1805
+ case "embedding":
1806
+ case "unknown":
1807
+ case void 0: {
1808
+ return fallback === "text" ? dialect.jsonExtractText(column, pointer) : dialect.jsonExtract(column, pointer);
1809
+ }
1810
+ default: {
1811
+ return fallback === "text" ? dialect.jsonExtractText(column, pointer) : dialect.jsonExtract(column, pointer);
1890
1812
  }
1891
1813
  }
1892
1814
  }
1893
- function inspectProjectPlan(logicalPlan) {
1894
- if (logicalPlan.root.op !== "project") {
1895
- throw new CompilerInvariantError(
1896
- `SQL emitter expected logical plan root to be "project", got "${logicalPlan.root.op}"`,
1897
- { component: "plan-inspector" }
1898
- );
1899
- }
1900
- const operations = /* @__PURE__ */ new Set();
1901
- collectPlanOperations(logicalPlan.root, operations);
1902
- const limitOffsetNode = findUnaryNodeInProjectChain(
1903
- logicalPlan.root,
1904
- "limit_offset"
1905
- );
1906
- const sortNode = findUnaryNodeInProjectChain(
1907
- logicalPlan.root,
1908
- "sort"
1909
- );
1910
- return {
1911
- hasAggregate: operations.has("aggregate"),
1912
- hasLimitOffset: operations.has("limit_offset"),
1913
- hasRecursiveExpand: operations.has("recursive_expand"),
1914
- hasSetOperation: operations.has("set_op"),
1915
- hasSort: operations.has("sort"),
1916
- hasVectorKnn: operations.has("vector_knn"),
1917
- limitOffsetNode,
1918
- rootProjectNode: logicalPlan.root,
1919
- sortNode
1920
- };
1815
+ var NODE_COLUMNS = [
1816
+ "id",
1817
+ "kind",
1818
+ "props",
1819
+ "version",
1820
+ "valid_from",
1821
+ "valid_to",
1822
+ "created_at",
1823
+ "updated_at",
1824
+ "deleted_at"
1825
+ ];
1826
+ var EDGE_COLUMNS = [
1827
+ "id",
1828
+ "kind",
1829
+ "from_id",
1830
+ "to_id",
1831
+ "props",
1832
+ "valid_from",
1833
+ "valid_to",
1834
+ "created_at",
1835
+ "updated_at",
1836
+ "deleted_at"
1837
+ ];
1838
+ var EMPTY_REQUIRED_COLUMNS = /* @__PURE__ */ new Set();
1839
+ function quoteIdentifier(identifier) {
1840
+ return sql.raw(`"${identifier.replaceAll('"', '""')}"`);
1921
1841
  }
1922
- function inspectStandardProjectPlan(logicalPlan) {
1923
- const shape = inspectProjectPlan(logicalPlan);
1924
- if (shape.hasSetOperation) {
1925
- throw new CompilerInvariantError(
1926
- 'Standard SQL emitter does not support plans containing "set_op" nodes',
1927
- { component: "plan-inspector" }
1928
- );
1929
- }
1930
- if (shape.hasRecursiveExpand) {
1931
- throw new CompilerInvariantError(
1932
- 'Standard SQL emitter does not support plans containing "recursive_expand" nodes',
1933
- { component: "plan-inspector" }
1934
- );
1935
- }
1936
- return shape;
1842
+ function shouldProjectColumn(requiredColumns, column, alwaysRequiredColumns) {
1843
+ if (alwaysRequiredColumns?.has(column)) return true;
1844
+ if (requiredColumns === void 0) return true;
1845
+ return requiredColumns.has(column);
1937
1846
  }
1938
- function inspectRecursiveProjectPlan(logicalPlan) {
1939
- const shape = inspectProjectPlan(logicalPlan);
1940
- if (!shape.hasRecursiveExpand) {
1941
- throw new CompilerInvariantError(
1942
- 'Recursive SQL emitter expected logical plan to contain a "recursive_expand" node',
1943
- { component: "plan-inspector" }
1944
- );
1945
- }
1946
- if (shape.hasSetOperation) {
1947
- throw new CompilerInvariantError(
1948
- 'Recursive SQL emitter does not support plans containing "set_op" nodes',
1949
- { component: "plan-inspector" }
1950
- );
1847
+ function addRequiredColumn(requiredColumnsByAlias, alias, column) {
1848
+ const existing = requiredColumnsByAlias.get(alias);
1849
+ if (existing) {
1850
+ existing.add(column);
1851
+ return;
1951
1852
  }
1952
- return shape;
1853
+ requiredColumnsByAlias.set(alias, /* @__PURE__ */ new Set([column]));
1953
1854
  }
1954
- function findTopLevelLimitOffsetNode(rootNode) {
1955
- let currentNode = rootNode;
1956
- for (; ; ) {
1957
- if (currentNode.op === "limit_offset") {
1958
- return currentNode;
1959
- }
1960
- switch (currentNode.op) {
1961
- case "aggregate":
1962
- case "filter":
1963
- case "join":
1964
- case "project":
1965
- case "recursive_expand":
1966
- case "sort":
1967
- case "vector_knn": {
1968
- currentNode = currentNode.input;
1969
- continue;
1970
- }
1971
- case "scan":
1972
- case "set_op": {
1973
- return void 0;
1974
- }
1975
- }
1976
- }
1855
+ function markFieldRefAsRequired(requiredColumnsByAlias, field2) {
1856
+ const column = field2.path[0];
1857
+ if (column === void 0) return;
1858
+ addRequiredColumn(requiredColumnsByAlias, field2.alias, column);
1977
1859
  }
1978
- function findTopLevelSortNode(rootNode) {
1979
- let currentNode = rootNode;
1980
- for (; ; ) {
1981
- if (currentNode.op === "sort") {
1982
- return currentNode;
1983
- }
1984
- switch (currentNode.op) {
1985
- case "aggregate":
1986
- case "filter":
1987
- case "join":
1988
- case "limit_offset":
1989
- case "project":
1990
- case "recursive_expand":
1991
- case "vector_knn": {
1992
- currentNode = currentNode.input;
1993
- continue;
1994
- }
1995
- case "scan":
1996
- case "set_op": {
1997
- return void 0;
1998
- }
1999
- }
1860
+ function mapSelectiveSystemFieldToColumn(field2) {
1861
+ if (field2 === "fromId") return "from_id";
1862
+ if (field2 === "toId") return "to_id";
1863
+ if (field2.startsWith("meta.")) {
1864
+ return field2.slice(5).replaceAll(/([A-Z])/g, "_$1").toLowerCase();
2000
1865
  }
1866
+ return field2;
2001
1867
  }
2002
- function inspectSetOperationPlan(logicalPlan) {
2003
- const operations = /* @__PURE__ */ new Set();
2004
- collectPlanOperations(logicalPlan.root, operations);
2005
- if (!operations.has("set_op")) {
2006
- throw new CompilerInvariantError(
2007
- 'Set-operation SQL emitter expected logical plan to contain a "set_op" node',
2008
- { component: "plan-inspector" }
1868
+ function markSelectiveFieldAsRequired(requiredColumnsByAlias, field2) {
1869
+ if (field2.isSystemField) {
1870
+ addRequiredColumn(
1871
+ requiredColumnsByAlias,
1872
+ field2.alias,
1873
+ mapSelectiveSystemFieldToColumn(field2.field)
2009
1874
  );
1875
+ return;
2010
1876
  }
2011
- const limitOffsetNode = findTopLevelLimitOffsetNode(logicalPlan.root);
2012
- const sortNode = findTopLevelSortNode(logicalPlan.root);
2013
- return {
2014
- hasLimitOffset: limitOffsetNode !== void 0,
2015
- hasSetOperation: true,
2016
- hasSort: sortNode !== void 0,
2017
- limitOffsetNode,
2018
- sortNode
2019
- };
1877
+ addRequiredColumn(requiredColumnsByAlias, field2.alias, "props");
2020
1878
  }
2021
- function assertRecursiveEmitterClauseAlignment(logicalPlan, input) {
2022
- const planShape = inspectRecursiveProjectPlan(logicalPlan);
2023
- if (planShape.hasSort && input.orderBy === void 0) {
2024
- throw new CompilerInvariantError(
2025
- "Recursive SQL emitter expected ORDER BY clause for plan containing a sort node",
2026
- { component: "recursive-emitter" }
2027
- );
2028
- }
2029
- if (!planShape.hasSort && input.orderBy !== void 0) {
2030
- throw new CompilerInvariantError(
2031
- "Recursive SQL emitter received ORDER BY clause for a plan without sort nodes",
2032
- { component: "recursive-emitter" }
2033
- );
2034
- }
2035
- if (planShape.hasLimitOffset && input.limitOffset === void 0) {
2036
- throw new CompilerInvariantError(
2037
- "Recursive SQL emitter expected LIMIT/OFFSET clause for plan containing a limit_offset node",
2038
- { component: "recursive-emitter" }
2039
- );
2040
- }
2041
- if (!planShape.hasLimitOffset && input.limitOffset !== void 0) {
2042
- throw new CompilerInvariantError(
2043
- "Recursive SQL emitter received LIMIT/OFFSET clause for a plan without limit_offset nodes",
2044
- { component: "recursive-emitter" }
2045
- );
1879
+ function isIdFieldRef(field2) {
1880
+ return field2.path.length === 1 && field2.path[0] === "id" && field2.jsonPointer === void 0;
1881
+ }
1882
+ function isAggregateExpr(source) {
1883
+ return "__type" in source && source.__type === "aggregate";
1884
+ }
1885
+
1886
+ // src/query/compiler/emitter/standard-builders.ts
1887
+ function compileColumnReference(tableAlias, column) {
1888
+ if (tableAlias === void 0) {
1889
+ return sql.raw(column);
2046
1890
  }
1891
+ return sql`${sql.raw(tableAlias)}.${sql.raw(column)}`;
2047
1892
  }
2048
- function emitRecursiveQuerySql(input) {
2049
- assertRecursiveEmitterClauseAlignment(input.logicalPlan, input);
2050
- const parts = [
2051
- sql`WITH RECURSIVE`,
2052
- input.recursiveCte,
2053
- sql`SELECT ${input.projection}`,
2054
- sql`FROM recursive_cte`,
2055
- input.depthFilter
1893
+ function compileNodeSelectColumns(tableAlias, alias, requiredColumns) {
1894
+ return NODE_COLUMNS.filter(
1895
+ (column) => column === "id" || column === "kind" || shouldProjectColumn(requiredColumns, column)
1896
+ ).map(
1897
+ (column) => sql`${compileColumnReference(tableAlias, column)} AS ${sql.raw(`${alias}_${column}`)}`
1898
+ );
1899
+ }
1900
+ function compileEdgeSelectColumns(tableAlias, alias, requiredColumns) {
1901
+ return EDGE_COLUMNS.filter(
1902
+ (column) => shouldProjectColumn(requiredColumns, column)
1903
+ ).map(
1904
+ (column) => sql`${compileColumnReference(tableAlias, column)} AS ${sql.raw(`${alias}_${column}`)}`
1905
+ );
1906
+ }
1907
+ function buildStandardStartCte(input) {
1908
+ const { ast, ctx, graphId, predicateIndex, requiredColumnsByAlias } = input;
1909
+ const alias = ast.start.alias;
1910
+ const kinds = ast.start.kinds;
1911
+ const kindFilter = compileKindFilter(sql.raw("kind"), kinds);
1912
+ const temporalFilter = input.temporalFilterPass.forAlias();
1913
+ const cteContext = { ...ctx, cteColumnPrefix: "" };
1914
+ const predicateClauses = compilePredicateClauses(
1915
+ getPredicatesForAlias(predicateIndex, alias, "node"),
1916
+ cteContext
1917
+ );
1918
+ const whereClauses = [
1919
+ sql`graph_id = ${graphId}`,
1920
+ kindFilter,
1921
+ temporalFilter,
1922
+ ...predicateClauses
2056
1923
  ];
2057
- if (input.orderBy !== void 0) {
2058
- parts.push(input.orderBy);
1924
+ const effectiveRequiredColumns = requiredColumnsByAlias ? requiredColumnsByAlias.get(alias) ?? EMPTY_REQUIRED_COLUMNS : void 0;
1925
+ return sql`
1926
+ cte_${sql.raw(alias)} AS (
1927
+ SELECT ${sql.join(
1928
+ compileNodeSelectColumns(void 0, alias, effectiveRequiredColumns),
1929
+ sql`, `
1930
+ )}
1931
+ FROM ${ctx.schema.nodesTable}
1932
+ WHERE ${sql.join(whereClauses, sql` AND `)}
1933
+ )
1934
+ `;
1935
+ }
1936
+ function buildStandardTraversalCte(input) {
1937
+ const {
1938
+ ast,
1939
+ carryForwardPreviousColumns,
1940
+ ctx,
1941
+ graphId,
1942
+ materializeCte,
1943
+ predicateIndex,
1944
+ requiredColumnsByAlias,
1945
+ temporalFilterPass,
1946
+ traversalIndex,
1947
+ traversalLimit
1948
+ } = input;
1949
+ const traversal = ast.traversals[traversalIndex];
1950
+ const traversalLimitValue = traversalIndex === ast.traversals.length - 1 ? traversalLimit : void 0;
1951
+ const previousNodeKinds = getNodeKindsForAlias(ast, traversal.joinFromAlias);
1952
+ const directEdgeKinds = [...new Set(traversal.edgeKinds)];
1953
+ const inverseEdgeKinds = traversal.inverseEdgeKinds === void 0 ? [] : [...new Set(traversal.inverseEdgeKinds)];
1954
+ const nodeKinds = traversal.nodeKinds;
1955
+ const nodeKindFilter = compileKindFilter(sql.raw("n.kind"), nodeKinds);
1956
+ const edgeTemporalFilter = temporalFilterPass.forAlias("e");
1957
+ const nodeTemporalFilter = temporalFilterPass.forAlias("n");
1958
+ const nodeCteContext = {
1959
+ ...ctx,
1960
+ cteColumnPrefix: "n"
1961
+ };
1962
+ const nodePredicateClauses = compilePredicateClauses(
1963
+ getPredicatesForAlias(predicateIndex, traversal.nodeAlias, "node"),
1964
+ nodeCteContext
1965
+ );
1966
+ const edgeCteContext = {
1967
+ ...ctx,
1968
+ cteColumnPrefix: "e"
1969
+ };
1970
+ const edgePredicateClauses = compilePredicateClauses(
1971
+ getPredicatesForAlias(predicateIndex, traversal.edgeAlias, "edge"),
1972
+ edgeCteContext
1973
+ );
1974
+ const baseWhereClauses = [
1975
+ sql`e.graph_id = ${graphId}`,
1976
+ nodeKindFilter,
1977
+ edgeTemporalFilter,
1978
+ nodeTemporalFilter,
1979
+ ...nodePredicateClauses,
1980
+ ...edgePredicateClauses
1981
+ ];
1982
+ const previousAlias = traversal.joinFromAlias;
1983
+ const edgeAlias = traversal.edgeAlias;
1984
+ const nodeAlias = traversal.nodeAlias;
1985
+ const requiredNodeColumns = requiredColumnsByAlias ? requiredColumnsByAlias.get(nodeAlias) ?? EMPTY_REQUIRED_COLUMNS : void 0;
1986
+ const requiredEdgeColumns = requiredColumnsByAlias ? requiredColumnsByAlias.get(edgeAlias) ?? EMPTY_REQUIRED_COLUMNS : void 0;
1987
+ const previousRowColumns = carryForwardPreviousColumns ? [sql`cte_${sql.raw(previousAlias)}.*`] : [
1988
+ sql`cte_${sql.raw(previousAlias)}.${sql.raw(previousAlias)}_id AS ${sql.raw(previousAlias)}_id`,
1989
+ sql`cte_${sql.raw(previousAlias)}.${sql.raw(previousAlias)}_kind AS ${sql.raw(previousAlias)}_kind`
1990
+ ];
1991
+ const selectColumns = [
1992
+ ...previousRowColumns,
1993
+ ...compileEdgeSelectColumns("e", edgeAlias, requiredEdgeColumns),
1994
+ ...compileNodeSelectColumns("n", nodeAlias, requiredNodeColumns)
1995
+ ];
1996
+ const cteMaterialization = materializeCte ? sql`MATERIALIZED ` : sql``;
1997
+ function compileTraversalBranch(branch) {
1998
+ const whereClauses = [
1999
+ ...baseWhereClauses,
2000
+ compileKindFilter(sql.raw("e.kind"), branch.edgeKinds),
2001
+ compileKindFilter(
2002
+ sql.raw(`e.${branch.joinKindField}`),
2003
+ previousNodeKinds
2004
+ ),
2005
+ compileKindFilter(sql.raw(`e.${branch.targetKindField}`), nodeKinds)
2006
+ ];
2007
+ if (branch.duplicateGuard !== void 0) {
2008
+ whereClauses.push(branch.duplicateGuard);
2009
+ }
2010
+ return sql`
2011
+ SELECT ${sql.join(selectColumns, sql`, `)}
2012
+ FROM cte_${sql.raw(previousAlias)}
2013
+ JOIN ${ctx.schema.edgesTable} e ON cte_${sql.raw(previousAlias)}.${sql.raw(previousAlias)}_id = e.${sql.raw(branch.joinField)}
2014
+ AND cte_${sql.raw(previousAlias)}.${sql.raw(previousAlias)}_kind = e.${sql.raw(branch.joinKindField)}
2015
+ JOIN ${ctx.schema.nodesTable} n ON n.graph_id = e.graph_id
2016
+ AND n.id = e.${sql.raw(branch.targetField)}
2017
+ AND n.kind = e.${sql.raw(branch.targetKindField)}
2018
+ WHERE ${sql.join(whereClauses, sql` AND `)}
2019
+ `;
2059
2020
  }
2060
- if (input.limitOffset !== void 0) {
2061
- parts.push(input.limitOffset);
2021
+ const directJoinField = traversal.direction === "out" ? "from_id" : "to_id";
2022
+ const directTargetField = traversal.direction === "out" ? "to_id" : "from_id";
2023
+ const directJoinKindField = traversal.direction === "out" ? "from_kind" : "to_kind";
2024
+ const directTargetKindField = traversal.direction === "out" ? "to_kind" : "from_kind";
2025
+ const directBranch = compileTraversalBranch({
2026
+ edgeKinds: directEdgeKinds,
2027
+ joinField: directJoinField,
2028
+ joinKindField: directJoinKindField,
2029
+ targetField: directTargetField,
2030
+ targetKindField: directTargetKindField
2031
+ });
2032
+ if (inverseEdgeKinds.length === 0) {
2033
+ if (traversalLimitValue !== void 0) {
2034
+ return sql`
2035
+ cte_${sql.raw(nodeAlias)} AS ${cteMaterialization}(
2036
+ SELECT * FROM (
2037
+ ${directBranch}
2038
+ ) AS traversal_rows
2039
+ LIMIT ${traversalLimitValue}
2040
+ )
2041
+ `;
2042
+ }
2043
+ return sql`
2044
+ cte_${sql.raw(nodeAlias)} AS ${cteMaterialization}(
2045
+ ${directBranch}
2046
+ )
2047
+ `;
2048
+ }
2049
+ const inverseJoinField = traversal.direction === "out" ? "to_id" : "from_id";
2050
+ const inverseTargetField = traversal.direction === "out" ? "from_id" : "to_id";
2051
+ const inverseJoinKindField = traversal.direction === "out" ? "to_kind" : "from_kind";
2052
+ const inverseTargetKindField = traversal.direction === "out" ? "from_kind" : "to_kind";
2053
+ const overlappingKinds = inverseEdgeKinds.filter(
2054
+ (kind) => directEdgeKinds.includes(kind)
2055
+ );
2056
+ const duplicateGuard = overlappingKinds.length > 0 ? sql`NOT (e.from_id = e.to_id AND ${compileKindFilter(
2057
+ sql.raw("e.kind"),
2058
+ overlappingKinds
2059
+ )})` : void 0;
2060
+ const inverseBranch = compileTraversalBranch({
2061
+ duplicateGuard,
2062
+ edgeKinds: inverseEdgeKinds,
2063
+ joinField: inverseJoinField,
2064
+ joinKindField: inverseJoinKindField,
2065
+ targetField: inverseTargetField,
2066
+ targetKindField: inverseTargetKindField
2067
+ });
2068
+ if (traversalLimitValue !== void 0) {
2069
+ return sql`
2070
+ cte_${sql.raw(nodeAlias)} AS ${cteMaterialization}(
2071
+ SELECT * FROM (
2072
+ ${directBranch}
2073
+ UNION ALL
2074
+ ${inverseBranch}
2075
+ ) AS traversal_rows
2076
+ LIMIT ${traversalLimitValue}
2077
+ )
2078
+ `;
2079
+ }
2080
+ return sql`
2081
+ cte_${sql.raw(nodeAlias)} AS ${cteMaterialization}(
2082
+ ${directBranch}
2083
+ UNION ALL
2084
+ ${inverseBranch}
2085
+ )
2086
+ `;
2087
+ }
2088
+ function compileAggregateExprFromSource(expr, dialect) {
2089
+ const { field: field2 } = expr;
2090
+ const fn = expr.function;
2091
+ switch (fn) {
2092
+ case "count":
2093
+ case "countDistinct":
2094
+ case "sum":
2095
+ case "avg":
2096
+ case "min":
2097
+ case "max": {
2098
+ const cteAlias = `cte_${field2.alias}`;
2099
+ const column = compileFieldValue(
2100
+ field2,
2101
+ dialect,
2102
+ field2.valueType,
2103
+ cteAlias
2104
+ );
2105
+ if (fn === "countDistinct") {
2106
+ return sql`COUNT(DISTINCT ${column})`;
2107
+ }
2108
+ return sql`${sql.raw(fn.toUpperCase())}(${column})`;
2109
+ }
2110
+ default: {
2111
+ throw new UnsupportedPredicateError(
2112
+ `Unknown aggregate function: ${String(fn)}`
2113
+ );
2114
+ }
2115
+ }
2116
+ }
2117
+ function compileProjectedSource(field2, dialect) {
2118
+ if (isAggregateExpr(field2.source)) {
2119
+ return compileAggregateExprFromSource(field2.source, dialect);
2120
+ }
2121
+ const cteAlias = field2.cteAlias ?? `cte_${field2.source.alias}`;
2122
+ return compileFieldValue(
2123
+ field2.source,
2124
+ dialect,
2125
+ field2.source.valueType,
2126
+ cteAlias
2127
+ );
2128
+ }
2129
+ function buildStandardProjection(input) {
2130
+ const { ast, collapsedTraversalCteAlias, dialect } = input;
2131
+ if (ast.selectiveFields && ast.selectiveFields.length > 0) {
2132
+ return compileSelectiveProjection(
2133
+ ast.selectiveFields,
2134
+ dialect,
2135
+ ast,
2136
+ collapsedTraversalCteAlias
2137
+ );
2138
+ }
2139
+ const fields = ast.projection.fields;
2140
+ if (fields.length === 0) {
2141
+ return sql.raw("*");
2142
+ }
2143
+ const projectedFields = fields.map((field2) => {
2144
+ const source = compileProjectedSource(field2, dialect);
2145
+ return sql`${source} AS ${quoteIdentifier(field2.outputName)}`;
2146
+ });
2147
+ return sql.join(projectedFields, sql`, `);
2148
+ }
2149
+ function compileSelectiveProjection(fields, dialect, ast, collapsedTraversalCteAlias) {
2150
+ const aliasToCte = /* @__PURE__ */ new Map([
2151
+ [ast.start.alias, `cte_${ast.start.alias}`]
2152
+ ]);
2153
+ for (const traversal of ast.traversals) {
2154
+ aliasToCte.set(traversal.nodeAlias, `cte_${traversal.nodeAlias}`);
2155
+ aliasToCte.set(traversal.edgeAlias, `cte_${traversal.nodeAlias}`);
2156
+ }
2157
+ const columns = fields.map((field2) => {
2158
+ const cteAlias = collapsedTraversalCteAlias ?? aliasToCte.get(field2.alias) ?? `cte_${field2.alias}`;
2159
+ if (field2.isSystemField) {
2160
+ const dbColumn = mapSelectiveSystemFieldToColumn(field2.field);
2161
+ return sql`${sql.raw(cteAlias)}.${sql.raw(`${field2.alias}_${dbColumn}`)} AS ${quoteIdentifier(field2.outputName)}`;
2162
+ }
2163
+ const propsColumn = `${field2.alias}_props`;
2164
+ const column = sql`${sql.raw(cteAlias)}.${sql.raw(propsColumn)}`;
2165
+ const pointer = jsonPointer([field2.field]);
2166
+ const extracted = compileTypedJsonExtract({
2167
+ column,
2168
+ dialect,
2169
+ pointer,
2170
+ valueType: field2.valueType
2171
+ });
2172
+ return sql`${extracted} AS ${quoteIdentifier(field2.outputName)}`;
2173
+ });
2174
+ return sql.join(columns, sql`, `);
2175
+ }
2176
+ function buildStandardFromClause(input) {
2177
+ const { ast, collapsedTraversalCteAlias, vectorPredicate } = input;
2178
+ if (collapsedTraversalCteAlias !== void 0) {
2179
+ return sql`FROM ${sql.raw(collapsedTraversalCteAlias)}`;
2180
+ }
2181
+ const startAlias = ast.start.alias;
2182
+ const fromClause = sql`FROM cte_${sql.raw(startAlias)}`;
2183
+ const joins = [];
2184
+ for (const traversal of ast.traversals) {
2185
+ const cteAlias = `cte_${traversal.nodeAlias}`;
2186
+ const previousAlias = traversal.joinFromAlias;
2187
+ const joinType = traversal.optional ? "LEFT JOIN" : "INNER JOIN";
2188
+ joins.push(
2189
+ sql`${sql.raw(joinType)} ${sql.raw(cteAlias)} ON ${sql.raw(cteAlias)}.${sql.raw(previousAlias)}_id = cte_${sql.raw(previousAlias)}.${sql.raw(previousAlias)}_id AND ${sql.raw(cteAlias)}.${sql.raw(previousAlias)}_kind = cte_${sql.raw(previousAlias)}.${sql.raw(previousAlias)}_kind`
2190
+ );
2191
+ }
2192
+ if (vectorPredicate) {
2193
+ const nodeAlias = vectorPredicate.field.alias;
2194
+ joins.push(
2195
+ sql`INNER JOIN cte_embeddings ON cte_embeddings.node_id = cte_${sql.raw(nodeAlias)}.${sql.raw(nodeAlias)}_id`
2196
+ );
2197
+ }
2198
+ return joins.length === 0 ? fromClause : sql`${fromClause} ${sql.join(joins, sql` `)}`;
2199
+ }
2200
+ function buildStandardOrderBy(input) {
2201
+ const { ast, collapsedTraversalCteAlias, dialect } = input;
2202
+ if (!ast.orderBy || ast.orderBy.length === 0) {
2203
+ return void 0;
2204
+ }
2205
+ const parts = [];
2206
+ for (const orderSpec of ast.orderBy) {
2207
+ const valueType = orderSpec.field.valueType;
2208
+ if (valueType === "array" || valueType === "object") {
2209
+ throw new UnsupportedPredicateError(
2210
+ "Ordering by JSON arrays or objects is not supported"
2211
+ );
2212
+ }
2213
+ const cteAlias = collapsedTraversalCteAlias ?? `cte_${orderSpec.field.alias}`;
2214
+ const field2 = compileFieldValue(
2215
+ orderSpec.field,
2216
+ dialect,
2217
+ valueType,
2218
+ cteAlias
2219
+ );
2220
+ const direction = sql.raw(orderSpec.direction.toUpperCase());
2221
+ const nulls = orderSpec.nulls ?? (orderSpec.direction === "asc" ? "last" : "first");
2222
+ const nullsDirection = sql.raw(nulls === "first" ? "DESC" : "ASC");
2223
+ parts.push(
2224
+ sql`(${field2} IS NULL) ${nullsDirection}`,
2225
+ sql`${field2} ${direction}`
2226
+ );
2227
+ }
2228
+ return sql`ORDER BY ${sql.join(parts, sql`, `)}`;
2229
+ }
2230
+ function fieldRefKey(field2) {
2231
+ const pointer = field2.jsonPointer ?? "";
2232
+ return `${field2.alias}:${field2.path.join(".")}:${pointer}`;
2233
+ }
2234
+ function buildStandardGroupBy(input) {
2235
+ const { ast, dialect } = input;
2236
+ if (!ast.groupBy || ast.groupBy.fields.length === 0) {
2237
+ return void 0;
2238
+ }
2239
+ const seenKeys = /* @__PURE__ */ new Set();
2240
+ const allFields = [];
2241
+ for (const projectedField of ast.projection.fields) {
2242
+ if (projectedField.source.__type === "field_ref") {
2243
+ const key = fieldRefKey(projectedField.source);
2244
+ if (!seenKeys.has(key)) {
2245
+ seenKeys.add(key);
2246
+ allFields.push(projectedField.source);
2247
+ }
2248
+ }
2249
+ }
2250
+ for (const field2 of ast.groupBy.fields) {
2251
+ const key = fieldRefKey(field2);
2252
+ if (!seenKeys.has(key)) {
2253
+ seenKeys.add(key);
2254
+ allFields.push(field2);
2255
+ }
2256
+ }
2257
+ if (allFields.length === 0) {
2258
+ return void 0;
2259
+ }
2260
+ const parts = allFields.map(
2261
+ (field2) => compileFieldValue(field2, dialect, field2.valueType, `cte_${field2.alias}`)
2262
+ );
2263
+ return sql`GROUP BY ${sql.join(parts, sql`, `)}`;
2264
+ }
2265
+ function buildStandardHaving(input) {
2266
+ const { ast, ctx } = input;
2267
+ if (!ast.having) {
2268
+ return void 0;
2269
+ }
2270
+ const condition = compilePredicateExpression(ast.having, ctx);
2271
+ return sql`HAVING ${condition}`;
2272
+ }
2273
+ function buildStandardEmbeddingsCte(input) {
2274
+ const { ctx, graphId, vectorPredicate } = input;
2275
+ const { dialect } = ctx;
2276
+ const { field: field2, metric, minScore, queryEmbedding } = vectorPredicate;
2277
+ const fieldPath = field2.jsonPointer ? field2.jsonPointer : field2.path.length > 1 && field2.path[0] === "props" ? `/${field2.path.slice(1).join("/")}` : `/${field2.path.join("/")}`;
2278
+ const distanceExpr = dialect.vectorDistance(
2279
+ sql.raw("embedding"),
2280
+ queryEmbedding,
2281
+ metric
2282
+ );
2283
+ const conditions = [
2284
+ sql`graph_id = ${graphId}`,
2285
+ sql`field_path = ${fieldPath}`
2286
+ ];
2287
+ if (minScore !== void 0) {
2288
+ conditions.push(
2289
+ compileVectorMinScoreCondition(distanceExpr, metric, minScore)
2290
+ );
2291
+ }
2292
+ const scoreExpr = compileVectorScoreExpression(distanceExpr, metric);
2293
+ return sql`
2294
+ cte_embeddings AS (
2295
+ SELECT
2296
+ node_id,
2297
+ ${distanceExpr} AS distance,
2298
+ ${scoreExpr} AS score
2299
+ FROM ${ctx.schema.embeddingsTable}
2300
+ WHERE ${sql.join(conditions, sql` AND `)}
2301
+ ORDER BY ${distanceExpr} ASC
2302
+ )
2303
+ `;
2304
+ }
2305
+ function compileVectorScoreExpression(distanceExpr, metric) {
2306
+ switch (metric) {
2307
+ case "cosine": {
2308
+ return sql`(1.0 - ${distanceExpr})`;
2309
+ }
2310
+ case "l2":
2311
+ case "inner_product": {
2312
+ return distanceExpr;
2313
+ }
2062
2314
  }
2063
- return sql.join(parts, sql` `);
2064
2315
  }
2065
- function assertSetOperationEmitterClauseAlignment(logicalPlan, suffixClauses) {
2066
- const shape = inspectSetOperationPlan(logicalPlan);
2067
- const hasSuffixClauses = suffixClauses !== void 0 && suffixClauses.length > 0;
2068
- if (!shape.hasSort && !shape.hasLimitOffset && hasSuffixClauses) {
2069
- throw new CompilerInvariantError(
2070
- "Set-operation SQL emitter received suffix clauses for a plan without top-level sort or limit_offset nodes",
2071
- { component: "set-operation-emitter" }
2072
- );
2316
+ function compileVectorMinScoreCondition(distanceExpr, metric, minScore) {
2317
+ switch (metric) {
2318
+ case "cosine": {
2319
+ const threshold = 1 - minScore;
2320
+ return sql`${distanceExpr} <= ${threshold}`;
2321
+ }
2322
+ case "l2": {
2323
+ return sql`${distanceExpr} <= ${minScore}`;
2324
+ }
2325
+ case "inner_product": {
2326
+ const negativeThreshold = -minScore;
2327
+ return sql`${distanceExpr} <= ${negativeThreshold}`;
2328
+ }
2073
2329
  }
2074
- if (!hasSuffixClauses) {
2075
- if (shape.hasSort || shape.hasLimitOffset) {
2076
- throw new CompilerInvariantError(
2077
- "Set-operation SQL emitter expected suffix clauses for plan containing top-level sort or limit_offset nodes",
2078
- { component: "set-operation-emitter" }
2330
+ }
2331
+ function buildStandardVectorOrderBy(input) {
2332
+ const { ast, dialect } = input;
2333
+ const distanceOrder = sql`cte_embeddings.distance ASC`;
2334
+ const additionalOrders = [];
2335
+ if (ast.orderBy && ast.orderBy.length > 0) {
2336
+ for (const orderSpec of ast.orderBy) {
2337
+ const valueType = orderSpec.field.valueType;
2338
+ if (valueType === "array" || valueType === "object") {
2339
+ throw new UnsupportedPredicateError(
2340
+ "Ordering by JSON arrays or objects is not supported"
2341
+ );
2342
+ }
2343
+ const cteAlias = `cte_${orderSpec.field.alias}`;
2344
+ const field2 = compileFieldValue(
2345
+ orderSpec.field,
2346
+ dialect,
2347
+ valueType,
2348
+ cteAlias
2349
+ );
2350
+ const direction = sql.raw(orderSpec.direction.toUpperCase());
2351
+ const nulls = orderSpec.nulls ?? (orderSpec.direction === "asc" ? "last" : "first");
2352
+ const nullsDirection = sql.raw(nulls === "first" ? "DESC" : "ASC");
2353
+ additionalOrders.push(
2354
+ sql`(${field2} IS NULL) ${nullsDirection}`,
2355
+ sql`${field2} ${direction}`
2079
2356
  );
2080
2357
  }
2081
- return;
2082
- }
2083
- const limitOffsetClauseCount = shape.limitOffsetNode === void 0 ? 0 : (shape.limitOffsetNode.limit === void 0 ? 0 : 1) + (shape.limitOffsetNode.offset === void 0 ? 0 : 1);
2084
- const expectedClauseCount = (shape.sortNode === void 0 ? 0 : 1) + limitOffsetClauseCount;
2085
- if (suffixClauses.length !== expectedClauseCount) {
2086
- throw new CompilerInvariantError(
2087
- `Set-operation SQL emitter expected ${String(expectedClauseCount)} top-level suffix clause(s) from logical plan, got ${String(suffixClauses.length)}`,
2088
- { component: "set-operation-emitter" }
2089
- );
2090
2358
  }
2359
+ const allOrders = [distanceOrder, ...additionalOrders];
2360
+ return sql`ORDER BY ${sql.join(allOrders, sql`, `)}`;
2091
2361
  }
2092
- function emitSetOperationQuerySql(input) {
2093
- assertSetOperationEmitterClauseAlignment(
2094
- input.logicalPlan,
2095
- input.suffixClauses
2096
- );
2362
+ function buildLimitOffsetClause(input) {
2363
+ const { limit, offset } = input;
2097
2364
  const parts = [];
2098
- if (input.ctes !== void 0 && input.ctes.length > 0) {
2099
- parts.push(sql`WITH ${sql.join([...input.ctes], sql`, `)}`);
2365
+ if (limit !== void 0) {
2366
+ parts.push(sql`LIMIT ${limit}`);
2100
2367
  }
2101
- parts.push(input.baseQuery);
2102
- if (input.suffixClauses !== void 0 && input.suffixClauses.length > 0) {
2103
- parts.push(...input.suffixClauses);
2368
+ if (offset !== void 0) {
2369
+ parts.push(sql`OFFSET ${offset}`);
2104
2370
  }
2105
- return sql.join(parts, sql` `);
2371
+ return parts.length > 0 ? sql.join(parts, sql` `) : void 0;
2106
2372
  }
2107
- function assertStandardEmitterClauseAlignment(logicalPlan, input) {
2108
- const planShape = inspectStandardProjectPlan(logicalPlan);
2109
- if (input.groupBy !== void 0 && !planShape.hasAggregate) {
2110
- throw new CompilerInvariantError(
2111
- "Standard SQL emitter received GROUP BY clause for a plan without aggregate nodes",
2112
- { component: "standard-emitter" }
2113
- );
2373
+
2374
+ // src/query/compiler/passes/recursive.ts
2375
+ function runRecursiveTraversalSelectionPass(ast) {
2376
+ const variableLengthTraversal = ast.traversals.find(
2377
+ (traversal) => traversal.variableLength !== void 0
2378
+ );
2379
+ if (variableLengthTraversal === void 0) {
2380
+ throw new CompilerInvariantError("No variable-length traversal found");
2114
2381
  }
2115
- if (input.having !== void 0 && !planShape.hasAggregate) {
2116
- throw new CompilerInvariantError(
2117
- "Standard SQL emitter received HAVING clause for a plan without aggregate nodes",
2118
- { component: "standard-emitter" }
2382
+ if (ast.traversals.length > 1) {
2383
+ throw new UnsupportedPredicateError(
2384
+ "Variable-length traversals with multiple traversals are not yet supported. Please use a single variable-length traversal."
2119
2385
  );
2120
2386
  }
2121
- const expectsOrderBy = planShape.hasSort || planShape.hasVectorKnn;
2122
- if (expectsOrderBy && input.orderBy === void 0) {
2123
- throw new CompilerInvariantError(
2124
- "Standard SQL emitter expected ORDER BY clause for plan containing a sort or vector_knn node",
2125
- { component: "standard-emitter" }
2126
- );
2387
+ return variableLengthTraversal;
2388
+ }
2389
+
2390
+ // src/query/compiler/passes/runner.ts
2391
+ function runCompilerPass(state, pass) {
2392
+ const output = pass.execute(state);
2393
+ return {
2394
+ state: pass.update(state, output)
2395
+ };
2396
+ }
2397
+ function compileTemporalFilter(options) {
2398
+ const { mode, asOf, tableAlias, currentTimestamp } = options;
2399
+ const prefix = tableAlias ? sql.raw(`${tableAlias}.`) : sql.raw("");
2400
+ const deletedAt = sql`${prefix}deleted_at`;
2401
+ const validFrom = sql`${prefix}valid_from`;
2402
+ const validTo = sql`${prefix}valid_to`;
2403
+ switch (mode) {
2404
+ case "current": {
2405
+ const now = currentTimestamp ?? sql`CURRENT_TIMESTAMP`;
2406
+ return sql`${deletedAt} IS NULL AND (${validFrom} IS NULL OR ${validFrom} <= ${now}) AND (${validTo} IS NULL OR ${validTo} > ${now})`;
2407
+ }
2408
+ case "asOf": {
2409
+ const timestamp = asOf;
2410
+ return sql`${deletedAt} IS NULL AND (${validFrom} IS NULL OR ${validFrom} <= ${timestamp}) AND (${validTo} IS NULL OR ${validTo} > ${timestamp})`;
2411
+ }
2412
+ case "includeEnded": {
2413
+ return sql`${deletedAt} IS NULL`;
2414
+ }
2415
+ case "includeTombstones": {
2416
+ return sql.raw("1=1");
2417
+ }
2127
2418
  }
2128
- if (!expectsOrderBy && input.orderBy !== void 0) {
2129
- throw new CompilerInvariantError(
2130
- "Standard SQL emitter received ORDER BY clause for a plan without sort or vector_knn nodes",
2131
- { component: "standard-emitter" }
2419
+ }
2420
+ function extractTemporalOptions(ast, tableAlias) {
2421
+ return {
2422
+ mode: ast.temporalMode.mode,
2423
+ asOf: ast.temporalMode.asOf,
2424
+ tableAlias
2425
+ };
2426
+ }
2427
+
2428
+ // src/query/compiler/passes/temporal.ts
2429
+ function createTemporalFilterPass(ast, currentTimestamp) {
2430
+ return {
2431
+ forAlias(tableAlias) {
2432
+ return compileTemporalFilter({
2433
+ ...extractTemporalOptions(ast, tableAlias),
2434
+ currentTimestamp
2435
+ });
2436
+ }
2437
+ };
2438
+ }
2439
+
2440
+ // src/query/compiler/passes/vector.ts
2441
+ function runVectorPredicatePass(ast, dialect) {
2442
+ const vectorPredicates = extractVectorSimilarityPredicates(ast.predicates);
2443
+ if (vectorPredicates.length > 1) {
2444
+ throw new UnsupportedPredicateError(
2445
+ "Multiple vector similarity predicates in a single query are not supported"
2132
2446
  );
2133
2447
  }
2134
- if (planShape.hasLimitOffset && input.limitOffset === void 0) {
2135
- throw new CompilerInvariantError(
2136
- "Standard SQL emitter expected LIMIT/OFFSET clause for plan containing a limit_offset node",
2137
- { component: "standard-emitter" }
2138
- );
2448
+ const vectorPredicate = vectorPredicates[0];
2449
+ if (vectorPredicate === void 0) {
2450
+ return { vectorPredicate: void 0 };
2139
2451
  }
2140
- if (!planShape.hasLimitOffset && input.limitOffset !== void 0) {
2141
- throw new CompilerInvariantError(
2142
- "Standard SQL emitter received LIMIT/OFFSET clause for a plan without limit_offset nodes",
2143
- { component: "standard-emitter" }
2452
+ const vectorStrategy = dialect.capabilities.vectorPredicateStrategy;
2453
+ if (vectorStrategy === "unsupported" || !dialect.supportsVectors) {
2454
+ throw new UnsupportedPredicateError(
2455
+ `Vector similarity predicates are not supported for dialect "${dialect.name}"`
2144
2456
  );
2145
2457
  }
2146
- }
2147
- function emitStandardQuerySql(input) {
2148
- assertStandardEmitterClauseAlignment(input.logicalPlan, input);
2149
- const parts = [];
2150
- if (input.ctes.length > 0) {
2151
- parts.push(sql`WITH ${sql.join([...input.ctes], sql`, `)}`);
2458
+ if (!dialect.capabilities.vectorMetrics.includes(vectorPredicate.metric)) {
2459
+ throw new UnsupportedPredicateError(
2460
+ `Vector metric "${vectorPredicate.metric}" is not supported for dialect "${dialect.name}"`
2461
+ );
2152
2462
  }
2153
- parts.push(sql`SELECT ${input.projection}`, input.fromClause);
2154
- if (input.groupBy !== void 0) {
2155
- parts.push(input.groupBy);
2463
+ if (!Number.isFinite(vectorPredicate.limit) || vectorPredicate.limit <= 0) {
2464
+ throw new UnsupportedPredicateError(
2465
+ `Vector predicate limit must be a positive finite number, got ${String(vectorPredicate.limit)}`
2466
+ );
2156
2467
  }
2157
- if (input.having !== void 0) {
2158
- parts.push(input.having);
2468
+ const { minScore } = vectorPredicate;
2469
+ if (minScore !== void 0) {
2470
+ if (!Number.isFinite(minScore)) {
2471
+ throw new UnsupportedPredicateError(
2472
+ `Vector minScore must be a finite number, got ${String(minScore)}`
2473
+ );
2474
+ }
2475
+ if (vectorPredicate.metric === "cosine" && (minScore < -1 || minScore > 1)) {
2476
+ throw new UnsupportedPredicateError(
2477
+ `Cosine minScore must be between -1 and 1, got ${String(minScore)}`
2478
+ );
2479
+ }
2159
2480
  }
2160
- if (input.orderBy !== void 0) {
2161
- parts.push(input.orderBy);
2481
+ return { vectorPredicate };
2482
+ }
2483
+ function resolveVectorAwareLimit(astLimit, vectorPredicate) {
2484
+ if (vectorPredicate === void 0) {
2485
+ return astLimit;
2162
2486
  }
2163
- if (input.limitOffset !== void 0) {
2164
- parts.push(input.limitOffset);
2487
+ if (astLimit === void 0) {
2488
+ return vectorPredicate.limit;
2165
2489
  }
2166
- return sql.join(parts, sql` `);
2167
- }
2168
- var EMPTY_PREDICATES = [];
2169
- function buildPredicateIndexKey(alias, targetType) {
2170
- return `${alias}\0${targetType}`;
2171
- }
2172
- function resolvePredicateTargetType(predicate2) {
2173
- return predicate2.targetType === "edge" ? "edge" : "node";
2490
+ return Math.min(astLimit, vectorPredicate.limit);
2174
2491
  }
2175
- function buildPredicateIndex(ast) {
2176
- const byAliasAndType = /* @__PURE__ */ new Map();
2177
- for (const predicate2 of ast.predicates) {
2178
- const key = buildPredicateIndexKey(
2179
- predicate2.targetAlias,
2180
- resolvePredicateTargetType(predicate2)
2181
- );
2182
- const existing = byAliasAndType.get(key);
2183
- if (existing === void 0) {
2184
- byAliasAndType.set(key, [predicate2]);
2185
- } else {
2186
- existing.push(predicate2);
2492
+
2493
+ // src/query/compiler/plan/lowering.ts
2494
+ function createPlanNodeIdFactory() {
2495
+ let current = 0;
2496
+ return function nextPlanNodeId() {
2497
+ current += 1;
2498
+ return `plan_${current.toString(36)}`;
2499
+ };
2500
+ }
2501
+ function extractAggregateExpressions(ast) {
2502
+ const aggregates = [];
2503
+ for (const field2 of ast.projection.fields) {
2504
+ if ("__type" in field2.source && field2.source.__type === "aggregate") {
2505
+ aggregates.push(field2.source);
2187
2506
  }
2188
2507
  }
2189
- return { byAliasAndType };
2190
- }
2191
- function getPredicatesForAlias(predicateIndex, alias, targetType) {
2192
- return predicateIndex.byAliasAndType.get(
2193
- buildPredicateIndexKey(alias, targetType)
2194
- ) ?? EMPTY_PREDICATES;
2508
+ return aggregates;
2195
2509
  }
2196
- function compilePredicateClauses(predicates, predicateContext) {
2197
- return predicates.map(
2198
- (predicate2) => compilePredicateExpression(predicate2.expression, predicateContext)
2199
- );
2510
+ function getAliasPredicates(ast, alias, predicateTargetType) {
2511
+ return ast.predicates.filter((predicate2) => {
2512
+ const targetType = predicate2.targetType ?? "node";
2513
+ return predicate2.targetAlias === alias && targetType === predicateTargetType;
2514
+ });
2200
2515
  }
2201
- function compileKindFilter(column, kinds) {
2202
- if (kinds.length === 0) {
2203
- return sql`1 = 0`;
2204
- }
2205
- if (kinds.length === 1) {
2206
- return sql`${column} = ${kinds[0]}`;
2516
+ function wrapWithAliasFilterNode(currentNode, ast, alias, predicateTargetType, nextPlanNodeId) {
2517
+ const aliasPredicates = getAliasPredicates(ast, alias, predicateTargetType);
2518
+ if (aliasPredicates.length === 0) {
2519
+ return currentNode;
2207
2520
  }
2208
- return sql`${column} IN (${sql.join(
2209
- kinds.map((kind) => sql`${kind}`),
2210
- sql`, `
2211
- )})`;
2521
+ return {
2522
+ alias,
2523
+ id: nextPlanNodeId(),
2524
+ input: currentNode,
2525
+ op: "filter",
2526
+ predicateTargetType,
2527
+ predicates: aliasPredicates.map((predicate2) => predicate2.expression)
2528
+ };
2212
2529
  }
2213
- function getNodeKindsForAlias(ast, alias) {
2214
- if (alias === ast.start.alias) {
2215
- return ast.start.kinds;
2530
+ function appendAggregateSortLimitAndProjectNodes(currentNode, ast, nextPlanNodeId, limit, collapsedTraversalCteAlias) {
2531
+ let node = currentNode;
2532
+ const aggregateExpressions = extractAggregateExpressions(ast);
2533
+ if (aggregateExpressions.length > 0 || ast.groupBy !== void 0 || ast.having !== void 0) {
2534
+ const aggregateNode = {
2535
+ aggregates: aggregateExpressions,
2536
+ groupBy: ast.groupBy?.fields ?? [],
2537
+ id: nextPlanNodeId(),
2538
+ input: node,
2539
+ op: "aggregate"
2540
+ };
2541
+ node = ast.having === void 0 ? aggregateNode : { ...aggregateNode, having: ast.having };
2216
2542
  }
2217
- for (const traversal of ast.traversals) {
2218
- if (traversal.nodeAlias === alias) {
2219
- return traversal.nodeKinds;
2220
- }
2543
+ if (ast.orderBy !== void 0 && ast.orderBy.length > 0) {
2544
+ node = {
2545
+ id: nextPlanNodeId(),
2546
+ input: node,
2547
+ op: "sort",
2548
+ orderBy: ast.orderBy
2549
+ };
2221
2550
  }
2222
- throw new CompilerInvariantError(`Unknown traversal source alias: ${alias}`);
2223
- }
2224
-
2225
- // src/query/compiler/typed-json-extract.ts
2226
- function compileTypedJsonExtract(input) {
2227
- const { column, dialect, pointer, valueType } = input;
2228
- const fallback = input.fallback ?? "json";
2229
- switch (valueType) {
2230
- case "string": {
2231
- return dialect.jsonExtractText(column, pointer);
2232
- }
2233
- case "number": {
2234
- return dialect.jsonExtractNumber(column, pointer);
2235
- }
2236
- case "boolean": {
2237
- return dialect.jsonExtractBoolean(column, pointer);
2238
- }
2239
- case "date": {
2240
- return dialect.jsonExtractDate(column, pointer);
2241
- }
2242
- case "array":
2243
- case "object":
2244
- case "embedding":
2245
- case "unknown":
2246
- case void 0: {
2247
- return fallback === "text" ? dialect.jsonExtractText(column, pointer) : dialect.jsonExtract(column, pointer);
2248
- }
2249
- default: {
2250
- return fallback === "text" ? dialect.jsonExtractText(column, pointer) : dialect.jsonExtract(column, pointer);
2551
+ if (limit !== void 0 || ast.offset !== void 0) {
2552
+ const limitOffsetNodeBase = {
2553
+ id: nextPlanNodeId(),
2554
+ input: node,
2555
+ op: "limit_offset"
2556
+ };
2557
+ const hasLimit = limit !== void 0;
2558
+ const hasOffset = ast.offset !== void 0;
2559
+ if (hasLimit && hasOffset) {
2560
+ node = {
2561
+ ...limitOffsetNodeBase,
2562
+ limit,
2563
+ offset: ast.offset
2564
+ };
2565
+ } else if (hasLimit) {
2566
+ node = { ...limitOffsetNodeBase, limit };
2567
+ } else if (hasOffset) {
2568
+ node = { ...limitOffsetNodeBase, offset: ast.offset };
2569
+ } else {
2570
+ throw new CompilerInvariantError(
2571
+ "limit_offset node requires limit or offset to be present"
2572
+ );
2251
2573
  }
2252
2574
  }
2575
+ const projectNodeBase = {
2576
+ fields: ast.projection.fields,
2577
+ id: nextPlanNodeId(),
2578
+ input: node,
2579
+ op: "project"
2580
+ };
2581
+ return collapsedTraversalCteAlias === void 0 ? projectNodeBase : {
2582
+ ...projectNodeBase,
2583
+ collapsedTraversalAlias: collapsedTraversalCteAlias
2584
+ };
2253
2585
  }
2254
- var NODE_COLUMNS = [
2255
- "id",
2256
- "kind",
2257
- "props",
2258
- "version",
2259
- "valid_from",
2260
- "valid_to",
2261
- "created_at",
2262
- "updated_at",
2263
- "deleted_at"
2264
- ];
2265
- var EDGE_COLUMNS = [
2266
- "id",
2267
- "kind",
2268
- "from_id",
2269
- "to_id",
2270
- "props",
2271
- "valid_from",
2272
- "valid_to",
2273
- "created_at",
2274
- "updated_at",
2275
- "deleted_at"
2276
- ];
2277
- var EMPTY_REQUIRED_COLUMNS = /* @__PURE__ */ new Set();
2278
- function quoteIdentifier(identifier) {
2279
- return sql.raw(`"${identifier.replaceAll('"', '""')}"`);
2586
+ function lowerStandardQueryToLogicalPlanNode(input) {
2587
+ const { ast, nextPlanNodeId } = input;
2588
+ let currentNode = {
2589
+ alias: ast.start.alias,
2590
+ graphId: input.graphId,
2591
+ id: nextPlanNodeId(),
2592
+ kinds: ast.start.kinds,
2593
+ op: "scan",
2594
+ source: "nodes"
2595
+ };
2596
+ currentNode = wrapWithAliasFilterNode(
2597
+ currentNode,
2598
+ ast,
2599
+ ast.start.alias,
2600
+ "node",
2601
+ nextPlanNodeId
2602
+ );
2603
+ for (const traversal of ast.traversals) {
2604
+ currentNode = {
2605
+ direction: traversal.direction,
2606
+ edgeAlias: traversal.edgeAlias,
2607
+ edgeKinds: traversal.edgeKinds,
2608
+ id: nextPlanNodeId(),
2609
+ input: currentNode,
2610
+ inverseEdgeKinds: traversal.inverseEdgeKinds ?? [],
2611
+ joinFromAlias: traversal.joinFromAlias,
2612
+ joinType: traversal.optional ? "left" : "inner",
2613
+ nodeAlias: traversal.nodeAlias,
2614
+ nodeKinds: traversal.nodeKinds,
2615
+ op: "join"
2616
+ };
2617
+ currentNode = wrapWithAliasFilterNode(
2618
+ currentNode,
2619
+ ast,
2620
+ traversal.edgeAlias,
2621
+ "edge",
2622
+ nextPlanNodeId
2623
+ );
2624
+ currentNode = wrapWithAliasFilterNode(
2625
+ currentNode,
2626
+ ast,
2627
+ traversal.nodeAlias,
2628
+ "node",
2629
+ nextPlanNodeId
2630
+ );
2631
+ }
2632
+ if (input.vectorPredicate !== void 0) {
2633
+ currentNode = {
2634
+ id: nextPlanNodeId(),
2635
+ input: currentNode,
2636
+ op: "vector_knn",
2637
+ predicate: input.vectorPredicate
2638
+ };
2639
+ }
2640
+ return appendAggregateSortLimitAndProjectNodes(
2641
+ currentNode,
2642
+ ast,
2643
+ nextPlanNodeId,
2644
+ input.effectiveLimit,
2645
+ input.collapsedTraversalCteAlias
2646
+ );
2280
2647
  }
2281
- function shouldProjectColumn(requiredColumns, column, alwaysRequiredColumns) {
2282
- if (alwaysRequiredColumns?.has(column)) return true;
2283
- if (requiredColumns === void 0) return true;
2284
- return requiredColumns.has(column);
2648
+ function lowerRecursiveQueryToLogicalPlanNode(input) {
2649
+ const { ast, nextPlanNodeId } = input;
2650
+ const traversal = input.traversal ?? runRecursiveTraversalSelectionPass(input.ast);
2651
+ let currentNode = {
2652
+ alias: ast.start.alias,
2653
+ graphId: input.graphId,
2654
+ id: nextPlanNodeId(),
2655
+ kinds: ast.start.kinds,
2656
+ op: "scan",
2657
+ source: "nodes"
2658
+ };
2659
+ currentNode = wrapWithAliasFilterNode(
2660
+ currentNode,
2661
+ ast,
2662
+ ast.start.alias,
2663
+ "node",
2664
+ nextPlanNodeId
2665
+ );
2666
+ currentNode = {
2667
+ edgeAlias: traversal.edgeAlias,
2668
+ edgeKinds: traversal.edgeKinds,
2669
+ id: nextPlanNodeId(),
2670
+ input: currentNode,
2671
+ inverseEdgeKinds: traversal.inverseEdgeKinds ?? [],
2672
+ nodeAlias: traversal.nodeAlias,
2673
+ nodeKinds: traversal.nodeKinds,
2674
+ op: "recursive_expand",
2675
+ traversal: traversal.variableLength
2676
+ };
2677
+ currentNode = wrapWithAliasFilterNode(
2678
+ currentNode,
2679
+ ast,
2680
+ traversal.edgeAlias,
2681
+ "edge",
2682
+ nextPlanNodeId
2683
+ );
2684
+ currentNode = wrapWithAliasFilterNode(
2685
+ currentNode,
2686
+ ast,
2687
+ traversal.nodeAlias,
2688
+ "node",
2689
+ nextPlanNodeId
2690
+ );
2691
+ return appendAggregateSortLimitAndProjectNodes(
2692
+ currentNode,
2693
+ ast,
2694
+ nextPlanNodeId,
2695
+ ast.limit
2696
+ );
2285
2697
  }
2286
- function addRequiredColumn(requiredColumnsByAlias, alias, column) {
2287
- const existing = requiredColumnsByAlias.get(alias);
2288
- if (existing) {
2289
- existing.add(column);
2290
- return;
2698
+ function lowerComposableQueryToLogicalPlanNode(query, dialect, graphId, nextPlanNodeId) {
2699
+ if ("__type" in query) {
2700
+ return lowerSetOperationToLogicalPlanNode(
2701
+ query,
2702
+ graphId,
2703
+ dialect,
2704
+ nextPlanNodeId
2705
+ );
2291
2706
  }
2292
- requiredColumnsByAlias.set(alias, /* @__PURE__ */ new Set([column]));
2293
- }
2294
- function markFieldRefAsRequired(requiredColumnsByAlias, field2) {
2295
- const column = field2.path[0];
2296
- if (column === void 0) return;
2297
- addRequiredColumn(requiredColumnsByAlias, field2.alias, column);
2298
- }
2299
- function mapSelectiveSystemFieldToColumn(field2) {
2300
- if (field2 === "fromId") return "from_id";
2301
- if (field2 === "toId") return "to_id";
2302
- if (field2.startsWith("meta.")) {
2303
- return field2.slice(5).replaceAll(/([A-Z])/g, "_$1").toLowerCase();
2707
+ const hasVariableLengthTraversal2 = query.traversals.some(
2708
+ (traversal) => traversal.variableLength !== void 0
2709
+ );
2710
+ if (hasVariableLengthTraversal2) {
2711
+ return lowerRecursiveQueryToLogicalPlanNode({
2712
+ ast: query,
2713
+ graphId,
2714
+ nextPlanNodeId
2715
+ });
2304
2716
  }
2305
- return field2;
2717
+ const vectorPredicate = runVectorPredicatePass(
2718
+ query,
2719
+ getDialect(dialect)
2720
+ ).vectorPredicate;
2721
+ const effectiveLimit = resolveVectorAwareLimit(query.limit, vectorPredicate);
2722
+ const loweringInput = {
2723
+ ast: query,
2724
+ graphId,
2725
+ nextPlanNodeId,
2726
+ ...effectiveLimit === void 0 ? {} : { effectiveLimit },
2727
+ ...vectorPredicate === void 0 ? {} : { vectorPredicate }
2728
+ };
2729
+ return lowerStandardQueryToLogicalPlanNode(loweringInput);
2306
2730
  }
2307
- function markSelectiveFieldAsRequired(requiredColumnsByAlias, field2) {
2308
- if (field2.isSystemField) {
2309
- addRequiredColumn(
2310
- requiredColumnsByAlias,
2311
- field2.alias,
2312
- mapSelectiveSystemFieldToColumn(field2.field)
2313
- );
2314
- return;
2731
+ function lowerSetOperationToLogicalPlanNode(op, graphId, dialect, nextPlanNodeId) {
2732
+ let currentNode = {
2733
+ id: nextPlanNodeId(),
2734
+ left: lowerComposableQueryToLogicalPlanNode(
2735
+ op.left,
2736
+ dialect,
2737
+ graphId,
2738
+ nextPlanNodeId
2739
+ ),
2740
+ op: "set_op",
2741
+ operator: op.operator,
2742
+ right: lowerComposableQueryToLogicalPlanNode(
2743
+ op.right,
2744
+ dialect,
2745
+ graphId,
2746
+ nextPlanNodeId
2747
+ )
2748
+ };
2749
+ if (op.orderBy !== void 0 && op.orderBy.length > 0) {
2750
+ currentNode = {
2751
+ id: nextPlanNodeId(),
2752
+ input: currentNode,
2753
+ op: "sort",
2754
+ orderBy: op.orderBy
2755
+ };
2315
2756
  }
2316
- addRequiredColumn(requiredColumnsByAlias, field2.alias, "props");
2317
- }
2318
- function isIdFieldRef(field2) {
2319
- return field2.path.length === 1 && field2.path[0] === "id" && field2.jsonPointer === void 0;
2757
+ if (op.limit !== void 0 || op.offset !== void 0) {
2758
+ const limitOffsetBase = {
2759
+ id: nextPlanNodeId(),
2760
+ input: currentNode,
2761
+ op: "limit_offset"
2762
+ };
2763
+ if (op.limit !== void 0 && op.offset !== void 0) {
2764
+ currentNode = { ...limitOffsetBase, limit: op.limit, offset: op.offset };
2765
+ } else if (op.limit === void 0) {
2766
+ currentNode = { ...limitOffsetBase, offset: op.offset };
2767
+ } else {
2768
+ currentNode = { ...limitOffsetBase, limit: op.limit };
2769
+ }
2770
+ }
2771
+ return currentNode;
2320
2772
  }
2321
- function isAggregateExpr(source) {
2322
- return "__type" in source && source.__type === "aggregate";
2773
+ function lowerStandardQueryToLogicalPlan(input) {
2774
+ const nextPlanNodeId = createPlanNodeIdFactory();
2775
+ return {
2776
+ metadata: {
2777
+ dialect: input.dialect,
2778
+ graphId: input.graphId
2779
+ },
2780
+ root: lowerStandardQueryToLogicalPlanNode({
2781
+ ...input,
2782
+ nextPlanNodeId
2783
+ })
2784
+ };
2323
2785
  }
2324
-
2325
- // src/query/compiler/emitter/standard-builders.ts
2326
- function compileColumnReference(tableAlias, column) {
2327
- if (tableAlias === void 0) {
2328
- return sql.raw(column);
2329
- }
2330
- return sql`${sql.raw(tableAlias)}.${sql.raw(column)}`;
2786
+ function lowerRecursiveQueryToLogicalPlan(input) {
2787
+ const nextPlanNodeId = createPlanNodeIdFactory();
2788
+ return {
2789
+ metadata: {
2790
+ dialect: input.dialect,
2791
+ graphId: input.graphId
2792
+ },
2793
+ root: lowerRecursiveQueryToLogicalPlanNode({
2794
+ ...input,
2795
+ nextPlanNodeId
2796
+ })
2797
+ };
2331
2798
  }
2332
- function compileNodeSelectColumns(tableAlias, alias, requiredColumns) {
2333
- return NODE_COLUMNS.filter(
2334
- (column) => column === "id" || column === "kind" || shouldProjectColumn(requiredColumns, column)
2335
- ).map(
2336
- (column) => sql`${compileColumnReference(tableAlias, column)} AS ${sql.raw(`${alias}_${column}`)}`
2337
- );
2799
+ function lowerSetOperationToLogicalPlan(input) {
2800
+ const nextPlanNodeId = createPlanNodeIdFactory();
2801
+ return {
2802
+ metadata: {
2803
+ dialect: input.dialect,
2804
+ graphId: input.graphId
2805
+ },
2806
+ root: lowerSetOperationToLogicalPlanNode(
2807
+ input.op,
2808
+ input.graphId,
2809
+ input.dialect,
2810
+ nextPlanNodeId
2811
+ )
2812
+ };
2338
2813
  }
2339
- function compileEdgeSelectColumns(tableAlias, alias, requiredColumns) {
2340
- return EDGE_COLUMNS.filter(
2341
- (column) => shouldProjectColumn(requiredColumns, column)
2342
- ).map(
2343
- (column) => sql`${compileColumnReference(tableAlias, column)} AS ${sql.raw(`${alias}_${column}`)}`
2344
- );
2814
+
2815
+ // src/query/compiler/recursive.ts
2816
+ var MAX_RECURSIVE_DEPTH = 100;
2817
+ var MAX_EXPLICIT_RECURSIVE_DEPTH = 1e3;
2818
+ var NO_ALWAYS_REQUIRED_COLUMNS = /* @__PURE__ */ new Set();
2819
+ function runRecursiveQueryPassPipeline(ast, graphId, ctx) {
2820
+ let state = {
2821
+ ast,
2822
+ ctx,
2823
+ graphId,
2824
+ logicalPlan: void 0,
2825
+ requiredColumnsByAlias: void 0,
2826
+ temporalFilterPass: void 0,
2827
+ traversal: void 0
2828
+ };
2829
+ const recursiveTraversalPass = runCompilerPass(state, {
2830
+ name: "recursive_traversal",
2831
+ execute(currentState) {
2832
+ return runRecursiveTraversalSelectionPass(currentState.ast);
2833
+ },
2834
+ update(currentState, traversal) {
2835
+ return {
2836
+ ...currentState,
2837
+ traversal
2838
+ };
2839
+ }
2840
+ });
2841
+ state = recursiveTraversalPass.state;
2842
+ const temporalPass = runCompilerPass(state, {
2843
+ name: "temporal_filters",
2844
+ execute(currentState) {
2845
+ return createTemporalFilterPass(
2846
+ currentState.ast,
2847
+ currentState.ctx.dialect.currentTimestamp()
2848
+ );
2849
+ },
2850
+ update(currentState, temporalFilterPass) {
2851
+ return {
2852
+ ...currentState,
2853
+ temporalFilterPass
2854
+ };
2855
+ }
2856
+ });
2857
+ state = temporalPass.state;
2858
+ const columnPruningPass = runCompilerPass(state, {
2859
+ name: "column_pruning",
2860
+ execute(currentState) {
2861
+ const traversal = currentState.traversal;
2862
+ if (traversal === void 0) {
2863
+ throw new CompilerInvariantError(
2864
+ "Recursive traversal pass did not select traversal"
2865
+ );
2866
+ }
2867
+ return collectRequiredColumnsByAlias(currentState.ast, traversal);
2868
+ },
2869
+ update(currentState, requiredColumnsByAlias) {
2870
+ return {
2871
+ ...currentState,
2872
+ requiredColumnsByAlias
2873
+ };
2874
+ }
2875
+ });
2876
+ state = columnPruningPass.state;
2877
+ const logicalPlanPass = runCompilerPass(state, {
2878
+ name: "logical_plan",
2879
+ execute(currentState) {
2880
+ const loweringInput = {
2881
+ ast: currentState.ast,
2882
+ dialect: currentState.ctx.dialect.name,
2883
+ graphId: currentState.graphId,
2884
+ ...currentState.traversal === void 0 ? {} : { traversal: currentState.traversal }
2885
+ };
2886
+ return lowerRecursiveQueryToLogicalPlan(loweringInput);
2887
+ },
2888
+ update(currentState, logicalPlan) {
2889
+ return {
2890
+ ...currentState,
2891
+ logicalPlan
2892
+ };
2893
+ }
2894
+ });
2895
+ state = logicalPlanPass.state;
2896
+ return state;
2345
2897
  }
2346
- function buildStandardStartCte(input) {
2347
- const { ast, ctx, graphId, predicateIndex, requiredColumnsByAlias } = input;
2348
- const alias = ast.start.alias;
2349
- const kinds = ast.start.kinds;
2350
- const kindFilter = compileKindFilter(sql.raw("kind"), kinds);
2351
- const temporalFilter = input.temporalFilterPass.forAlias();
2352
- const cteContext = { ...ctx, cteColumnPrefix: "" };
2353
- const predicateClauses = compilePredicateClauses(
2354
- getPredicatesForAlias(predicateIndex, alias, "node"),
2355
- cteContext
2356
- );
2357
- const whereClauses = [
2358
- sql`graph_id = ${graphId}`,
2359
- kindFilter,
2360
- temporalFilter,
2361
- ...predicateClauses
2362
- ];
2363
- const effectiveRequiredColumns = requiredColumnsByAlias ? requiredColumnsByAlias.get(alias) ?? EMPTY_REQUIRED_COLUMNS : void 0;
2364
- return sql`
2365
- cte_${sql.raw(alias)} AS (
2366
- SELECT ${sql.join(
2367
- compileNodeSelectColumns(void 0, alias, effectiveRequiredColumns),
2368
- sql`, `
2369
- )}
2370
- FROM ${ctx.schema.nodesTable}
2371
- WHERE ${sql.join(whereClauses, sql` AND `)}
2372
- )
2373
- `;
2898
+ function compileVariableLengthQuery(ast, graphId, ctx) {
2899
+ const strategy = ctx.dialect.capabilities.recursiveQueryStrategy;
2900
+ const handler = RECURSIVE_QUERY_STRATEGY_HANDLERS[strategy];
2901
+ return handler(ast, graphId, ctx);
2374
2902
  }
2375
- function buildStandardTraversalCte(input) {
2903
+ var RECURSIVE_QUERY_STRATEGY_HANDLERS = {
2904
+ recursive_cte: compileVariableLengthQueryWithRecursiveCteStrategy
2905
+ };
2906
+ function compileVariableLengthQueryWithRecursiveCteStrategy(ast, graphId, ctx) {
2907
+ const passState = runRecursiveQueryPassPipeline(ast, graphId, ctx);
2908
+ const { dialect } = ctx;
2376
2909
  const {
2910
+ logicalPlan,
2911
+ requiredColumnsByAlias,
2912
+ temporalFilterPass,
2913
+ traversal: vlTraversal
2914
+ } = passState;
2915
+ if (temporalFilterPass === void 0) {
2916
+ throw new CompilerInvariantError(
2917
+ "Temporal filter pass did not initialize temporal state"
2918
+ );
2919
+ }
2920
+ if (logicalPlan === void 0) {
2921
+ throw new CompilerInvariantError(
2922
+ "Logical plan pass did not initialize plan state"
2923
+ );
2924
+ }
2925
+ if (vlTraversal === void 0) {
2926
+ throw new CompilerInvariantError(
2927
+ "Recursive traversal pass did not select traversal"
2928
+ );
2929
+ }
2930
+ const recursiveCte = compileRecursiveCte(
2377
2931
  ast,
2378
- carryForwardPreviousColumns,
2379
- ctx,
2932
+ vlTraversal,
2380
2933
  graphId,
2381
- materializeCte,
2382
- predicateIndex,
2934
+ ctx,
2383
2935
  requiredColumnsByAlias,
2384
- temporalFilterPass,
2385
- traversalIndex,
2386
- traversalLimit
2387
- } = input;
2388
- const traversal = ast.traversals[traversalIndex];
2389
- const traversalLimitValue = traversalIndex === ast.traversals.length - 1 ? traversalLimit : void 0;
2390
- const previousNodeKinds = getNodeKindsForAlias(ast, traversal.joinFromAlias);
2936
+ temporalFilterPass
2937
+ );
2938
+ const projection = compileRecursiveProjection(ast, vlTraversal, dialect);
2939
+ const minDepth = vlTraversal.variableLength.minDepth;
2940
+ const depthFilter = minDepth > 0 ? sql`WHERE depth >= ${minDepth}` : sql.raw("");
2941
+ const orderBy = compileRecursiveOrderBy(ast, dialect);
2942
+ const limitOffset = compileLimitOffset(ast);
2943
+ return emitRecursiveQuerySql({
2944
+ depthFilter,
2945
+ ...limitOffset === void 0 ? {} : { limitOffset },
2946
+ logicalPlan,
2947
+ ...orderBy === void 0 ? {} : { orderBy },
2948
+ projection,
2949
+ recursiveCte
2950
+ });
2951
+ }
2952
+ function hasVariableLengthTraversal(ast) {
2953
+ return ast.traversals.some((t) => t.variableLength !== void 0);
2954
+ }
2955
+ function compileRecursiveCte(ast, traversal, graphId, ctx, requiredColumnsByAlias, temporalFilterPass) {
2956
+ const { dialect } = ctx;
2957
+ const startAlias = ast.start.alias;
2958
+ const startKinds = ast.start.kinds;
2959
+ const nodeAlias = traversal.nodeAlias;
2391
2960
  const directEdgeKinds = [...new Set(traversal.edgeKinds)];
2392
2961
  const inverseEdgeKinds = traversal.inverseEdgeKinds === void 0 ? [] : [...new Set(traversal.inverseEdgeKinds)];
2962
+ const forceWorktableOuterJoinOrder = dialect.capabilities.forceRecursiveWorktableOuterJoinOrder;
2393
2963
  const nodeKinds = traversal.nodeKinds;
2394
- const nodeKindFilter = compileKindFilter(sql.raw("n.kind"), nodeKinds);
2964
+ const previousNodeKinds = [.../* @__PURE__ */ new Set([...startKinds, ...nodeKinds])];
2965
+ const direction = traversal.direction;
2966
+ const vl = traversal.variableLength;
2967
+ const shouldEnforceCycleCheck = vl.cyclePolicy !== "allow";
2968
+ const shouldTrackPath = shouldEnforceCycleCheck || vl.pathAlias !== void 0;
2969
+ const recursiveJoinRequiredColumns = /* @__PURE__ */ new Set(["id"]);
2970
+ if (previousNodeKinds.length > 1) {
2971
+ recursiveJoinRequiredColumns.add("kind");
2972
+ }
2973
+ const requiredStartColumns = requiredColumnsByAlias ? requiredColumnsByAlias.get(startAlias) ?? EMPTY_REQUIRED_COLUMNS : void 0;
2974
+ const requiredNodeColumns = requiredColumnsByAlias ? requiredColumnsByAlias.get(nodeAlias) ?? EMPTY_REQUIRED_COLUMNS : void 0;
2975
+ const startColumnsFromBase = compileNodeSelectColumnsFromTable(
2976
+ "n0",
2977
+ startAlias,
2978
+ requiredStartColumns,
2979
+ NO_ALWAYS_REQUIRED_COLUMNS
2980
+ );
2981
+ const startColumnsFromRecursive = compileNodeSelectColumnsFromRecursiveRow(
2982
+ startAlias,
2983
+ requiredStartColumns,
2984
+ NO_ALWAYS_REQUIRED_COLUMNS
2985
+ );
2986
+ const nodeColumnsFromBase = compileNodeSelectColumnsFromTable(
2987
+ "n0",
2988
+ nodeAlias,
2989
+ requiredNodeColumns,
2990
+ recursiveJoinRequiredColumns
2991
+ );
2992
+ const nodeColumnsFromRecursive = compileNodeSelectColumnsFromTable(
2993
+ "n",
2994
+ nodeAlias,
2995
+ requiredNodeColumns,
2996
+ recursiveJoinRequiredColumns
2997
+ );
2998
+ const startKindFilter = compileKindFilter2(startKinds, "n0.kind");
2999
+ const nodeKindFilter = compileKindFilter2(nodeKinds, "n.kind");
3000
+ const startTemporalFilter = temporalFilterPass.forAlias("n0");
2395
3001
  const edgeTemporalFilter = temporalFilterPass.forAlias("e");
2396
3002
  const nodeTemporalFilter = temporalFilterPass.forAlias("n");
2397
- const nodeCteContext = {
2398
- ...ctx,
2399
- cteColumnPrefix: "n"
2400
- };
2401
- const nodePredicateClauses = compilePredicateClauses(
2402
- getPredicatesForAlias(predicateIndex, traversal.nodeAlias, "node"),
2403
- nodeCteContext
3003
+ const startContext = { ...ctx, cteColumnPrefix: "" };
3004
+ const startPredicates = compileNodePredicates(ast, startAlias, startContext);
3005
+ const edgeContext = { ...ctx, cteColumnPrefix: "e" };
3006
+ const edgePredicates = compileEdgePredicates(
3007
+ ast,
3008
+ traversal.edgeAlias,
3009
+ edgeContext
2404
3010
  );
2405
- const edgeCteContext = {
2406
- ...ctx,
2407
- cteColumnPrefix: "e"
2408
- };
2409
- const edgePredicateClauses = compilePredicateClauses(
2410
- getPredicatesForAlias(predicateIndex, traversal.edgeAlias, "edge"),
2411
- edgeCteContext
3011
+ const targetContext = { ...ctx, cteColumnPrefix: "n" };
3012
+ const targetNodePredicates = compileNodePredicates(
3013
+ ast,
3014
+ nodeAlias,
3015
+ targetContext
2412
3016
  );
3017
+ if (vl.maxDepth > MAX_EXPLICIT_RECURSIVE_DEPTH) {
3018
+ throw new UnsupportedPredicateError(
3019
+ `Recursive traversal maxHops(${vl.maxDepth}) exceeds maximum explicit depth of ${MAX_EXPLICIT_RECURSIVE_DEPTH}`
3020
+ );
3021
+ }
3022
+ const effectiveMaxDepth = vl.maxDepth > 0 ? vl.maxDepth : MAX_RECURSIVE_DEPTH;
3023
+ const maxDepthCondition = sql`r.depth < ${effectiveMaxDepth}`;
3024
+ const cycleCheck = shouldEnforceCycleCheck ? dialect.cycleCheck(sql.raw("n.id"), sql.raw("r.path")) : void 0;
3025
+ const initialPath = shouldTrackPath ? dialect.initializePath(sql.raw("n0.id")) : void 0;
3026
+ const pathExtension = shouldTrackPath ? dialect.extendPath(sql.raw("r.path"), sql.raw("n.id")) : void 0;
2413
3027
  const baseWhereClauses = [
3028
+ sql`n0.graph_id = ${graphId}`,
3029
+ startKindFilter,
3030
+ startTemporalFilter,
3031
+ ...startPredicates
3032
+ ];
3033
+ const recursiveBaseWhereClauses = [
2414
3034
  sql`e.graph_id = ${graphId}`,
2415
3035
  nodeKindFilter,
2416
3036
  edgeTemporalFilter,
2417
3037
  nodeTemporalFilter,
2418
- ...nodePredicateClauses,
2419
- ...edgePredicateClauses
2420
- ];
2421
- const previousAlias = traversal.joinFromAlias;
2422
- const edgeAlias = traversal.edgeAlias;
2423
- const nodeAlias = traversal.nodeAlias;
2424
- const requiredNodeColumns = requiredColumnsByAlias ? requiredColumnsByAlias.get(nodeAlias) ?? EMPTY_REQUIRED_COLUMNS : void 0;
2425
- const requiredEdgeColumns = requiredColumnsByAlias ? requiredColumnsByAlias.get(edgeAlias) ?? EMPTY_REQUIRED_COLUMNS : void 0;
2426
- const previousRowColumns = carryForwardPreviousColumns ? [sql`cte_${sql.raw(previousAlias)}.*`] : [
2427
- sql`cte_${sql.raw(previousAlias)}.${sql.raw(previousAlias)}_id AS ${sql.raw(previousAlias)}_id`,
2428
- sql`cte_${sql.raw(previousAlias)}.${sql.raw(previousAlias)}_kind AS ${sql.raw(previousAlias)}_kind`
2429
- ];
2430
- const selectColumns = [
2431
- ...previousRowColumns,
2432
- ...compileEdgeSelectColumns("e", edgeAlias, requiredEdgeColumns),
2433
- ...compileNodeSelectColumns("n", nodeAlias, requiredNodeColumns)
3038
+ maxDepthCondition
2434
3039
  ];
2435
- const cteMaterialization = materializeCte ? sql`MATERIALIZED ` : sql``;
2436
- function compileTraversalBranch(branch) {
2437
- const whereClauses = [
2438
- ...baseWhereClauses,
2439
- compileKindFilter(sql.raw("e.kind"), branch.edgeKinds),
2440
- compileKindFilter(
2441
- sql.raw(`e.${branch.joinKindField}`),
2442
- previousNodeKinds
2443
- ),
2444
- compileKindFilter(sql.raw(`e.${branch.targetKindField}`), nodeKinds)
3040
+ if (cycleCheck !== void 0) {
3041
+ recursiveBaseWhereClauses.push(cycleCheck);
3042
+ }
3043
+ recursiveBaseWhereClauses.push(...edgePredicates, ...targetNodePredicates);
3044
+ function compileRecursiveBranch2(branch) {
3045
+ const recursiveFilterClauses = [
3046
+ ...recursiveBaseWhereClauses,
3047
+ compileKindFilter2(branch.edgeKinds, "e.kind"),
3048
+ compileKindFilter2(previousNodeKinds, `e.${branch.joinKindField}`),
3049
+ compileKindFilter2(nodeKinds, `e.${branch.targetKindField}`)
2445
3050
  ];
2446
3051
  if (branch.duplicateGuard !== void 0) {
2447
- whereClauses.push(branch.duplicateGuard);
3052
+ recursiveFilterClauses.push(branch.duplicateGuard);
2448
3053
  }
2449
- return sql`
2450
- SELECT ${sql.join(selectColumns, sql`, `)}
2451
- FROM cte_${sql.raw(previousAlias)}
2452
- JOIN ${ctx.schema.edgesTable} e ON cte_${sql.raw(previousAlias)}.${sql.raw(previousAlias)}_id = e.${sql.raw(branch.joinField)}
2453
- AND cte_${sql.raw(previousAlias)}.${sql.raw(previousAlias)}_kind = e.${sql.raw(branch.joinKindField)}
3054
+ const recursiveSelectColumns = [
3055
+ ...startColumnsFromRecursive,
3056
+ ...nodeColumnsFromRecursive,
3057
+ sql`r.depth + 1 AS depth`
3058
+ ];
3059
+ if (pathExtension !== void 0) {
3060
+ recursiveSelectColumns.push(sql`${pathExtension} AS path`);
3061
+ }
3062
+ const recursiveJoinClauses = [
3063
+ sql`e.${sql.raw(branch.joinField)} = r.${sql.raw(nodeAlias)}_id`
3064
+ ];
3065
+ if (previousNodeKinds.length > 1) {
3066
+ recursiveJoinClauses.push(
3067
+ sql`e.${sql.raw(branch.joinKindField)} = r.${sql.raw(nodeAlias)}_kind`
3068
+ );
3069
+ }
3070
+ if (forceWorktableOuterJoinOrder) {
3071
+ const recursiveWhereClauses = [
3072
+ ...recursiveJoinClauses,
3073
+ ...recursiveFilterClauses
3074
+ ];
3075
+ return sql`
3076
+ SELECT ${sql.join(recursiveSelectColumns, sql`, `)}
3077
+ FROM recursive_cte r
3078
+ CROSS JOIN ${ctx.schema.edgesTable} e
3079
+ JOIN ${ctx.schema.nodesTable} n ON n.graph_id = e.graph_id
3080
+ AND n.id = e.${sql.raw(branch.targetField)}
3081
+ AND n.kind = e.${sql.raw(branch.targetKindField)}
3082
+ WHERE ${sql.join(recursiveWhereClauses, sql` AND `)}
3083
+ `;
3084
+ }
3085
+ return sql`
3086
+ SELECT ${sql.join(recursiveSelectColumns, sql`, `)}
3087
+ FROM recursive_cte r
3088
+ JOIN ${ctx.schema.edgesTable} e ON ${sql.join(recursiveJoinClauses, sql` AND `)}
2454
3089
  JOIN ${ctx.schema.nodesTable} n ON n.graph_id = e.graph_id
2455
3090
  AND n.id = e.${sql.raw(branch.targetField)}
2456
3091
  AND n.kind = e.${sql.raw(branch.targetKindField)}
2457
- WHERE ${sql.join(whereClauses, sql` AND `)}
3092
+ WHERE ${sql.join(recursiveFilterClauses, sql` AND `)}
2458
3093
  `;
2459
3094
  }
2460
- const directJoinField = traversal.direction === "out" ? "from_id" : "to_id";
2461
- const directTargetField = traversal.direction === "out" ? "to_id" : "from_id";
2462
- const directJoinKindField = traversal.direction === "out" ? "from_kind" : "to_kind";
2463
- const directTargetKindField = traversal.direction === "out" ? "to_kind" : "from_kind";
2464
- const directBranch = compileTraversalBranch({
2465
- edgeKinds: directEdgeKinds,
3095
+ const directJoinField = direction === "out" ? "from_id" : "to_id";
3096
+ const directTargetField = direction === "out" ? "to_id" : "from_id";
3097
+ const directJoinKindField = direction === "out" ? "from_kind" : "to_kind";
3098
+ const directTargetKindField = direction === "out" ? "to_kind" : "from_kind";
3099
+ const directBranch = compileRecursiveBranch2({
2466
3100
  joinField: directJoinField,
2467
- joinKindField: directJoinKindField,
2468
3101
  targetField: directTargetField,
2469
- targetKindField: directTargetKindField
3102
+ joinKindField: directJoinKindField,
3103
+ targetKindField: directTargetKindField,
3104
+ edgeKinds: directEdgeKinds
2470
3105
  });
2471
- if (inverseEdgeKinds.length === 0) {
2472
- if (traversalLimitValue !== void 0) {
2473
- return sql`
2474
- cte_${sql.raw(nodeAlias)} AS ${cteMaterialization}(
2475
- SELECT * FROM (
2476
- ${directBranch}
2477
- ) AS traversal_rows
2478
- LIMIT ${traversalLimitValue}
2479
- )
2480
- `;
2481
- }
3106
+ function compileInverseRecursiveBranch() {
3107
+ const inverseJoinField = direction === "out" ? "to_id" : "from_id";
3108
+ const inverseTargetField = direction === "out" ? "from_id" : "to_id";
3109
+ const inverseJoinKindField = direction === "out" ? "to_kind" : "from_kind";
3110
+ const inverseTargetKindField = direction === "out" ? "from_kind" : "to_kind";
3111
+ const overlappingKinds = inverseEdgeKinds.filter(
3112
+ (kind) => directEdgeKinds.includes(kind)
3113
+ );
3114
+ const duplicateGuard = overlappingKinds.length > 0 ? sql`NOT (e.from_id = e.to_id AND ${compileKindFilter2(overlappingKinds, "e.kind")})` : void 0;
3115
+ const inverseBranch = compileRecursiveBranch2({
3116
+ joinField: inverseJoinField,
3117
+ targetField: inverseTargetField,
3118
+ joinKindField: inverseJoinKindField,
3119
+ targetKindField: inverseTargetKindField,
3120
+ edgeKinds: inverseEdgeKinds,
3121
+ duplicateGuard
3122
+ });
2482
3123
  return sql`
2483
- cte_${sql.raw(nodeAlias)} AS ${cteMaterialization}(
2484
- ${directBranch}
2485
- )
3124
+ ${directBranch}
3125
+ UNION ALL
3126
+ ${inverseBranch}
2486
3127
  `;
2487
3128
  }
2488
- const inverseJoinField = traversal.direction === "out" ? "to_id" : "from_id";
2489
- const inverseTargetField = traversal.direction === "out" ? "from_id" : "to_id";
2490
- const inverseJoinKindField = traversal.direction === "out" ? "to_kind" : "from_kind";
2491
- const inverseTargetKindField = traversal.direction === "out" ? "from_kind" : "to_kind";
2492
- const overlappingKinds = inverseEdgeKinds.filter(
2493
- (kind) => directEdgeKinds.includes(kind)
2494
- );
2495
- const duplicateGuard = overlappingKinds.length > 0 ? sql`NOT (e.from_id = e.to_id AND ${compileKindFilter(
2496
- sql.raw("e.kind"),
2497
- overlappingKinds
2498
- )})` : void 0;
2499
- const inverseBranch = compileTraversalBranch({
2500
- duplicateGuard,
2501
- edgeKinds: inverseEdgeKinds,
2502
- joinField: inverseJoinField,
2503
- joinKindField: inverseJoinKindField,
2504
- targetField: inverseTargetField,
2505
- targetKindField: inverseTargetKindField
2506
- });
2507
- if (traversalLimitValue !== void 0) {
2508
- return sql`
2509
- cte_${sql.raw(nodeAlias)} AS ${cteMaterialization}(
2510
- SELECT * FROM (
2511
- ${directBranch}
2512
- UNION ALL
2513
- ${inverseBranch}
2514
- ) AS traversal_rows
2515
- LIMIT ${traversalLimitValue}
2516
- )
2517
- `;
3129
+ const recursiveBranchSql = inverseEdgeKinds.length === 0 ? directBranch : compileInverseRecursiveBranch();
3130
+ const baseSelectColumns = [
3131
+ ...startColumnsFromBase,
3132
+ ...nodeColumnsFromBase,
3133
+ sql`0 AS depth`
3134
+ ];
3135
+ if (initialPath !== void 0) {
3136
+ baseSelectColumns.push(sql`${initialPath} AS path`);
2518
3137
  }
2519
3138
  return sql`
2520
- cte_${sql.raw(nodeAlias)} AS ${cteMaterialization}(
2521
- ${directBranch}
3139
+ recursive_cte AS (
3140
+ -- Base case: starting nodes
3141
+ SELECT ${sql.join(baseSelectColumns, sql`, `)}
3142
+ FROM ${ctx.schema.nodesTable} n0
3143
+ WHERE ${sql.join(baseWhereClauses, sql` AND `)}
3144
+
2522
3145
  UNION ALL
2523
- ${inverseBranch}
3146
+
3147
+ -- Recursive case: follow edges
3148
+ ${recursiveBranchSql}
2524
3149
  )
2525
3150
  `;
2526
3151
  }
2527
- function compileAggregateExprFromSource(expr, dialect) {
2528
- const { field: field2 } = expr;
2529
- const fn = expr.function;
2530
- switch (fn) {
2531
- case "count":
2532
- case "countDistinct":
2533
- case "sum":
2534
- case "avg":
2535
- case "min":
2536
- case "max": {
2537
- const cteAlias = `cte_${field2.alias}`;
2538
- const column = compileFieldValue(
2539
- field2,
2540
- dialect,
2541
- field2.valueType,
2542
- cteAlias
2543
- );
2544
- if (fn === "countDistinct") {
2545
- return sql`COUNT(DISTINCT ${column})`;
2546
- }
2547
- return sql`${sql.raw(fn.toUpperCase())}(${column})`;
2548
- }
2549
- default: {
2550
- throw new UnsupportedPredicateError(
2551
- `Unknown aggregate function: ${String(fn)}`
2552
- );
3152
+ function compileKindFilter2(kinds, columnExpr) {
3153
+ return compileKindFilter(sql.raw(columnExpr), kinds);
3154
+ }
3155
+ function compileNodePredicates(ast, alias, ctx) {
3156
+ return ast.predicates.filter((p) => p.targetAlias === alias && p.targetType !== "edge").map((p) => compilePredicateExpression(p.expression, ctx));
3157
+ }
3158
+ function compileEdgePredicates(ast, edgeAlias, ctx) {
3159
+ return ast.predicates.filter((p) => p.targetAlias === edgeAlias && p.targetType === "edge").map((p) => compilePredicateExpression(p.expression, ctx));
3160
+ }
3161
+ function collectRequiredColumnsByAlias(ast, traversal) {
3162
+ const selectiveFields = ast.selectiveFields;
3163
+ if (selectiveFields === void 0 || selectiveFields.length === 0) {
3164
+ return void 0;
3165
+ }
3166
+ const requiredColumnsByAlias = /* @__PURE__ */ new Map();
3167
+ const previousNodeKinds = [
3168
+ .../* @__PURE__ */ new Set([...ast.start.kinds, ...traversal.nodeKinds])
3169
+ ];
3170
+ addRequiredColumn(requiredColumnsByAlias, traversal.nodeAlias, "id");
3171
+ if (previousNodeKinds.length > 1) {
3172
+ addRequiredColumn(requiredColumnsByAlias, traversal.nodeAlias, "kind");
3173
+ }
3174
+ for (const field2 of selectiveFields) {
3175
+ markSelectiveFieldAsRequired(requiredColumnsByAlias, field2);
3176
+ }
3177
+ if (ast.orderBy) {
3178
+ for (const orderSpec of ast.orderBy) {
3179
+ markFieldRefAsRequired(requiredColumnsByAlias, orderSpec.field);
2553
3180
  }
2554
3181
  }
3182
+ return requiredColumnsByAlias;
2555
3183
  }
2556
- function compileProjectedSource(field2, dialect) {
2557
- if (isAggregateExpr(field2.source)) {
2558
- return compileAggregateExprFromSource(field2.source, dialect);
2559
- }
2560
- const cteAlias = field2.cteAlias ?? `cte_${field2.source.alias}`;
2561
- return compileFieldValue(
2562
- field2.source,
2563
- dialect,
2564
- field2.source.valueType,
2565
- cteAlias
3184
+ function compileNodeSelectColumnsFromTable(tableAlias, alias, requiredColumns, alwaysRequiredColumns) {
3185
+ return NODE_COLUMNS.filter(
3186
+ (column) => shouldProjectColumn(requiredColumns, column, alwaysRequiredColumns)
3187
+ ).map(
3188
+ (column) => sql`${sql.raw(tableAlias)}.${sql.raw(column)} AS ${sql.raw(`${alias}_${column}`)}`
2566
3189
  );
2567
3190
  }
2568
- function buildStandardProjection(input) {
2569
- const { ast, collapsedTraversalCteAlias, dialect } = input;
3191
+ function compileNodeSelectColumnsFromRecursiveRow(alias, requiredColumns, alwaysRequiredColumns) {
3192
+ return NODE_COLUMNS.filter(
3193
+ (column) => shouldProjectColumn(requiredColumns, column, alwaysRequiredColumns)
3194
+ ).map((column) => {
3195
+ const projected = `${alias}_${column}`;
3196
+ return sql`r.${sql.raw(projected)} AS ${sql.raw(projected)}`;
3197
+ });
3198
+ }
3199
+ function compileRecursiveProjection(ast, traversal, dialect) {
2570
3200
  if (ast.selectiveFields && ast.selectiveFields.length > 0) {
2571
- return compileSelectiveProjection(
3201
+ return compileRecursiveSelectiveProjection(
2572
3202
  ast.selectiveFields,
2573
- dialect,
2574
3203
  ast,
2575
- collapsedTraversalCteAlias
3204
+ traversal,
3205
+ dialect
2576
3206
  );
2577
3207
  }
2578
- const fields = ast.projection.fields;
2579
- if (fields.length === 0) {
2580
- return sql.raw("*");
3208
+ const startAlias = ast.start.alias;
3209
+ const nodeAlias = traversal.nodeAlias;
3210
+ const vl = traversal.variableLength;
3211
+ const fields = [
3212
+ // Start alias fields with metadata
3213
+ sql`${sql.raw(startAlias)}_id`,
3214
+ sql`${sql.raw(startAlias)}_kind`,
3215
+ sql`${sql.raw(startAlias)}_props`,
3216
+ sql`${sql.raw(startAlias)}_version`,
3217
+ sql`${sql.raw(startAlias)}_valid_from`,
3218
+ sql`${sql.raw(startAlias)}_valid_to`,
3219
+ sql`${sql.raw(startAlias)}_created_at`,
3220
+ sql`${sql.raw(startAlias)}_updated_at`,
3221
+ sql`${sql.raw(startAlias)}_deleted_at`,
3222
+ // Node alias fields with metadata
3223
+ sql`${sql.raw(nodeAlias)}_id`,
3224
+ sql`${sql.raw(nodeAlias)}_kind`,
3225
+ sql`${sql.raw(nodeAlias)}_props`,
3226
+ sql`${sql.raw(nodeAlias)}_version`,
3227
+ sql`${sql.raw(nodeAlias)}_valid_from`,
3228
+ sql`${sql.raw(nodeAlias)}_valid_to`,
3229
+ sql`${sql.raw(nodeAlias)}_created_at`,
3230
+ sql`${sql.raw(nodeAlias)}_updated_at`,
3231
+ sql`${sql.raw(nodeAlias)}_deleted_at`
3232
+ ];
3233
+ if (vl.depthAlias !== void 0) {
3234
+ fields.push(sql`depth AS ${quoteIdentifier(vl.depthAlias)}`);
2581
3235
  }
2582
- const projectedFields = fields.map((field2) => {
2583
- const source = compileProjectedSource(field2, dialect);
2584
- return sql`${source} AS ${quoteIdentifier(field2.outputName)}`;
2585
- });
2586
- return sql.join(projectedFields, sql`, `);
2587
- }
2588
- function compileSelectiveProjection(fields, dialect, ast, collapsedTraversalCteAlias) {
2589
- const aliasToCte = /* @__PURE__ */ new Map([
2590
- [ast.start.alias, `cte_${ast.start.alias}`]
2591
- ]);
2592
- for (const traversal of ast.traversals) {
2593
- aliasToCte.set(traversal.nodeAlias, `cte_${traversal.nodeAlias}`);
2594
- aliasToCte.set(traversal.edgeAlias, `cte_${traversal.nodeAlias}`);
3236
+ if (vl.pathAlias !== void 0) {
3237
+ fields.push(sql`path AS ${quoteIdentifier(vl.pathAlias)}`);
2595
3238
  }
3239
+ return sql.join(fields, sql`, `);
3240
+ }
3241
+ function compileRecursiveSelectiveProjection(fields, ast, traversal, dialect) {
3242
+ const allowedAliases = /* @__PURE__ */ new Set([ast.start.alias, traversal.nodeAlias]);
2596
3243
  const columns = fields.map((field2) => {
2597
- const cteAlias = collapsedTraversalCteAlias ?? aliasToCte.get(field2.alias) ?? `cte_${field2.alias}`;
3244
+ if (!allowedAliases.has(field2.alias)) {
3245
+ throw new UnsupportedPredicateError(
3246
+ `Selective projection for recursive traversals does not support alias "${field2.alias}"`
3247
+ );
3248
+ }
2598
3249
  if (field2.isSystemField) {
2599
3250
  const dbColumn = mapSelectiveSystemFieldToColumn(field2.field);
2600
- return sql`${sql.raw(cteAlias)}.${sql.raw(`${field2.alias}_${dbColumn}`)} AS ${quoteIdentifier(field2.outputName)}`;
3251
+ return sql`${sql.raw(`${field2.alias}_${dbColumn}`)} AS ${quoteIdentifier(field2.outputName)}`;
2601
3252
  }
2602
- const propsColumn = `${field2.alias}_props`;
2603
- const column = sql`${sql.raw(cteAlias)}.${sql.raw(propsColumn)}`;
2604
- const pointer = jsonPointer([field2.field]);
3253
+ const column = sql.raw(`${field2.alias}_props`);
2605
3254
  const extracted = compileTypedJsonExtract({
2606
3255
  column,
2607
3256
  dialect,
2608
- pointer,
3257
+ pointer: jsonPointer([field2.field]),
2609
3258
  valueType: field2.valueType
2610
3259
  });
2611
3260
  return sql`${extracted} AS ${quoteIdentifier(field2.outputName)}`;
2612
3261
  });
2613
- return sql.join(columns, sql`, `);
2614
- }
2615
- function buildStandardFromClause(input) {
2616
- const { ast, collapsedTraversalCteAlias, vectorPredicate } = input;
2617
- if (collapsedTraversalCteAlias !== void 0) {
2618
- return sql`FROM ${sql.raw(collapsedTraversalCteAlias)}`;
2619
- }
2620
- const startAlias = ast.start.alias;
2621
- const fromClause = sql`FROM cte_${sql.raw(startAlias)}`;
2622
- const joins = [];
2623
- for (const traversal of ast.traversals) {
2624
- const cteAlias = `cte_${traversal.nodeAlias}`;
2625
- const previousAlias = traversal.joinFromAlias;
2626
- const joinType = traversal.optional ? "LEFT JOIN" : "INNER JOIN";
2627
- joins.push(
2628
- sql`${sql.raw(joinType)} ${sql.raw(cteAlias)} ON ${sql.raw(cteAlias)}.${sql.raw(previousAlias)}_id = cte_${sql.raw(previousAlias)}.${sql.raw(previousAlias)}_id AND ${sql.raw(cteAlias)}.${sql.raw(previousAlias)}_kind = cte_${sql.raw(previousAlias)}.${sql.raw(previousAlias)}_kind`
2629
- );
3262
+ const vl = traversal.variableLength;
3263
+ if (vl.depthAlias !== void 0) {
3264
+ columns.push(sql`depth AS ${quoteIdentifier(vl.depthAlias)}`);
2630
3265
  }
2631
- if (vectorPredicate) {
2632
- const nodeAlias = vectorPredicate.field.alias;
2633
- joins.push(
2634
- sql`INNER JOIN cte_embeddings ON cte_embeddings.node_id = cte_${sql.raw(nodeAlias)}.${sql.raw(nodeAlias)}_id`
2635
- );
3266
+ if (vl.pathAlias !== void 0) {
3267
+ columns.push(sql`path AS ${quoteIdentifier(vl.pathAlias)}`);
2636
3268
  }
2637
- return joins.length === 0 ? fromClause : sql`${fromClause} ${sql.join(joins, sql` `)}`;
3269
+ return sql.join(columns, sql`, `);
2638
3270
  }
2639
- function buildStandardOrderBy(input) {
2640
- const { ast, collapsedTraversalCteAlias, dialect } = input;
3271
+ function compileRecursiveOrderBy(ast, dialect) {
2641
3272
  if (!ast.orderBy || ast.orderBy.length === 0) {
2642
3273
  return void 0;
2643
3274
  }
@@ -2649,13 +3280,7 @@ function buildStandardOrderBy(input) {
2649
3280
  "Ordering by JSON arrays or objects is not supported"
2650
3281
  );
2651
3282
  }
2652
- const cteAlias = collapsedTraversalCteAlias ?? `cte_${orderSpec.field.alias}`;
2653
- const field2 = compileFieldValue(
2654
- orderSpec.field,
2655
- dialect,
2656
- valueType,
2657
- cteAlias
2658
- );
3283
+ const field2 = compileFieldValue(orderSpec.field, dialect, valueType);
2659
3284
  const direction = sql.raw(orderSpec.direction.toUpperCase());
2660
3285
  const nulls = orderSpec.nulls ?? (orderSpec.direction === "asc" ? "last" : "first");
2661
3286
  const nullsDirection = sql.raw(nulls === "first" ? "DESC" : "ASC");
@@ -2666,677 +3291,585 @@ function buildStandardOrderBy(input) {
2666
3291
  }
2667
3292
  return sql`ORDER BY ${sql.join(parts, sql`, `)}`;
2668
3293
  }
2669
- function fieldRefKey(field2) {
2670
- const pointer = field2.jsonPointer ?? "";
2671
- return `${field2.alias}:${field2.path.join(".")}:${pointer}`;
2672
- }
2673
- function buildStandardGroupBy(input) {
2674
- const { ast, dialect } = input;
2675
- if (!ast.groupBy || ast.groupBy.fields.length === 0) {
2676
- return void 0;
2677
- }
2678
- const seenKeys = /* @__PURE__ */ new Set();
2679
- const allFields = [];
2680
- for (const projectedField of ast.projection.fields) {
2681
- if (projectedField.source.__type === "field_ref") {
2682
- const key = fieldRefKey(projectedField.source);
2683
- if (!seenKeys.has(key)) {
2684
- seenKeys.add(key);
2685
- allFields.push(projectedField.source);
2686
- }
2687
- }
2688
- }
2689
- for (const field2 of ast.groupBy.fields) {
2690
- const key = fieldRefKey(field2);
2691
- if (!seenKeys.has(key)) {
2692
- seenKeys.add(key);
2693
- allFields.push(field2);
2694
- }
3294
+ function compileLimitOffset(ast) {
3295
+ const parts = [];
3296
+ if (ast.limit !== void 0) {
3297
+ parts.push(sql`LIMIT ${ast.limit}`);
2695
3298
  }
2696
- if (allFields.length === 0) {
2697
- return void 0;
3299
+ if (ast.offset !== void 0) {
3300
+ parts.push(sql`OFFSET ${ast.offset}`);
2698
3301
  }
2699
- const parts = allFields.map(
2700
- (field2) => compileFieldValue(field2, dialect, field2.valueType, `cte_${field2.alias}`)
2701
- );
2702
- return sql`GROUP BY ${sql.join(parts, sql`, `)}`;
3302
+ return parts.length > 0 ? sql.join(parts, sql` `) : void 0;
2703
3303
  }
2704
- function buildStandardHaving(input) {
2705
- const { ast, ctx } = input;
2706
- if (!ast.having) {
2707
- return void 0;
3304
+ var DEFAULT_TABLE_NAMES = {
3305
+ nodes: "typegraph_nodes",
3306
+ edges: "typegraph_edges",
3307
+ embeddings: "typegraph_node_embeddings"
3308
+ };
3309
+ var MAX_IDENTIFIER_LENGTH = 63;
3310
+ var VALID_IDENTIFIER_PATTERN = /^[a-z_][a-z0-9_$]*$/i;
3311
+ function validateTableName(name, label) {
3312
+ if (!name || name.length === 0) {
3313
+ throw new ConfigurationError(`${label} table name cannot be empty`);
2708
3314
  }
2709
- const condition = compilePredicateExpression(ast.having, ctx);
2710
- return sql`HAVING ${condition}`;
2711
- }
2712
- function buildStandardEmbeddingsCte(input) {
2713
- const { ctx, graphId, vectorPredicate } = input;
2714
- const { dialect } = ctx;
2715
- const { field: field2, metric, minScore, queryEmbedding } = vectorPredicate;
2716
- const fieldPath = field2.jsonPointer ? field2.jsonPointer : field2.path.length > 1 && field2.path[0] === "props" ? `/${field2.path.slice(1).join("/")}` : `/${field2.path.join("/")}`;
2717
- const distanceExpr = dialect.vectorDistance(
2718
- sql.raw("embedding"),
2719
- queryEmbedding,
2720
- metric
2721
- );
2722
- const conditions = [
2723
- sql`graph_id = ${graphId}`,
2724
- sql`field_path = ${fieldPath}`
2725
- ];
2726
- if (minScore !== void 0) {
2727
- conditions.push(
2728
- compileVectorMinScoreCondition(distanceExpr, metric, minScore)
3315
+ if (name.length > MAX_IDENTIFIER_LENGTH) {
3316
+ throw new ConfigurationError(
3317
+ `${label} table name exceeds maximum length of ${MAX_IDENTIFIER_LENGTH} characters`
2729
3318
  );
2730
3319
  }
2731
- const scoreExpr = compileVectorScoreExpression(distanceExpr, metric);
2732
- return sql`
2733
- cte_embeddings AS (
2734
- SELECT
2735
- node_id,
2736
- ${distanceExpr} AS distance,
2737
- ${scoreExpr} AS score
2738
- FROM ${ctx.schema.embeddingsTable}
2739
- WHERE ${sql.join(conditions, sql` AND `)}
2740
- ORDER BY ${distanceExpr} ASC
2741
- )
2742
- `;
2743
- }
2744
- function compileVectorScoreExpression(distanceExpr, metric) {
2745
- switch (metric) {
2746
- case "cosine": {
2747
- return sql`(1.0 - ${distanceExpr})`;
2748
- }
2749
- case "l2":
2750
- case "inner_product": {
2751
- return distanceExpr;
2752
- }
3320
+ if (!VALID_IDENTIFIER_PATTERN.test(name)) {
3321
+ throw new ConfigurationError(
3322
+ `${label} table name "${name}" is not a valid SQL identifier. Table names must start with a letter or underscore and contain only letters, digits, underscores, or dollar signs.`
3323
+ );
2753
3324
  }
2754
3325
  }
2755
- function compileVectorMinScoreCondition(distanceExpr, metric, minScore) {
2756
- switch (metric) {
2757
- case "cosine": {
2758
- const threshold = 1 - minScore;
2759
- return sql`${distanceExpr} <= ${threshold}`;
2760
- }
2761
- case "l2": {
2762
- return sql`${distanceExpr} <= ${minScore}`;
2763
- }
2764
- case "inner_product": {
2765
- const negativeThreshold = -minScore;
2766
- return sql`${distanceExpr} <= ${negativeThreshold}`;
2767
- }
2768
- }
3326
+ function quoteIdentifier2(name) {
3327
+ return `"${name.replaceAll('"', '""')}"`;
2769
3328
  }
2770
- function buildStandardVectorOrderBy(input) {
2771
- const { ast, dialect } = input;
2772
- const distanceOrder = sql`cte_embeddings.distance ASC`;
2773
- const additionalOrders = [];
2774
- if (ast.orderBy && ast.orderBy.length > 0) {
2775
- for (const orderSpec of ast.orderBy) {
2776
- const valueType = orderSpec.field.valueType;
2777
- if (valueType === "array" || valueType === "object") {
2778
- throw new UnsupportedPredicateError(
2779
- "Ordering by JSON arrays or objects is not supported"
2780
- );
2781
- }
2782
- const cteAlias = `cte_${orderSpec.field.alias}`;
2783
- const field2 = compileFieldValue(
2784
- orderSpec.field,
2785
- dialect,
2786
- valueType,
2787
- cteAlias
2788
- );
2789
- const direction = sql.raw(orderSpec.direction.toUpperCase());
2790
- const nulls = orderSpec.nulls ?? (orderSpec.direction === "asc" ? "last" : "first");
2791
- const nullsDirection = sql.raw(nulls === "first" ? "DESC" : "ASC");
2792
- additionalOrders.push(
2793
- sql`(${field2} IS NULL) ${nullsDirection}`,
2794
- sql`${field2} ${direction}`
2795
- );
2796
- }
2797
- }
2798
- const allOrders = [distanceOrder, ...additionalOrders];
2799
- return sql`ORDER BY ${sql.join(allOrders, sql`, `)}`;
3329
+ function createSqlSchema(names = {}) {
3330
+ const tables = { ...DEFAULT_TABLE_NAMES, ...names };
3331
+ validateTableName(tables.nodes, "nodes");
3332
+ validateTableName(tables.edges, "edges");
3333
+ validateTableName(tables.embeddings, "embeddings");
3334
+ return {
3335
+ tables,
3336
+ nodesTable: sql.raw(quoteIdentifier2(tables.nodes)),
3337
+ edgesTable: sql.raw(quoteIdentifier2(tables.edges)),
3338
+ embeddingsTable: sql.raw(quoteIdentifier2(tables.embeddings))
3339
+ };
2800
3340
  }
2801
- function buildLimitOffsetClause(input) {
2802
- const { limit, offset } = input;
2803
- const parts = [];
2804
- if (limit !== void 0) {
2805
- parts.push(sql`LIMIT ${limit}`);
2806
- }
2807
- if (offset !== void 0) {
2808
- parts.push(sql`OFFSET ${offset}`);
3341
+ var DEFAULT_SQL_SCHEMA = createSqlSchema();
3342
+
3343
+ // src/query/execution/value-decoder.ts
3344
+ function nullToUndefined(value) {
3345
+ return value === null ? void 0 : value;
3346
+ }
3347
+ function decodeSelectedValue(value, typeInfo) {
3348
+ const normalized = nullToUndefined(value);
3349
+ if (normalized === void 0) return void 0;
3350
+ if (typeInfo === void 0) {
3351
+ return normalized;
2809
3352
  }
2810
- return parts.length > 0 ? sql.join(parts, sql` `) : void 0;
3353
+ return decodeByValueType(normalized, typeInfo.valueType);
2811
3354
  }
2812
-
2813
- // src/query/compiler/recursive.ts
2814
- var MAX_RECURSIVE_DEPTH = 100;
2815
- var MAX_EXPLICIT_RECURSIVE_DEPTH = 1e3;
2816
- var NO_ALWAYS_REQUIRED_COLUMNS = /* @__PURE__ */ new Set();
2817
- function runRecursiveQueryPassPipeline(ast, graphId, ctx) {
2818
- let state = {
2819
- ast,
2820
- ctx,
2821
- graphId,
2822
- logicalPlan: void 0,
2823
- requiredColumnsByAlias: void 0,
2824
- temporalFilterPass: void 0,
2825
- traversal: void 0
2826
- };
2827
- const recursiveTraversalPass = runCompilerPass(state, {
2828
- name: "recursive_traversal",
2829
- execute(currentState) {
2830
- return runRecursiveTraversalSelectionPass(currentState.ast);
2831
- },
2832
- update(currentState, traversal) {
2833
- return {
2834
- ...currentState,
2835
- traversal
2836
- };
3355
+ function decodeByValueType(value, valueType) {
3356
+ switch (valueType) {
3357
+ case "boolean": {
3358
+ if (typeof value === "boolean") return value;
3359
+ if (typeof value === "number") return value !== 0;
3360
+ if (typeof value === "string") {
3361
+ if (value === "0") return false;
3362
+ if (value === "1") return true;
3363
+ if (value.toLowerCase() === "true") return true;
3364
+ if (value.toLowerCase() === "false") return false;
3365
+ }
3366
+ return Boolean(value);
2837
3367
  }
2838
- });
2839
- state = recursiveTraversalPass.state;
2840
- const temporalPass = runCompilerPass(state, {
2841
- name: "temporal_filters",
2842
- execute(currentState) {
2843
- return createTemporalFilterPass(
2844
- currentState.ast,
2845
- currentState.ctx.dialect.currentTimestamp()
2846
- );
2847
- },
2848
- update(currentState, temporalFilterPass) {
2849
- return {
2850
- ...currentState,
2851
- temporalFilterPass
2852
- };
3368
+ case "number": {
3369
+ if (typeof value === "number") return value;
3370
+ if (typeof value === "string") {
3371
+ const parsed = Number(value);
3372
+ return Number.isNaN(parsed) ? value : parsed;
3373
+ }
3374
+ return value;
2853
3375
  }
2854
- });
2855
- state = temporalPass.state;
2856
- const columnPruningPass = runCompilerPass(state, {
2857
- name: "column_pruning",
2858
- execute(currentState) {
2859
- const traversal = currentState.traversal;
2860
- if (traversal === void 0) {
2861
- throw new CompilerInvariantError(
2862
- "Recursive traversal pass did not select traversal"
2863
- );
3376
+ case "array":
3377
+ case "object":
3378
+ case "embedding": {
3379
+ if (typeof value !== "string") return value;
3380
+ const trimmed = value.trim();
3381
+ const looksJson = trimmed.startsWith("[") || trimmed.startsWith("{");
3382
+ if (!looksJson) return value;
3383
+ try {
3384
+ return JSON.parse(trimmed);
3385
+ } catch {
3386
+ return value;
2864
3387
  }
2865
- return collectRequiredColumnsByAlias(currentState.ast, traversal);
2866
- },
2867
- update(currentState, requiredColumnsByAlias) {
2868
- return {
2869
- ...currentState,
2870
- requiredColumnsByAlias
2871
- };
2872
3388
  }
2873
- });
2874
- state = columnPruningPass.state;
2875
- const logicalPlanPass = runCompilerPass(state, {
2876
- name: "logical_plan",
2877
- execute(currentState) {
2878
- const loweringInput = {
2879
- ast: currentState.ast,
2880
- dialect: currentState.ctx.dialect.name,
2881
- graphId: currentState.graphId,
2882
- ...currentState.traversal === void 0 ? {} : { traversal: currentState.traversal }
2883
- };
2884
- return lowerRecursiveQueryToLogicalPlan(loweringInput);
2885
- },
2886
- update(currentState, logicalPlan) {
2887
- return {
2888
- ...currentState,
2889
- logicalPlan
2890
- };
3389
+ case "string":
3390
+ case "date":
3391
+ case "unknown": {
3392
+ return value;
2891
3393
  }
2892
- });
2893
- state = logicalPlanPass.state;
2894
- return state;
2895
- }
2896
- function compileVariableLengthQuery(ast, graphId, ctx) {
2897
- const strategy = ctx.dialect.capabilities.recursiveQueryStrategy;
2898
- const handler = RECURSIVE_QUERY_STRATEGY_HANDLERS[strategy];
2899
- return handler(ast, graphId, ctx);
3394
+ default: {
3395
+ return value;
3396
+ }
3397
+ }
2900
3398
  }
2901
- var RECURSIVE_QUERY_STRATEGY_HANDLERS = {
2902
- recursive_cte: compileVariableLengthQueryWithRecursiveCteStrategy
2903
- };
2904
- function compileVariableLengthQueryWithRecursiveCteStrategy(ast, graphId, ctx) {
2905
- const passState = runRecursiveQueryPassPipeline(ast, graphId, ctx);
2906
- const { dialect } = ctx;
2907
- const {
2908
- logicalPlan,
2909
- requiredColumnsByAlias,
2910
- temporalFilterPass,
2911
- traversal: vlTraversal
2912
- } = passState;
2913
- if (temporalFilterPass === void 0) {
2914
- throw new CompilerInvariantError(
2915
- "Temporal filter pass did not initialize temporal state"
3399
+
3400
+ // src/store/reserved-keys.ts
3401
+ var RESERVED_NODE_KEYS2 = /* @__PURE__ */ new Set([
3402
+ "id",
3403
+ "kind",
3404
+ "meta"
3405
+ ]);
3406
+ var RESERVED_EDGE_KEYS2 = /* @__PURE__ */ new Set([
3407
+ "id",
3408
+ "kind",
3409
+ "meta",
3410
+ "fromKind",
3411
+ "fromId",
3412
+ "toKind",
3413
+ "toId"
3414
+ ]);
3415
+ var PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
3416
+ "__proto__",
3417
+ "constructor",
3418
+ "prototype"
3419
+ ]);
3420
+ function validateProjectionField(field2, entityType, kind) {
3421
+ const reserved = entityType === "node" ? RESERVED_NODE_KEYS2 : RESERVED_EDGE_KEYS2;
3422
+ if (reserved.has(field2)) {
3423
+ throw new ConfigurationError(
3424
+ `Projection field "${field2}" on ${entityType} kind "${kind}" conflicts with a reserved structural key`,
3425
+ { field: field2, kind, entityType, reservedKeys: [...reserved] },
3426
+ {
3427
+ suggestion: `Remove "${field2}" from the projection. Structural fields (${[...reserved].join(", ")}) are included automatically when relevant.`
3428
+ }
2916
3429
  );
2917
3430
  }
2918
- if (logicalPlan === void 0) {
2919
- throw new CompilerInvariantError(
2920
- "Logical plan pass did not initialize plan state"
3431
+ if (PROTOTYPE_POLLUTION_KEYS.has(field2)) {
3432
+ throw new ConfigurationError(
3433
+ `Projection field "${field2}" on ${entityType} kind "${kind}" is not allowed`,
3434
+ { field: field2, kind, entityType },
3435
+ {
3436
+ suggestion: `"${field2}" cannot be used as a projection field name.`
3437
+ }
2921
3438
  );
2922
3439
  }
2923
- if (vlTraversal === void 0) {
2924
- throw new CompilerInvariantError(
2925
- "Recursive traversal pass did not select traversal"
2926
- );
3440
+ }
3441
+ function filterReservedKeys(props, reservedKeys) {
3442
+ const filtered = {};
3443
+ for (const [key, value] of Object.entries(props)) {
3444
+ if (!reservedKeys.has(key)) {
3445
+ filtered[key] = value;
3446
+ }
2927
3447
  }
2928
- const recursiveCte = compileRecursiveCte(
2929
- ast,
2930
- vlTraversal,
2931
- graphId,
2932
- ctx,
2933
- requiredColumnsByAlias,
2934
- temporalFilterPass
2935
- );
2936
- const projection = compileRecursiveProjection(ast, vlTraversal, dialect);
2937
- const minDepth = vlTraversal.variableLength.minDepth;
2938
- const depthFilter = minDepth > 0 ? sql`WHERE depth >= ${minDepth}` : sql.raw("");
2939
- const orderBy = compileRecursiveOrderBy(ast, dialect);
2940
- const limitOffset = compileLimitOffset(ast);
2941
- return emitRecursiveQuerySql({
2942
- depthFilter,
2943
- ...limitOffset === void 0 ? {} : { limitOffset },
2944
- logicalPlan,
2945
- ...orderBy === void 0 ? {} : { orderBy },
2946
- projection,
2947
- recursiveCte
2948
- });
3448
+ return filtered;
2949
3449
  }
2950
- function hasVariableLengthTraversal(ast) {
2951
- return ast.traversals.some((t) => t.variableLength !== void 0);
3450
+
3451
+ // src/store/row-mappers.ts
3452
+ function nullToUndefined2(value) {
3453
+ return value === null ? void 0 : value;
2952
3454
  }
2953
- function compileRecursiveCte(ast, traversal, graphId, ctx, requiredColumnsByAlias, temporalFilterPass) {
2954
- const { dialect } = ctx;
2955
- const startAlias = ast.start.alias;
2956
- const startKinds = ast.start.kinds;
2957
- const nodeAlias = traversal.nodeAlias;
2958
- const directEdgeKinds = [...new Set(traversal.edgeKinds)];
2959
- const inverseEdgeKinds = traversal.inverseEdgeKinds === void 0 ? [] : [...new Set(traversal.inverseEdgeKinds)];
2960
- const forceWorktableOuterJoinOrder = dialect.capabilities.forceRecursiveWorktableOuterJoinOrder;
2961
- const nodeKinds = traversal.nodeKinds;
2962
- const previousNodeKinds = [.../* @__PURE__ */ new Set([...startKinds, ...nodeKinds])];
2963
- const direction = traversal.direction;
2964
- const vl = traversal.variableLength;
2965
- const shouldEnforceCycleCheck = vl.cyclePolicy !== "allow";
2966
- const shouldTrackPath = shouldEnforceCycleCheck || vl.pathAlias !== void 0;
2967
- const recursiveJoinRequiredColumns = /* @__PURE__ */ new Set(["id"]);
2968
- if (previousNodeKinds.length > 1) {
2969
- recursiveJoinRequiredColumns.add("kind");
3455
+ function rowToNode(row) {
3456
+ const rawProps = JSON.parse(row.props);
3457
+ const props = filterReservedKeys(rawProps, RESERVED_NODE_KEYS2);
3458
+ return {
3459
+ kind: row.kind,
3460
+ id: row.id,
3461
+ meta: rowToNodeMeta(row),
3462
+ ...props
3463
+ };
3464
+ }
3465
+ function rowToNodeMeta(row) {
3466
+ return {
3467
+ version: row.version,
3468
+ validFrom: nullToUndefined2(row.valid_from),
3469
+ validTo: nullToUndefined2(row.valid_to),
3470
+ createdAt: row.created_at,
3471
+ updatedAt: row.updated_at,
3472
+ deletedAt: nullToUndefined2(row.deleted_at)
3473
+ };
3474
+ }
3475
+ function rowToEdge(row) {
3476
+ const rawProps = JSON.parse(row.props);
3477
+ const props = filterReservedKeys(rawProps, RESERVED_EDGE_KEYS2);
3478
+ return {
3479
+ id: row.id,
3480
+ kind: row.kind,
3481
+ fromKind: row.from_kind,
3482
+ fromId: row.from_id,
3483
+ toKind: row.to_kind,
3484
+ toId: row.to_id,
3485
+ meta: rowToEdgeMeta(row),
3486
+ ...props
3487
+ };
3488
+ }
3489
+ function rowToEdgeMeta(row) {
3490
+ return {
3491
+ validFrom: nullToUndefined2(row.valid_from),
3492
+ validTo: nullToUndefined2(row.valid_to),
3493
+ createdAt: row.created_at,
3494
+ updatedAt: row.updated_at,
3495
+ deletedAt: nullToUndefined2(row.deleted_at)
3496
+ };
3497
+ }
3498
+
3499
+ // src/store/subgraph.ts
3500
+ var DEFAULT_SUBGRAPH_MAX_DEPTH = 10;
3501
+ var MAX_PG_IDENTIFIER_LENGTH = 63;
3502
+ function fnv1aBase36(input) {
3503
+ let hash = 2166136261;
3504
+ for (const character of input) {
3505
+ const codePoint = character.codePointAt(0);
3506
+ if (codePoint === void 0) continue;
3507
+ hash ^= codePoint;
3508
+ hash = Math.imul(hash, 16777619);
3509
+ }
3510
+ return (hash >>> 0).toString(36);
3511
+ }
3512
+ var TEXT_ENCODER = new TextEncoder();
3513
+ function truncateToBytes(value, maxBytes) {
3514
+ const encoded = TEXT_ENCODER.encode(value);
3515
+ if (encoded.byteLength <= maxBytes) return value;
3516
+ let end = maxBytes;
3517
+ while (end > 0 && encoded[end] >= 128 && encoded[end] < 192) {
3518
+ end--;
3519
+ }
3520
+ return new TextDecoder().decode(encoded.slice(0, end));
3521
+ }
3522
+ function projectionAlias(entityPrefix, kind, field2) {
3523
+ const prefix = entityPrefix === "node" ? "sg_n" : "sg_e";
3524
+ const hash = fnv1aBase36(`${kind}\0${field2}`);
3525
+ const fixedBytes = prefix.length + 1 + 1 + hash.length;
3526
+ const maxKindBytes = MAX_PG_IDENTIFIER_LENGTH - fixedBytes;
3527
+ const truncatedKind = truncateToBytes(kind, maxKindBytes);
3528
+ return `${prefix}_${truncatedKind}_${hash}`;
3529
+ }
3530
+ function normalizeProps(value) {
3531
+ return typeof value === "string" ? value : JSON.stringify(value ?? {});
3532
+ }
3533
+ function defineSubgraphProject(_graph) {
3534
+ return (project) => project;
3535
+ }
3536
+ async function executeSubgraph(params) {
3537
+ const { options } = params;
3538
+ if (options.edges.length === 0) {
3539
+ return { nodes: [], edges: [] };
2970
3540
  }
2971
- const requiredStartColumns = requiredColumnsByAlias ? requiredColumnsByAlias.get(startAlias) ?? EMPTY_REQUIRED_COLUMNS : void 0;
2972
- const requiredNodeColumns = requiredColumnsByAlias ? requiredColumnsByAlias.get(nodeAlias) ?? EMPTY_REQUIRED_COLUMNS : void 0;
2973
- const startColumnsFromBase = compileNodeSelectColumnsFromTable(
2974
- "n0",
2975
- startAlias,
2976
- requiredStartColumns,
2977
- NO_ALWAYS_REQUIRED_COLUMNS
3541
+ const maxDepth = Math.min(
3542
+ options.maxDepth ?? DEFAULT_SUBGRAPH_MAX_DEPTH,
3543
+ MAX_RECURSIVE_DEPTH
2978
3544
  );
2979
- const startColumnsFromRecursive = compileNodeSelectColumnsFromRecursiveRow(
2980
- startAlias,
2981
- requiredStartColumns,
2982
- NO_ALWAYS_REQUIRED_COLUMNS
3545
+ const ctx = {
3546
+ graphId: params.graphId,
3547
+ rootId: params.rootId,
3548
+ edgeKinds: options.edges,
3549
+ maxDepth,
3550
+ includeKinds: options.includeKinds,
3551
+ excludeRoot: options.excludeRoot ?? false,
3552
+ direction: options.direction ?? "out",
3553
+ cyclePolicy: options.cyclePolicy ?? "prevent",
3554
+ dialect: params.dialect,
3555
+ schema: params.schema ?? DEFAULT_SQL_SCHEMA,
3556
+ backend: params.backend
3557
+ };
3558
+ const schemaIntrospector = getSubgraphSchemaIntrospector(params.graph);
3559
+ const nodeProjectionPlan = buildProjectionPlan(
3560
+ getIncludedNodeKinds(params.graph, options.includeKinds),
3561
+ options.project?.nodes,
3562
+ (kind, field2) => schemaIntrospector.getFieldTypeInfo(kind, field2),
3563
+ "node"
2983
3564
  );
2984
- const nodeColumnsFromBase = compileNodeSelectColumnsFromTable(
2985
- "n0",
2986
- nodeAlias,
2987
- requiredNodeColumns,
2988
- recursiveJoinRequiredColumns
3565
+ const edgeProjectionPlan = buildProjectionPlan(
3566
+ dedupeStrings(options.edges),
3567
+ options.project?.edges,
3568
+ (kind, field2) => schemaIntrospector.getEdgeFieldTypeInfo(kind, field2),
3569
+ "edge"
3570
+ );
3571
+ const reachableCte = buildReachableCte(ctx);
3572
+ const includedIdsCte = buildIncludedIdsCte(ctx);
3573
+ const [nodeRows, edgeRows] = await Promise.all([
3574
+ fetchSubgraphNodes(ctx, reachableCte, includedIdsCte, nodeProjectionPlan),
3575
+ fetchSubgraphEdges(ctx, reachableCte, includedIdsCte, edgeProjectionPlan)
3576
+ ]);
3577
+ const nodes = nodeRows.map(
3578
+ (row) => mapSubgraphNodeRow(row, nodeProjectionPlan)
2989
3579
  );
2990
- const nodeColumnsFromRecursive = compileNodeSelectColumnsFromTable(
2991
- "n",
2992
- nodeAlias,
2993
- requiredNodeColumns,
2994
- recursiveJoinRequiredColumns
3580
+ const edges = edgeRows.map(
3581
+ (row) => mapSubgraphEdgeRow(row, edgeProjectionPlan)
2995
3582
  );
2996
- const startKindFilter = compileKindFilter2(startKinds, "n0.kind");
2997
- const nodeKindFilter = compileKindFilter2(nodeKinds, "n.kind");
2998
- const startTemporalFilter = temporalFilterPass.forAlias("n0");
2999
- const edgeTemporalFilter = temporalFilterPass.forAlias("e");
3000
- const nodeTemporalFilter = temporalFilterPass.forAlias("n");
3001
- const startContext = { ...ctx, cteColumnPrefix: "" };
3002
- const startPredicates = compileNodePredicates(ast, startAlias, startContext);
3003
- const edgeContext = { ...ctx, cteColumnPrefix: "e" };
3004
- const edgePredicates = compileEdgePredicates(
3005
- ast,
3006
- traversal.edgeAlias,
3007
- edgeContext
3583
+ return {
3584
+ nodes,
3585
+ edges
3586
+ };
3587
+ }
3588
+ var introspectorCache = /* @__PURE__ */ new WeakMap();
3589
+ function getSubgraphSchemaIntrospector(graph) {
3590
+ const cached = introspectorCache.get(graph);
3591
+ if (cached !== void 0) return cached;
3592
+ const nodeKinds = new Map(
3593
+ Object.entries(graph.nodes).map(([kind, definition]) => [
3594
+ kind,
3595
+ { schema: definition.type.schema }
3596
+ ])
3008
3597
  );
3009
- const targetContext = { ...ctx, cteColumnPrefix: "n" };
3010
- const targetNodePredicates = compileNodePredicates(
3011
- ast,
3012
- nodeAlias,
3013
- targetContext
3598
+ const edgeKinds = new Map(
3599
+ Object.entries(graph.edges).map(([kind, definition]) => [
3600
+ kind,
3601
+ { schema: definition.type.schema }
3602
+ ])
3014
3603
  );
3015
- if (vl.maxDepth > MAX_EXPLICIT_RECURSIVE_DEPTH) {
3016
- throw new UnsupportedPredicateError(
3017
- `Recursive traversal maxHops(${vl.maxDepth}) exceeds maximum explicit depth of ${MAX_EXPLICIT_RECURSIVE_DEPTH}`
3604
+ const introspector = createSchemaIntrospector(nodeKinds, edgeKinds);
3605
+ introspectorCache.set(graph, introspector);
3606
+ return introspector;
3607
+ }
3608
+ function buildProjectionPlan(kinds, projectionMap, resolveFieldType, entityPrefix) {
3609
+ const projectedKinds = /* @__PURE__ */ new Map();
3610
+ const fullKinds = [];
3611
+ for (const kind of kinds) {
3612
+ const selection = projectionMap?.[kind];
3613
+ if (selection === void 0) {
3614
+ fullKinds.push(kind);
3615
+ continue;
3616
+ }
3617
+ projectedKinds.set(
3618
+ kind,
3619
+ buildKindProjectionPlan(kind, selection, resolveFieldType, entityPrefix)
3018
3620
  );
3019
3621
  }
3020
- const effectiveMaxDepth = vl.maxDepth > 0 ? vl.maxDepth : MAX_RECURSIVE_DEPTH;
3021
- const maxDepthCondition = sql`r.depth < ${effectiveMaxDepth}`;
3022
- const cycleCheck = shouldEnforceCycleCheck ? dialect.cycleCheck(sql.raw("n.id"), sql.raw("r.path")) : void 0;
3023
- const initialPath = shouldTrackPath ? dialect.initializePath(sql.raw("n0.id")) : void 0;
3024
- const pathExtension = shouldTrackPath ? dialect.extendPath(sql.raw("r.path"), sql.raw("n.id")) : void 0;
3025
- const baseWhereClauses = [
3026
- sql`n0.graph_id = ${graphId}`,
3027
- startKindFilter,
3028
- startTemporalFilter,
3029
- ...startPredicates
3030
- ];
3031
- const recursiveBaseWhereClauses = [
3032
- sql`e.graph_id = ${graphId}`,
3033
- nodeKindFilter,
3034
- edgeTemporalFilter,
3035
- nodeTemporalFilter,
3036
- maxDepthCondition
3037
- ];
3038
- if (cycleCheck !== void 0) {
3039
- recursiveBaseWhereClauses.push(cycleCheck);
3040
- }
3041
- recursiveBaseWhereClauses.push(...edgePredicates, ...targetNodePredicates);
3042
- function compileRecursiveBranch2(branch) {
3043
- const recursiveFilterClauses = [
3044
- ...recursiveBaseWhereClauses,
3045
- compileKindFilter2(branch.edgeKinds, "e.kind"),
3046
- compileKindFilter2(previousNodeKinds, `e.${branch.joinKindField}`),
3047
- compileKindFilter2(nodeKinds, `e.${branch.targetKindField}`)
3048
- ];
3049
- if (branch.duplicateGuard !== void 0) {
3050
- recursiveFilterClauses.push(branch.duplicateGuard);
3051
- }
3052
- const recursiveSelectColumns = [
3053
- ...startColumnsFromRecursive,
3054
- ...nodeColumnsFromRecursive,
3055
- sql`r.depth + 1 AS depth`
3056
- ];
3057
- if (pathExtension !== void 0) {
3058
- recursiveSelectColumns.push(sql`${pathExtension} AS path`);
3059
- }
3060
- const recursiveJoinClauses = [
3061
- sql`e.${sql.raw(branch.joinField)} = r.${sql.raw(nodeAlias)}_id`
3062
- ];
3063
- if (previousNodeKinds.length > 1) {
3064
- recursiveJoinClauses.push(
3065
- sql`e.${sql.raw(branch.joinKindField)} = r.${sql.raw(nodeAlias)}_kind`
3066
- );
3622
+ return { fullKinds, projectedKinds };
3623
+ }
3624
+ function buildKindProjectionPlan(kind, selection, resolveFieldType, entityPrefix) {
3625
+ const propertyFields = /* @__PURE__ */ new Map();
3626
+ let includeMeta = false;
3627
+ for (const field2 of selection) {
3628
+ if (field2 === "meta") {
3629
+ includeMeta = true;
3630
+ continue;
3067
3631
  }
3068
- if (forceWorktableOuterJoinOrder) {
3069
- const recursiveWhereClauses = [
3070
- ...recursiveJoinClauses,
3071
- ...recursiveFilterClauses
3072
- ];
3073
- return sql`
3074
- SELECT ${sql.join(recursiveSelectColumns, sql`, `)}
3075
- FROM recursive_cte r
3076
- CROSS JOIN ${ctx.schema.edgesTable} e
3077
- JOIN ${ctx.schema.nodesTable} n ON n.graph_id = e.graph_id
3078
- AND n.id = e.${sql.raw(branch.targetField)}
3079
- AND n.kind = e.${sql.raw(branch.targetKindField)}
3080
- WHERE ${sql.join(recursiveWhereClauses, sql` AND `)}
3081
- `;
3632
+ validateProjectionField(field2, entityPrefix, kind);
3633
+ if (!propertyFields.has(field2)) {
3634
+ propertyFields.set(field2, {
3635
+ field: field2,
3636
+ outputName: projectionAlias(entityPrefix, kind, field2),
3637
+ typeInfo: resolveFieldType(kind, field2)
3638
+ });
3082
3639
  }
3083
- return sql`
3084
- SELECT ${sql.join(recursiveSelectColumns, sql`, `)}
3085
- FROM recursive_cte r
3086
- JOIN ${ctx.schema.edgesTable} e ON ${sql.join(recursiveJoinClauses, sql` AND `)}
3087
- JOIN ${ctx.schema.nodesTable} n ON n.graph_id = e.graph_id
3088
- AND n.id = e.${sql.raw(branch.targetField)}
3089
- AND n.kind = e.${sql.raw(branch.targetKindField)}
3090
- WHERE ${sql.join(recursiveFilterClauses, sql` AND `)}
3091
- `;
3092
- }
3093
- const directJoinField = direction === "out" ? "from_id" : "to_id";
3094
- const directTargetField = direction === "out" ? "to_id" : "from_id";
3095
- const directJoinKindField = direction === "out" ? "from_kind" : "to_kind";
3096
- const directTargetKindField = direction === "out" ? "to_kind" : "from_kind";
3097
- const directBranch = compileRecursiveBranch2({
3098
- joinField: directJoinField,
3099
- targetField: directTargetField,
3100
- joinKindField: directJoinKindField,
3101
- targetKindField: directTargetKindField,
3102
- edgeKinds: directEdgeKinds
3103
- });
3104
- function compileInverseRecursiveBranch() {
3105
- const inverseJoinField = direction === "out" ? "to_id" : "from_id";
3106
- const inverseTargetField = direction === "out" ? "from_id" : "to_id";
3107
- const inverseJoinKindField = direction === "out" ? "to_kind" : "from_kind";
3108
- const inverseTargetKindField = direction === "out" ? "from_kind" : "to_kind";
3109
- const overlappingKinds = inverseEdgeKinds.filter(
3110
- (kind) => directEdgeKinds.includes(kind)
3111
- );
3112
- const duplicateGuard = overlappingKinds.length > 0 ? sql`NOT (e.from_id = e.to_id AND ${compileKindFilter2(overlappingKinds, "e.kind")})` : void 0;
3113
- const inverseBranch = compileRecursiveBranch2({
3114
- joinField: inverseJoinField,
3115
- targetField: inverseTargetField,
3116
- joinKindField: inverseJoinKindField,
3117
- targetKindField: inverseTargetKindField,
3118
- edgeKinds: inverseEdgeKinds,
3119
- duplicateGuard
3120
- });
3121
- return sql`
3122
- ${directBranch}
3123
- UNION ALL
3124
- ${inverseBranch}
3125
- `;
3126
- }
3127
- const recursiveBranchSql = inverseEdgeKinds.length === 0 ? directBranch : compileInverseRecursiveBranch();
3128
- const baseSelectColumns = [
3129
- ...startColumnsFromBase,
3130
- ...nodeColumnsFromBase,
3131
- sql`0 AS depth`
3132
- ];
3133
- if (initialPath !== void 0) {
3134
- baseSelectColumns.push(sql`${initialPath} AS path`);
3135
3640
  }
3136
- return sql`
3137
- recursive_cte AS (
3138
- -- Base case: starting nodes
3139
- SELECT ${sql.join(baseSelectColumns, sql`, `)}
3140
- FROM ${ctx.schema.nodesTable} n0
3141
- WHERE ${sql.join(baseWhereClauses, sql` AND `)}
3142
-
3143
- UNION ALL
3144
-
3145
- -- Recursive case: follow edges
3146
- ${recursiveBranchSql}
3147
- )
3148
- `;
3149
- }
3150
- function compileKindFilter2(kinds, columnExpr) {
3151
- return compileKindFilter(sql.raw(columnExpr), kinds);
3641
+ return {
3642
+ includeMeta,
3643
+ propertyFields: [...propertyFields.values()]
3644
+ };
3152
3645
  }
3153
- function compileNodePredicates(ast, alias, ctx) {
3154
- return ast.predicates.filter((p) => p.targetAlias === alias && p.targetType !== "edge").map((p) => compilePredicateExpression(p.expression, ctx));
3646
+ function getIncludedNodeKinds(graph, includeKinds) {
3647
+ if (includeKinds === void 0 || includeKinds.length === 0) {
3648
+ return Object.keys(graph.nodes);
3649
+ }
3650
+ return dedupeStrings(includeKinds);
3155
3651
  }
3156
- function compileEdgePredicates(ast, edgeAlias, ctx) {
3157
- return ast.predicates.filter((p) => p.targetAlias === edgeAlias && p.targetType === "edge").map((p) => compilePredicateExpression(p.expression, ctx));
3652
+ function dedupeStrings(values) {
3653
+ return [...new Set(values)];
3158
3654
  }
3159
- function collectRequiredColumnsByAlias(ast, traversal) {
3160
- const selectiveFields = ast.selectiveFields;
3161
- if (selectiveFields === void 0 || selectiveFields.length === 0) {
3162
- return void 0;
3655
+ function buildReachableCte(ctx) {
3656
+ const shouldTrackPath = ctx.cyclePolicy === "prevent";
3657
+ const edgeKindFilter = compileKindFilter(sql.raw("e.kind"), ctx.edgeKinds);
3658
+ const initialPath = shouldTrackPath ? ctx.dialect.initializePath(sql.raw("n.id")) : void 0;
3659
+ const pathExtension = shouldTrackPath ? ctx.dialect.extendPath(sql.raw("r.path"), sql.raw("n.id")) : void 0;
3660
+ const cycleCheck = shouldTrackPath ? ctx.dialect.cycleCheck(sql.raw("n.id"), sql.raw("r.path")) : void 0;
3661
+ const baseColumns = [sql`n.id`, sql`n.kind`, sql`0 AS depth`];
3662
+ if (initialPath !== void 0) {
3663
+ baseColumns.push(sql`${initialPath} AS path`);
3163
3664
  }
3164
- const requiredColumnsByAlias = /* @__PURE__ */ new Map();
3165
- const previousNodeKinds = [
3166
- .../* @__PURE__ */ new Set([...ast.start.kinds, ...traversal.nodeKinds])
3665
+ const baseCase = sql`SELECT ${sql.join(baseColumns, sql`, `)} FROM ${ctx.schema.nodesTable} n WHERE n.graph_id = ${ctx.graphId} AND n.id = ${ctx.rootId} AND n.deleted_at IS NULL`;
3666
+ const recursiveColumns = [
3667
+ sql`n.id`,
3668
+ sql`n.kind`,
3669
+ sql`r.depth + 1 AS depth`
3167
3670
  ];
3168
- addRequiredColumn(requiredColumnsByAlias, traversal.nodeAlias, "id");
3169
- if (previousNodeKinds.length > 1) {
3170
- addRequiredColumn(requiredColumnsByAlias, traversal.nodeAlias, "kind");
3671
+ if (pathExtension !== void 0) {
3672
+ recursiveColumns.push(sql`${pathExtension} AS path`);
3171
3673
  }
3172
- for (const field2 of selectiveFields) {
3173
- markSelectiveFieldAsRequired(requiredColumnsByAlias, field2);
3674
+ const recursiveWhereClauses = [
3675
+ sql`e.graph_id = ${ctx.graphId}`,
3676
+ edgeKindFilter,
3677
+ sql`e.deleted_at IS NULL`,
3678
+ sql`n.deleted_at IS NULL`,
3679
+ sql`r.depth < ${ctx.maxDepth}`
3680
+ ];
3681
+ if (cycleCheck !== void 0) {
3682
+ recursiveWhereClauses.push(cycleCheck);
3683
+ }
3684
+ const forceWorktableOuterJoinOrder = ctx.dialect.capabilities.forceRecursiveWorktableOuterJoinOrder;
3685
+ const recursiveCase = ctx.direction === "both" ? compileBidirectionalBranch({
3686
+ recursiveColumns,
3687
+ whereClauses: recursiveWhereClauses,
3688
+ forceWorktableOuterJoinOrder,
3689
+ schema: ctx.schema
3690
+ }) : compileRecursiveBranch({
3691
+ recursiveColumns,
3692
+ whereClauses: recursiveWhereClauses,
3693
+ joinField: "from_id",
3694
+ targetField: "to_id",
3695
+ targetKindField: "to_kind",
3696
+ forceWorktableOuterJoinOrder,
3697
+ schema: ctx.schema
3698
+ });
3699
+ return sql`WITH RECURSIVE reachable AS (${baseCase} UNION ALL ${recursiveCase})`;
3700
+ }
3701
+ function compileRecursiveBranch(params) {
3702
+ const columns = [...params.recursiveColumns];
3703
+ const selectClause = sql`SELECT ${sql.join(columns, sql`, `)}`;
3704
+ const nodeJoin = sql`JOIN ${params.schema.nodesTable} n ON n.graph_id = e.graph_id AND n.id = e.${sql.raw(params.targetField)} AND n.kind = e.${sql.raw(params.targetKindField)}`;
3705
+ if (params.forceWorktableOuterJoinOrder) {
3706
+ const allWhere = [
3707
+ ...params.whereClauses,
3708
+ sql`e.${sql.raw(params.joinField)} = r.id`
3709
+ ];
3710
+ return sql`${selectClause} FROM reachable r CROSS JOIN ${params.schema.edgesTable} e ${nodeJoin} WHERE ${sql.join(allWhere, sql` AND `)}`;
3711
+ }
3712
+ const where = [...params.whereClauses];
3713
+ return sql`${selectClause} FROM reachable r JOIN ${params.schema.edgesTable} e ON e.${sql.raw(params.joinField)} = r.id ${nodeJoin} WHERE ${sql.join(where, sql` AND `)}`;
3714
+ }
3715
+ function compileBidirectionalBranch(params) {
3716
+ const columns = [...params.recursiveColumns];
3717
+ const selectClause = sql`SELECT ${sql.join(columns, sql`, `)}`;
3718
+ const nodeJoin = sql`JOIN ${params.schema.nodesTable} n ON n.graph_id = e.graph_id AND ((e.to_id = r.id AND n.id = e.from_id AND n.kind = e.from_kind) OR (e.from_id = r.id AND n.id = e.to_id AND n.kind = e.to_kind))`;
3719
+ if (params.forceWorktableOuterJoinOrder) {
3720
+ const allWhere = [
3721
+ ...params.whereClauses,
3722
+ sql`(e.from_id = r.id OR e.to_id = r.id)`
3723
+ ];
3724
+ return sql`${selectClause} FROM reachable r CROSS JOIN ${params.schema.edgesTable} e ${nodeJoin} WHERE ${sql.join(allWhere, sql` AND `)}`;
3725
+ }
3726
+ return sql`${selectClause} FROM reachable r JOIN ${params.schema.edgesTable} e ON (e.from_id = r.id OR e.to_id = r.id) ${nodeJoin} WHERE ${sql.join([...params.whereClauses], sql` AND `)}`;
3727
+ }
3728
+ function buildIncludedIdsCte(ctx) {
3729
+ const filters = [];
3730
+ if (ctx.includeKinds !== void 0 && ctx.includeKinds.length > 0) {
3731
+ filters.push(compileKindFilter(sql.raw("kind"), ctx.includeKinds));
3174
3732
  }
3175
- if (ast.orderBy) {
3176
- for (const orderSpec of ast.orderBy) {
3177
- markFieldRefAsRequired(requiredColumnsByAlias, orderSpec.field);
3178
- }
3733
+ if (ctx.excludeRoot) {
3734
+ filters.push(sql`id != ${ctx.rootId}`);
3179
3735
  }
3180
- return requiredColumnsByAlias;
3736
+ const whereClause = filters.length > 0 ? sql` WHERE ${sql.join(filters, sql` AND `)}` : sql``;
3737
+ return sql`, included_ids AS (SELECT DISTINCT id FROM reachable${whereClause})`;
3181
3738
  }
3182
- function compileNodeSelectColumnsFromTable(tableAlias, alias, requiredColumns, alwaysRequiredColumns) {
3183
- return NODE_COLUMNS.filter(
3184
- (column) => shouldProjectColumn(requiredColumns, column, alwaysRequiredColumns)
3185
- ).map(
3186
- (column) => sql`${sql.raw(tableAlias)}.${sql.raw(column)} AS ${sql.raw(`${alias}_${column}`)}`
3187
- );
3739
+ async function fetchSubgraphNodes(ctx, reachableCte, includedIdsCte, projectionPlan) {
3740
+ const columns = [
3741
+ sql`n.kind`,
3742
+ sql`n.id`,
3743
+ buildFullPropsColumn("n", projectionPlan),
3744
+ ...buildMetadataColumns("n", projectionPlan, [
3745
+ "version",
3746
+ "valid_from",
3747
+ "valid_to",
3748
+ "created_at",
3749
+ "updated_at",
3750
+ "deleted_at"
3751
+ ]),
3752
+ ...buildProjectedPropertyColumns("n", projectionPlan, ctx.dialect)
3753
+ ];
3754
+ const query = sql`${reachableCte}${includedIdsCte} SELECT ${sql.join(columns, sql`, `)} FROM ${ctx.schema.nodesTable} n WHERE n.graph_id = ${ctx.graphId} AND n.id IN (SELECT id FROM included_ids)`;
3755
+ return ctx.backend.execute(query);
3188
3756
  }
3189
- function compileNodeSelectColumnsFromRecursiveRow(alias, requiredColumns, alwaysRequiredColumns) {
3190
- return NODE_COLUMNS.filter(
3191
- (column) => shouldProjectColumn(requiredColumns, column, alwaysRequiredColumns)
3192
- ).map((column) => {
3193
- const projected = `${alias}_${column}`;
3194
- return sql`r.${sql.raw(projected)} AS ${sql.raw(projected)}`;
3195
- });
3757
+ async function fetchSubgraphEdges(ctx, reachableCte, includedIdsCte, projectionPlan) {
3758
+ const edgeKindFilter = compileKindFilter(sql.raw("e.kind"), ctx.edgeKinds);
3759
+ const columns = [
3760
+ sql`e.id`,
3761
+ sql`e.kind`,
3762
+ sql`e.from_kind`,
3763
+ sql`e.from_id`,
3764
+ sql`e.to_kind`,
3765
+ sql`e.to_id`,
3766
+ buildFullPropsColumn("e", projectionPlan),
3767
+ ...buildMetadataColumns("e", projectionPlan, [
3768
+ "valid_from",
3769
+ "valid_to",
3770
+ "created_at",
3771
+ "updated_at",
3772
+ "deleted_at"
3773
+ ]),
3774
+ ...buildProjectedPropertyColumns("e", projectionPlan, ctx.dialect)
3775
+ ];
3776
+ const query = sql`${reachableCte}${includedIdsCte} SELECT ${sql.join(columns, sql`, `)} FROM ${ctx.schema.edgesTable} e WHERE e.graph_id = ${ctx.graphId} AND ${edgeKindFilter} AND e.deleted_at IS NULL AND e.from_id IN (SELECT id FROM included_ids) AND e.to_id IN (SELECT id FROM included_ids)`;
3777
+ return ctx.backend.execute(query);
3196
3778
  }
3197
- function compileRecursiveProjection(ast, traversal, dialect) {
3198
- if (ast.selectiveFields && ast.selectiveFields.length > 0) {
3199
- return compileRecursiveSelectiveProjection(
3200
- ast.selectiveFields,
3201
- ast,
3202
- traversal,
3203
- dialect
3204
- );
3779
+ function buildMetadataColumns(alias, plan, columns) {
3780
+ if (plan.projectedKinds.size === 0) {
3781
+ return columns.map((col) => sql`${sql.raw(`${alias}.${col}`)}`);
3205
3782
  }
3206
- const startAlias = ast.start.alias;
3207
- const nodeAlias = traversal.nodeAlias;
3208
- const vl = traversal.variableLength;
3209
- const fields = [
3210
- // Start alias fields with metadata
3211
- sql`${sql.raw(startAlias)}_id`,
3212
- sql`${sql.raw(startAlias)}_kind`,
3213
- sql`${sql.raw(startAlias)}_props`,
3214
- sql`${sql.raw(startAlias)}_version`,
3215
- sql`${sql.raw(startAlias)}_valid_from`,
3216
- sql`${sql.raw(startAlias)}_valid_to`,
3217
- sql`${sql.raw(startAlias)}_created_at`,
3218
- sql`${sql.raw(startAlias)}_updated_at`,
3219
- sql`${sql.raw(startAlias)}_deleted_at`,
3220
- // Node alias fields with metadata
3221
- sql`${sql.raw(nodeAlias)}_id`,
3222
- sql`${sql.raw(nodeAlias)}_kind`,
3223
- sql`${sql.raw(nodeAlias)}_props`,
3224
- sql`${sql.raw(nodeAlias)}_version`,
3225
- sql`${sql.raw(nodeAlias)}_valid_from`,
3226
- sql`${sql.raw(nodeAlias)}_valid_to`,
3227
- sql`${sql.raw(nodeAlias)}_created_at`,
3228
- sql`${sql.raw(nodeAlias)}_updated_at`,
3229
- sql`${sql.raw(nodeAlias)}_deleted_at`
3230
- ];
3231
- if (vl.depthAlias !== void 0) {
3232
- fields.push(sql`depth AS ${quoteIdentifier(vl.depthAlias)}`);
3783
+ const metaKinds = [...plan.fullKinds];
3784
+ for (const [kind, kindPlan] of plan.projectedKinds) {
3785
+ if (kindPlan.includeMeta) metaKinds.push(kind);
3233
3786
  }
3234
- if (vl.pathAlias !== void 0) {
3235
- fields.push(sql`path AS ${quoteIdentifier(vl.pathAlias)}`);
3787
+ if (metaKinds.length === 0) {
3788
+ return columns.map((col) => sql`NULL AS ${sql.raw(col)}`);
3236
3789
  }
3237
- return sql.join(fields, sql`, `);
3790
+ if (metaKinds.length === plan.fullKinds.length + plan.projectedKinds.size) {
3791
+ return columns.map((col) => sql`${sql.raw(`${alias}.${col}`)}`);
3792
+ }
3793
+ const filter = compileKindFilter(sql.raw(`${alias}.kind`), metaKinds);
3794
+ return columns.map(
3795
+ (col) => sql`CASE WHEN ${filter} THEN ${sql.raw(`${alias}.${col}`)} ELSE NULL END AS ${sql.raw(col)}`
3796
+ );
3238
3797
  }
3239
- function compileRecursiveSelectiveProjection(fields, ast, traversal, dialect) {
3240
- const allowedAliases = /* @__PURE__ */ new Set([ast.start.alias, traversal.nodeAlias]);
3241
- const columns = fields.map((field2) => {
3242
- if (!allowedAliases.has(field2.alias)) {
3243
- throw new UnsupportedPredicateError(
3244
- `Selective projection for recursive traversals does not support alias "${field2.alias}"`
3245
- );
3246
- }
3247
- if (field2.isSystemField) {
3248
- const dbColumn = mapSelectiveSystemFieldToColumn(field2.field);
3249
- return sql`${sql.raw(`${field2.alias}_${dbColumn}`)} AS ${quoteIdentifier(field2.outputName)}`;
3250
- }
3251
- const column = sql.raw(`${field2.alias}_props`);
3252
- const extracted = compileTypedJsonExtract({
3253
- column,
3254
- dialect,
3255
- pointer: jsonPointer([field2.field]),
3256
- valueType: field2.valueType
3257
- });
3258
- return sql`${extracted} AS ${quoteIdentifier(field2.outputName)}`;
3259
- });
3260
- const vl = traversal.variableLength;
3261
- if (vl.depthAlias !== void 0) {
3262
- columns.push(sql`depth AS ${quoteIdentifier(vl.depthAlias)}`);
3798
+ function buildFullPropsColumn(alias, plan) {
3799
+ if (plan.projectedKinds.size === 0) {
3800
+ return sql`${sql.raw(`${alias}.props`)} AS props`;
3263
3801
  }
3264
- if (vl.pathAlias !== void 0) {
3265
- columns.push(sql`path AS ${quoteIdentifier(vl.pathAlias)}`);
3802
+ if (plan.fullKinds.length === 0) {
3803
+ return sql`NULL AS props`;
3266
3804
  }
3267
- return sql.join(columns, sql`, `);
3805
+ const filter = compileKindFilter(sql.raw(`${alias}.kind`), plan.fullKinds);
3806
+ return sql`CASE WHEN ${filter} THEN ${sql.raw(`${alias}.props`)} ELSE NULL END AS props`;
3268
3807
  }
3269
- function compileRecursiveOrderBy(ast, dialect) {
3270
- if (!ast.orderBy || ast.orderBy.length === 0) {
3271
- return void 0;
3272
- }
3273
- const parts = [];
3274
- for (const orderSpec of ast.orderBy) {
3275
- const valueType = orderSpec.field.valueType;
3276
- if (valueType === "array" || valueType === "object") {
3277
- throw new UnsupportedPredicateError(
3278
- "Ordering by JSON arrays or objects is not supported"
3808
+ function buildProjectedPropertyColumns(alias, plan, dialect) {
3809
+ const columns = [];
3810
+ for (const [kind, kindPlan] of plan.projectedKinds.entries()) {
3811
+ for (const fieldPlan of kindPlan.propertyFields) {
3812
+ const extracted = compileTypedJsonExtract({
3813
+ column: sql.raw(`${alias}.props`),
3814
+ dialect,
3815
+ pointer: jsonPointer([fieldPlan.field]),
3816
+ valueType: fieldPlan.typeInfo?.valueType
3817
+ });
3818
+ columns.push(
3819
+ sql`CASE WHEN ${sql.raw(alias)}.kind = ${kind} THEN ${extracted} ELSE NULL END AS ${quoteIdentifier(fieldPlan.outputName)}`
3279
3820
  );
3280
3821
  }
3281
- const field2 = compileFieldValue(orderSpec.field, dialect, valueType);
3282
- const direction = sql.raw(orderSpec.direction.toUpperCase());
3283
- const nulls = orderSpec.nulls ?? (orderSpec.direction === "asc" ? "last" : "first");
3284
- const nullsDirection = sql.raw(nulls === "first" ? "DESC" : "ASC");
3285
- parts.push(
3286
- sql`(${field2} IS NULL) ${nullsDirection}`,
3287
- sql`${field2} ${direction}`
3288
- );
3289
3822
  }
3290
- return sql`ORDER BY ${sql.join(parts, sql`, `)}`;
3823
+ return columns;
3291
3824
  }
3292
- function compileLimitOffset(ast) {
3293
- const parts = [];
3294
- if (ast.limit !== void 0) {
3295
- parts.push(sql`LIMIT ${ast.limit}`);
3296
- }
3297
- if (ast.offset !== void 0) {
3298
- parts.push(sql`OFFSET ${ast.offset}`);
3825
+ function applyProjectedFields(target, row, kindPlan) {
3826
+ for (const fieldPlan of kindPlan.propertyFields) {
3827
+ target[fieldPlan.field] = decodeSelectedValue(
3828
+ row[fieldPlan.outputName],
3829
+ fieldPlan.typeInfo
3830
+ );
3299
3831
  }
3300
- return parts.length > 0 ? sql.join(parts, sql` `) : void 0;
3301
3832
  }
3302
- var DEFAULT_TABLE_NAMES = {
3303
- nodes: "typegraph_nodes",
3304
- edges: "typegraph_edges",
3305
- embeddings: "typegraph_node_embeddings"
3306
- };
3307
- var MAX_IDENTIFIER_LENGTH = 63;
3308
- var VALID_IDENTIFIER_PATTERN = /^[a-z_][a-z0-9_$]*$/i;
3309
- function validateTableName(name, label) {
3310
- if (!name || name.length === 0) {
3311
- throw new ConfigurationError(`${label} table name cannot be empty`);
3312
- }
3313
- if (name.length > MAX_IDENTIFIER_LENGTH) {
3314
- throw new ConfigurationError(
3315
- `${label} table name exceeds maximum length of ${MAX_IDENTIFIER_LENGTH} characters`
3316
- );
3833
+ function mapSubgraphNodeRow(row, projectionPlan) {
3834
+ const kindPlan = projectionPlan.projectedKinds.get(row.kind);
3835
+ if (kindPlan === void 0) {
3836
+ return rowToNode({
3837
+ ...row,
3838
+ props: normalizeProps(row.props)
3839
+ });
3317
3840
  }
3318
- if (!VALID_IDENTIFIER_PATTERN.test(name)) {
3319
- throw new ConfigurationError(
3320
- `${label} table name "${name}" is not a valid SQL identifier. Table names must start with a letter or underscore and contain only letters, digits, underscores, or dollar signs.`
3321
- );
3841
+ const projectedNode = {
3842
+ kind: row.kind,
3843
+ id: row.id
3844
+ };
3845
+ if (kindPlan.includeMeta) {
3846
+ projectedNode.meta = rowToNodeMeta(row);
3847
+ }
3848
+ applyProjectedFields(projectedNode, row, kindPlan);
3849
+ return projectedNode;
3850
+ }
3851
+ function mapSubgraphEdgeRow(row, projectionPlan) {
3852
+ const kindPlan = projectionPlan.projectedKinds.get(row.kind);
3853
+ if (kindPlan === void 0) {
3854
+ return rowToEdge({
3855
+ ...row,
3856
+ props: normalizeProps(row.props)
3857
+ });
3322
3858
  }
3323
- }
3324
- function quoteIdentifier2(name) {
3325
- return `"${name.replaceAll('"', '""')}"`;
3326
- }
3327
- function createSqlSchema(names = {}) {
3328
- const tables = { ...DEFAULT_TABLE_NAMES, ...names };
3329
- validateTableName(tables.nodes, "nodes");
3330
- validateTableName(tables.edges, "edges");
3331
- validateTableName(tables.embeddings, "embeddings");
3332
- return {
3333
- tables,
3334
- nodesTable: sql.raw(quoteIdentifier2(tables.nodes)),
3335
- edgesTable: sql.raw(quoteIdentifier2(tables.edges)),
3336
- embeddingsTable: sql.raw(quoteIdentifier2(tables.embeddings))
3859
+ const projectedEdge = {
3860
+ id: row.id,
3861
+ kind: row.kind,
3862
+ fromKind: row.from_kind,
3863
+ fromId: row.from_id,
3864
+ toKind: row.to_kind,
3865
+ toId: row.to_id
3337
3866
  };
3867
+ if (kindPlan.includeMeta) {
3868
+ projectedEdge.meta = rowToEdgeMeta(row);
3869
+ }
3870
+ applyProjectedFields(projectedEdge, row, kindPlan);
3871
+ return projectedEdge;
3338
3872
  }
3339
- var DEFAULT_SQL_SCHEMA = createSqlSchema();
3340
3873
  var OPERATOR_MAP = {
3341
3874
  union: "UNION",
3342
3875
  unionAll: "UNION ALL",
@@ -4705,9 +5238,9 @@ function transformPathColumns(rows, state, _dialect) {
4705
5238
  }
4706
5239
  return changed ? result : rows;
4707
5240
  }
4708
- var RESERVED_NODE_KEYS2 = /* @__PURE__ */ new Set(["id", "kind", "meta"]);
4709
- var RESERVED_EDGE_KEYS2 = /* @__PURE__ */ new Set(["id", "kind", "fromId", "toId", "meta"]);
4710
- function nullToUndefined(value) {
5241
+ var RESERVED_NODE_KEYS3 = /* @__PURE__ */ new Set(["id", "kind", "meta"]);
5242
+ var RESERVED_EDGE_KEYS3 = /* @__PURE__ */ new Set(["id", "kind", "fromId", "toId", "meta"]);
5243
+ function nullToUndefined3(value) {
4711
5244
  return value === null ? void 0 : value;
4712
5245
  }
4713
5246
  function assignPropsExcludingReserved(target, props, reservedKeys) {
@@ -4723,13 +5256,13 @@ function buildSelectableNode(row, alias) {
4723
5256
  const propsRaw = row[`${alias}_props`];
4724
5257
  const rawProps = typeof propsRaw === "string" ? JSON.parse(propsRaw) : propsRaw ?? {};
4725
5258
  const version = row[`${alias}_version`];
4726
- const validFrom = nullToUndefined(
5259
+ const validFrom = nullToUndefined3(
4727
5260
  row[`${alias}_valid_from`]
4728
5261
  );
4729
- const validTo = nullToUndefined(row[`${alias}_valid_to`]);
5262
+ const validTo = nullToUndefined3(row[`${alias}_valid_to`]);
4730
5263
  const createdAt = row[`${alias}_created_at`];
4731
5264
  const updatedAt = row[`${alias}_updated_at`];
4732
- const deletedAt = nullToUndefined(
5265
+ const deletedAt = nullToUndefined3(
4733
5266
  row[`${alias}_deleted_at`]
4734
5267
  );
4735
5268
  const result = {
@@ -4744,7 +5277,7 @@ function buildSelectableNode(row, alias) {
4744
5277
  deletedAt
4745
5278
  }
4746
5279
  };
4747
- assignPropsExcludingReserved(result, rawProps, RESERVED_NODE_KEYS2);
5280
+ assignPropsExcludingReserved(result, rawProps, RESERVED_NODE_KEYS3);
4748
5281
  return result;
4749
5282
  }
4750
5283
  function buildSelectableNodeOrUndefined(row, alias) {
@@ -4764,13 +5297,13 @@ function buildSelectableEdge(row, alias) {
4764
5297
  const toId = row[`${alias}_to_id`];
4765
5298
  const propsRaw = row[`${alias}_props`];
4766
5299
  const rawProps = typeof propsRaw === "string" ? JSON.parse(propsRaw) : propsRaw ?? {};
4767
- const validFrom = nullToUndefined(
5300
+ const validFrom = nullToUndefined3(
4768
5301
  row[`${alias}_valid_from`]
4769
5302
  );
4770
- const validTo = nullToUndefined(row[`${alias}_valid_to`]);
5303
+ const validTo = nullToUndefined3(row[`${alias}_valid_to`]);
4771
5304
  const createdAt = row[`${alias}_created_at`];
4772
5305
  const updatedAt = row[`${alias}_updated_at`];
4773
- const deletedAt = nullToUndefined(
5306
+ const deletedAt = nullToUndefined3(
4774
5307
  row[`${alias}_deleted_at`]
4775
5308
  );
4776
5309
  const result = {
@@ -4786,7 +5319,7 @@ function buildSelectableEdge(row, alias) {
4786
5319
  deletedAt
4787
5320
  }
4788
5321
  };
4789
- assignPropsExcludingReserved(result, rawProps, RESERVED_EDGE_KEYS2);
5322
+ assignPropsExcludingReserved(result, rawProps, RESERVED_EDGE_KEYS3);
4790
5323
  return result;
4791
5324
  }
4792
5325
  function buildSelectContext(row, startAlias, traversals) {
@@ -5172,63 +5705,6 @@ function getPlaceholderForValueType(valueType, mode) {
5172
5705
  }
5173
5706
  }
5174
5707
 
5175
- // src/query/execution/value-decoder.ts
5176
- function nullToUndefined2(value) {
5177
- return value === null ? void 0 : value;
5178
- }
5179
- function decodeSelectedValue(value, typeInfo) {
5180
- const normalized = nullToUndefined2(value);
5181
- if (normalized === void 0) return void 0;
5182
- if (typeInfo === void 0) {
5183
- return normalized;
5184
- }
5185
- return decodeByValueType(normalized, typeInfo.valueType);
5186
- }
5187
- function decodeByValueType(value, valueType) {
5188
- switch (valueType) {
5189
- case "boolean": {
5190
- if (typeof value === "boolean") return value;
5191
- if (typeof value === "number") return value !== 0;
5192
- if (typeof value === "string") {
5193
- if (value === "0") return false;
5194
- if (value === "1") return true;
5195
- if (value.toLowerCase() === "true") return true;
5196
- if (value.toLowerCase() === "false") return false;
5197
- }
5198
- return Boolean(value);
5199
- }
5200
- case "number": {
5201
- if (typeof value === "number") return value;
5202
- if (typeof value === "string") {
5203
- const parsed = Number(value);
5204
- return Number.isNaN(parsed) ? value : parsed;
5205
- }
5206
- return value;
5207
- }
5208
- case "array":
5209
- case "object":
5210
- case "embedding": {
5211
- if (typeof value !== "string") return value;
5212
- const trimmed = value.trim();
5213
- const looksJson = trimmed.startsWith("[") || trimmed.startsWith("{");
5214
- if (!looksJson) return value;
5215
- try {
5216
- return JSON.parse(trimmed);
5217
- } catch {
5218
- return value;
5219
- }
5220
- }
5221
- case "string":
5222
- case "date":
5223
- case "unknown": {
5224
- return value;
5225
- }
5226
- default: {
5227
- return value;
5228
- }
5229
- }
5230
- }
5231
-
5232
5708
  // src/query/execution/selective-result-mapper.ts
5233
5709
  var MissingSelectiveFieldError = class extends Error {
5234
5710
  alias;
@@ -5397,12 +5873,12 @@ function buildRequiredAliasValue(row, plan) {
5397
5873
  }
5398
5874
  };
5399
5875
  for (const field2 of plan.systemFields) {
5400
- base[field2.field] = nullToUndefined2(row[field2.outputName]);
5876
+ base[field2.field] = nullToUndefined(row[field2.outputName]);
5401
5877
  }
5402
5878
  if (plan.metaFields.length > 0) {
5403
5879
  const meta = {};
5404
5880
  for (const field2 of plan.metaFields) {
5405
- meta[field2.metaKey] = nullToUndefined2(row[field2.outputName]);
5881
+ meta[field2.metaKey] = nullToUndefined(row[field2.outputName]);
5406
5882
  }
5407
5883
  base.meta = createGuardedProxy(meta, `${plan.alias}.meta`);
5408
5884
  }
@@ -6370,7 +6846,35 @@ var ExecutableQuery = class _ExecutableQuery {
6370
6846
  return optimizedResult;
6371
6847
  }
6372
6848
  const compiled = this.compile();
6373
- const rawRows = await this.#config.backend.execute(compiled);
6849
+ const rawRows = await this.#config.backend.execute(compiled);
6850
+ this.#config.dialect ?? "sqlite";
6851
+ const rows = transformPathColumns(rawRows, this.#state);
6852
+ return mapResults(
6853
+ rows,
6854
+ this.#state.startAlias,
6855
+ this.#state.traversals,
6856
+ this.#selectFn
6857
+ );
6858
+ }
6859
+ /**
6860
+ * Executes the query against a provided backend.
6861
+ *
6862
+ * Used by `store.batch()` to run multiple queries over a single connection
6863
+ * (e.g., within a transaction). The full compile → execute → transform
6864
+ * pipeline runs identically to `execute()`, but against the given backend.
6865
+ */
6866
+ async executeOn(backend) {
6867
+ if (hasParameterReferences(this.toAst())) {
6868
+ throw new Error(
6869
+ "Query contains param() references. Use .prepare().execute({...}) instead of .execute()."
6870
+ );
6871
+ }
6872
+ const optimizedResult = await this.#tryOptimizedExecutionOn(backend);
6873
+ if (optimizedResult !== void 0) {
6874
+ return optimizedResult;
6875
+ }
6876
+ const compiled = this.compile();
6877
+ const rawRows = await backend.execute(compiled);
6374
6878
  this.#config.dialect ?? "sqlite";
6375
6879
  const rows = transformPathColumns(rawRows, this.#state);
6376
6880
  return mapResults(
@@ -6432,6 +6936,56 @@ var ExecutableQuery = class _ExecutableQuery {
6432
6936
  throw error;
6433
6937
  }
6434
6938
  }
6939
+ /**
6940
+ * Attempts optimized execution against a provided backend.
6941
+ * Mirror of #tryOptimizedExecution but delegates to the given backend.
6942
+ */
6943
+ async #tryOptimizedExecutionOn(backend) {
6944
+ const selectiveFields = this.#getSelectiveFieldsForExecute();
6945
+ if (selectiveFields === void 0) {
6946
+ return void 0;
6947
+ }
6948
+ let compiled;
6949
+ if (this.#cachedOptimizedCompiled === NOT_COMPUTED) {
6950
+ const baseAst = buildQueryAst(this.#config, this.#state);
6951
+ const selectiveAst = {
6952
+ ...baseAst,
6953
+ selectiveFields
6954
+ };
6955
+ compiled = compileQuery(
6956
+ selectiveAst,
6957
+ this.#config.graphId,
6958
+ this.#compileOptions()
6959
+ );
6960
+ this.#cachedOptimizedCompiled = compiled;
6961
+ } else {
6962
+ compiled = this.#cachedOptimizedCompiled;
6963
+ }
6964
+ const rawSelectiveRows = await backend.execute(compiled);
6965
+ this.#config.dialect ?? "sqlite";
6966
+ const rows = transformPathColumns(rawSelectiveRows, this.#state);
6967
+ try {
6968
+ return mapSelectiveResults(
6969
+ rows,
6970
+ this.#state,
6971
+ selectiveFields,
6972
+ this.#config.schemaIntrospector,
6973
+ this.#selectFn
6974
+ );
6975
+ } catch (error) {
6976
+ if (error instanceof MissingSelectiveFieldError) {
6977
+ this.#cachedSelectiveFieldsForExecute = void 0;
6978
+ this.#cachedOptimizedCompiled = NOT_COMPUTED;
6979
+ return void 0;
6980
+ }
6981
+ if (error instanceof UnsupportedPredicateError) {
6982
+ this.#cachedSelectiveFieldsForExecute = void 0;
6983
+ this.#cachedOptimizedCompiled = NOT_COMPUTED;
6984
+ return void 0;
6985
+ }
6986
+ throw error;
6987
+ }
6988
+ }
6435
6989
  #trackSelectFunctionAccesses(tracker) {
6436
6990
  const hasOptionalTraversal = this.#state.traversals.some(
6437
6991
  (traversal) => traversal.optional
@@ -6682,7 +7236,7 @@ var ExecutableQuery = class _ExecutableQuery {
6682
7236
  throw new MissingSelectiveFieldError(alias, fieldName);
6683
7237
  }
6684
7238
  const aliasObject2 = this.#getOrCreateAliasObject(cursorContext, alias);
6685
- aliasObject2[fieldName] = nullToUndefined2(row[outputName2]);
7239
+ aliasObject2[fieldName] = nullToUndefined(row[outputName2]);
6686
7240
  continue;
6687
7241
  }
6688
7242
  const segments = parseJsonPointer(jsonPointer2);
@@ -8065,6 +8619,29 @@ var UnionableQuery = class _UnionableQuery {
8065
8619
  }
8066
8620
  return rows;
8067
8621
  }
8622
+ /**
8623
+ * Executes the combined query against a provided backend.
8624
+ *
8625
+ * Used by `store.batch()` to run multiple queries over a single connection.
8626
+ */
8627
+ async executeOn(backend) {
8628
+ if (composableQueryHasParameterReferences(this.toAst())) {
8629
+ throw new Error(
8630
+ "Query contains param() references. Use .prepare().execute({...}) instead of .execute()."
8631
+ );
8632
+ }
8633
+ const compiled = this.compile();
8634
+ const rows = await backend.execute(compiled);
8635
+ if (this.#state.selectFn && this.#state.startAlias) {
8636
+ return mapResults(
8637
+ rows,
8638
+ this.#state.startAlias,
8639
+ this.#state.traversals ?? [],
8640
+ this.#state.selectFn
8641
+ );
8642
+ }
8643
+ return rows;
8644
+ }
8068
8645
  };
8069
8646
 
8070
8647
  // src/query/builder/aggregates.ts
@@ -9285,67 +9862,6 @@ async function checkCardinalityConstraint(ctx, edgeKind, cardinality, fromKind,
9285
9862
  }
9286
9863
  }
9287
9864
 
9288
- // src/store/row-mappers.ts
9289
- var RESERVED_NODE_KEYS3 = /* @__PURE__ */ new Set(["id", "kind", "meta"]);
9290
- function nullToUndefined3(value) {
9291
- return value === null ? void 0 : value;
9292
- }
9293
- function filterReservedKeys(props, reservedKeys) {
9294
- const filtered = {};
9295
- for (const [key, value] of Object.entries(props)) {
9296
- if (!reservedKeys.has(key)) {
9297
- filtered[key] = value;
9298
- }
9299
- }
9300
- return filtered;
9301
- }
9302
- function rowToNode(row) {
9303
- const rawProps = JSON.parse(row.props);
9304
- const props = filterReservedKeys(rawProps, RESERVED_NODE_KEYS3);
9305
- return {
9306
- kind: row.kind,
9307
- id: row.id,
9308
- meta: {
9309
- version: row.version,
9310
- validFrom: nullToUndefined3(row.valid_from),
9311
- validTo: nullToUndefined3(row.valid_to),
9312
- createdAt: row.created_at,
9313
- updatedAt: row.updated_at,
9314
- deletedAt: nullToUndefined3(row.deleted_at)
9315
- },
9316
- ...props
9317
- };
9318
- }
9319
- var RESERVED_EDGE_KEYS3 = /* @__PURE__ */ new Set([
9320
- "id",
9321
- "kind",
9322
- "meta",
9323
- "fromKind",
9324
- "fromId",
9325
- "toKind",
9326
- "toId"
9327
- ]);
9328
- function rowToEdge(row) {
9329
- const rawProps = JSON.parse(row.props);
9330
- const props = filterReservedKeys(rawProps, RESERVED_EDGE_KEYS3);
9331
- return {
9332
- id: row.id,
9333
- kind: row.kind,
9334
- fromKind: row.from_kind,
9335
- fromId: row.from_id,
9336
- toKind: row.to_kind,
9337
- toId: row.to_id,
9338
- meta: {
9339
- validFrom: nullToUndefined3(row.valid_from),
9340
- validTo: nullToUndefined3(row.valid_to),
9341
- createdAt: row.created_at,
9342
- updatedAt: row.updated_at,
9343
- deletedAt: nullToUndefined3(row.deleted_at)
9344
- },
9345
- ...props
9346
- };
9347
- }
9348
-
9349
9865
  // src/store/operations/edge-operations.ts
9350
9866
  function getEdgeRegistration(graph, kind) {
9351
9867
  const registration = graph.edges[kind];
@@ -11219,142 +11735,6 @@ async function executeNodeBulkGetOrCreateByConstraint(ctx, kind, constraintName,
11219
11735
  }
11220
11736
  return results;
11221
11737
  }
11222
- var DEFAULT_SUBGRAPH_MAX_DEPTH = 10;
11223
- function normalizeProps(value) {
11224
- return typeof value === "string" ? value : JSON.stringify(value ?? {});
11225
- }
11226
- async function executeSubgraph(params) {
11227
- const { options } = params;
11228
- if (options.edges.length === 0) {
11229
- return { nodes: [], edges: [] };
11230
- }
11231
- const maxDepth = Math.min(
11232
- options.maxDepth ?? DEFAULT_SUBGRAPH_MAX_DEPTH,
11233
- MAX_RECURSIVE_DEPTH
11234
- );
11235
- const ctx = {
11236
- graphId: params.graphId,
11237
- rootId: params.rootId,
11238
- edgeKinds: options.edges,
11239
- maxDepth,
11240
- includeKinds: options.includeKinds,
11241
- excludeRoot: options.excludeRoot ?? false,
11242
- direction: options.direction ?? "out",
11243
- cyclePolicy: options.cyclePolicy ?? "prevent",
11244
- dialect: params.dialect,
11245
- schema: params.schema ?? DEFAULT_SQL_SCHEMA,
11246
- backend: params.backend
11247
- };
11248
- const reachableCte = buildReachableCte(ctx);
11249
- const includedIdsCte = buildIncludedIdsCte(ctx);
11250
- const [nodeRows, edgeRows] = await Promise.all([
11251
- fetchSubgraphNodes(ctx, reachableCte, includedIdsCte),
11252
- fetchSubgraphEdges(ctx, reachableCte, includedIdsCte)
11253
- ]);
11254
- const nodes = nodeRows.map(
11255
- (row) => rowToNode({ ...row, props: normalizeProps(row.props) })
11256
- );
11257
- const edges = edgeRows.map(
11258
- (row) => rowToEdge({ ...row, props: normalizeProps(row.props) })
11259
- );
11260
- return {
11261
- nodes,
11262
- edges
11263
- };
11264
- }
11265
- function buildReachableCte(ctx) {
11266
- const shouldTrackPath = ctx.cyclePolicy === "prevent";
11267
- const edgeKindFilter = compileKindFilter(sql.raw("e.kind"), ctx.edgeKinds);
11268
- const initialPath = shouldTrackPath ? ctx.dialect.initializePath(sql.raw("n.id")) : void 0;
11269
- const pathExtension = shouldTrackPath ? ctx.dialect.extendPath(sql.raw("r.path"), sql.raw("n.id")) : void 0;
11270
- const cycleCheck = shouldTrackPath ? ctx.dialect.cycleCheck(sql.raw("n.id"), sql.raw("r.path")) : void 0;
11271
- const baseColumns = [sql`n.id`, sql`n.kind`, sql`0 AS depth`];
11272
- if (initialPath !== void 0) {
11273
- baseColumns.push(sql`${initialPath} AS path`);
11274
- }
11275
- const baseCase = sql`SELECT ${sql.join(baseColumns, sql`, `)} FROM ${ctx.schema.nodesTable} n WHERE n.graph_id = ${ctx.graphId} AND n.id = ${ctx.rootId} AND n.deleted_at IS NULL`;
11276
- const recursiveColumns = [
11277
- sql`n.id`,
11278
- sql`n.kind`,
11279
- sql`r.depth + 1 AS depth`
11280
- ];
11281
- if (pathExtension !== void 0) {
11282
- recursiveColumns.push(sql`${pathExtension} AS path`);
11283
- }
11284
- const recursiveWhereClauses = [
11285
- sql`e.graph_id = ${ctx.graphId}`,
11286
- edgeKindFilter,
11287
- sql`e.deleted_at IS NULL`,
11288
- sql`n.deleted_at IS NULL`,
11289
- sql`r.depth < ${ctx.maxDepth}`
11290
- ];
11291
- if (cycleCheck !== void 0) {
11292
- recursiveWhereClauses.push(cycleCheck);
11293
- }
11294
- const forceWorktableOuterJoinOrder = ctx.dialect.capabilities.forceRecursiveWorktableOuterJoinOrder;
11295
- const recursiveCase = ctx.direction === "both" ? compileBidirectionalBranch({
11296
- recursiveColumns,
11297
- whereClauses: recursiveWhereClauses,
11298
- forceWorktableOuterJoinOrder,
11299
- schema: ctx.schema
11300
- }) : compileRecursiveBranch({
11301
- recursiveColumns,
11302
- whereClauses: recursiveWhereClauses,
11303
- joinField: "from_id",
11304
- targetField: "to_id",
11305
- targetKindField: "to_kind",
11306
- forceWorktableOuterJoinOrder,
11307
- schema: ctx.schema
11308
- });
11309
- return sql`WITH RECURSIVE reachable AS (${baseCase} UNION ALL ${recursiveCase})`;
11310
- }
11311
- function compileRecursiveBranch(params) {
11312
- const columns = [...params.recursiveColumns];
11313
- const selectClause = sql`SELECT ${sql.join(columns, sql`, `)}`;
11314
- const nodeJoin = sql`JOIN ${params.schema.nodesTable} n ON n.graph_id = e.graph_id AND n.id = e.${sql.raw(params.targetField)} AND n.kind = e.${sql.raw(params.targetKindField)}`;
11315
- if (params.forceWorktableOuterJoinOrder) {
11316
- const allWhere = [
11317
- ...params.whereClauses,
11318
- sql`e.${sql.raw(params.joinField)} = r.id`
11319
- ];
11320
- return sql`${selectClause} FROM reachable r CROSS JOIN ${params.schema.edgesTable} e ${nodeJoin} WHERE ${sql.join(allWhere, sql` AND `)}`;
11321
- }
11322
- const where = [...params.whereClauses];
11323
- return sql`${selectClause} FROM reachable r JOIN ${params.schema.edgesTable} e ON e.${sql.raw(params.joinField)} = r.id ${nodeJoin} WHERE ${sql.join(where, sql` AND `)}`;
11324
- }
11325
- function compileBidirectionalBranch(params) {
11326
- const columns = [...params.recursiveColumns];
11327
- const selectClause = sql`SELECT ${sql.join(columns, sql`, `)}`;
11328
- const nodeJoin = sql`JOIN ${params.schema.nodesTable} n ON n.graph_id = e.graph_id AND ((e.to_id = r.id AND n.id = e.from_id AND n.kind = e.from_kind) OR (e.from_id = r.id AND n.id = e.to_id AND n.kind = e.to_kind))`;
11329
- if (params.forceWorktableOuterJoinOrder) {
11330
- const allWhere = [
11331
- ...params.whereClauses,
11332
- sql`(e.from_id = r.id OR e.to_id = r.id)`
11333
- ];
11334
- return sql`${selectClause} FROM reachable r CROSS JOIN ${params.schema.edgesTable} e ${nodeJoin} WHERE ${sql.join(allWhere, sql` AND `)}`;
11335
- }
11336
- return sql`${selectClause} FROM reachable r JOIN ${params.schema.edgesTable} e ON (e.from_id = r.id OR e.to_id = r.id) ${nodeJoin} WHERE ${sql.join([...params.whereClauses], sql` AND `)}`;
11337
- }
11338
- function buildIncludedIdsCte(ctx) {
11339
- const filters = [];
11340
- if (ctx.includeKinds !== void 0 && ctx.includeKinds.length > 0) {
11341
- filters.push(compileKindFilter(sql.raw("kind"), ctx.includeKinds));
11342
- }
11343
- if (ctx.excludeRoot) {
11344
- filters.push(sql`id != ${ctx.rootId}`);
11345
- }
11346
- const whereClause = filters.length > 0 ? sql` WHERE ${sql.join(filters, sql` AND `)}` : sql``;
11347
- return sql`, included_ids AS (SELECT DISTINCT id FROM reachable${whereClause})`;
11348
- }
11349
- async function fetchSubgraphNodes(ctx, reachableCte, includedIdsCte) {
11350
- const query = sql`${reachableCte}${includedIdsCte} SELECT n.kind, n.id, n.props, n.version, n.valid_from, n.valid_to, n.created_at, n.updated_at, n.deleted_at FROM ${ctx.schema.nodesTable} n WHERE n.graph_id = ${ctx.graphId} AND n.id IN (SELECT id FROM included_ids)`;
11351
- return ctx.backend.execute(query);
11352
- }
11353
- async function fetchSubgraphEdges(ctx, reachableCte, includedIdsCte) {
11354
- const edgeKindFilter = compileKindFilter(sql.raw("e.kind"), ctx.edgeKinds);
11355
- const query = sql`${reachableCte}${includedIdsCte} SELECT e.id, e.kind, e.from_kind, e.from_id, e.to_kind, e.to_id, e.props, e.valid_from, e.valid_to, e.created_at, e.updated_at, e.deleted_at FROM ${ctx.schema.edgesTable} e WHERE e.graph_id = ${ctx.graphId} AND ${edgeKindFilter} AND e.deleted_at IS NULL AND e.from_id IN (SELECT id FROM included_ids) AND e.to_id IN (SELECT id FROM included_ids)`;
11356
- return ctx.backend.execute(query);
11357
- }
11358
11738
 
11359
11739
  // src/store/store.ts
11360
11740
  var Store = class {
@@ -11558,6 +11938,46 @@ var Store = class {
11558
11938
  query() {
11559
11939
  return this.#createQueryForBackend(this.#backend);
11560
11940
  }
11941
+ // === Batch Query Execution ===
11942
+ /**
11943
+ * Executes multiple queries over a single connection with snapshot consistency.
11944
+ *
11945
+ * Acquires one connection via an implicit transaction, executes each query
11946
+ * sequentially on that connection, and returns a typed tuple of results.
11947
+ * Each query preserves its own result type, projection, filtering,
11948
+ * sorting, and pagination.
11949
+ *
11950
+ * Read-only — use `bulkCreate`, `bulkInsert`, etc. for write batching.
11951
+ *
11952
+ * @example
11953
+ * ```typescript
11954
+ * const [people, companies] = await store.batch(
11955
+ * store.query()
11956
+ * .from("Person", "p")
11957
+ * .select((ctx) => ({ id: ctx.p.id, name: ctx.p.name })),
11958
+ * store.query()
11959
+ * .from("Company", "c")
11960
+ * .select((ctx) => ({ id: ctx.c.id, name: ctx.c.name }))
11961
+ * .orderBy("c", "name", "asc")
11962
+ * .limit(5),
11963
+ * );
11964
+ * // people: readonly { id: string; name: string }[]
11965
+ * // companies: readonly { id: string; name: string }[]
11966
+ * ```
11967
+ *
11968
+ * @param queries - Two or more executable queries (from `.select()` or set operations)
11969
+ * @returns A tuple with per-query typed results, preserving input order
11970
+ */
11971
+ async batch(...queries) {
11972
+ return this.#backend.transaction(async (txBackend) => {
11973
+ const results = [];
11974
+ for (const query of queries) {
11975
+ const result = await query.executeOn(txBackend);
11976
+ results.push(result);
11977
+ }
11978
+ return results;
11979
+ });
11980
+ }
11561
11981
  // === Subgraph Extraction ===
11562
11982
  /**
11563
11983
  * Extracts a typed subgraph by traversing from a root node.
@@ -11583,6 +12003,7 @@ var Store = class {
11583
12003
  */
11584
12004
  async subgraph(rootId, options) {
11585
12005
  return executeSubgraph({
12006
+ graph: this.#graph,
11586
12007
  graphId: this.graphId,
11587
12008
  rootId,
11588
12009
  backend: this.#backend,
@@ -11762,6 +12183,6 @@ async function createStoreWithSchema(graph, backend, options) {
11762
12183
  return [store, result];
11763
12184
  }
11764
12185
 
11765
- export { DEFAULT_SQL_SCHEMA, ExecutableAggregateQuery, ExecutableQuery, MAX_EXPLICIT_RECURSIVE_DEPTH, MAX_RECURSIVE_DEPTH, PreparedQuery, QueryBuilder, UnionableQuery, avg, broader, composeFragments, core, count, countDistinct, createExternalRef, createFragment, createQueryBuilder, createSqlSchema, createStore, createStoreWithSchema, defineEdge, defineNode, differentFrom, disjointWith, equivalentTo, exists, externalRef, field, fieldRef, generateId, getExternalRefTable, hasPart, having, havingEq, havingGt, havingGte, havingLt, havingLte, implies, inSubquery, inverseOf, isExternalRefSchema, isParameterRef, limitFragment, max, metaEdge, min, narrower, notExists, notInSubquery, offsetFragment, orderByFragment, param, partOf, relatedTo, sameAs, subClassOf, sum };
12186
+ export { DEFAULT_SQL_SCHEMA, ExecutableAggregateQuery, ExecutableQuery, MAX_EXPLICIT_RECURSIVE_DEPTH, MAX_RECURSIVE_DEPTH, PreparedQuery, QueryBuilder, UnionableQuery, avg, broader, composeFragments, core, count, countDistinct, createExternalRef, createFragment, createQueryBuilder, createSqlSchema, createStore, createStoreWithSchema, defineEdge, defineNode, defineSubgraphProject, differentFrom, disjointWith, equivalentTo, exists, externalRef, field, fieldRef, generateId, getExternalRefTable, hasPart, having, havingEq, havingGt, havingGte, havingLt, havingLte, implies, inSubquery, inverseOf, isExternalRefSchema, isParameterRef, limitFragment, max, metaEdge, min, narrower, notExists, notInSubquery, offsetFragment, orderByFragment, param, partOf, relatedTo, sameAs, subClassOf, sum };
11766
12187
  //# sourceMappingURL=index.js.map
11767
12188
  //# sourceMappingURL=index.js.map