@sixb/pg 0.1.16 → 0.1.18

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.
Files changed (92) hide show
  1. package/README.md +125 -0
  2. package/dist/index.d.ts +15 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +3201 -1473
  5. package/dist/index.js.map +32 -25
  6. package/dist/migrations.d.ts.map +1 -1
  7. package/dist/objects/aggregate-admission.d.ts +10 -0
  8. package/dist/objects/aggregate-admission.d.ts.map +1 -0
  9. package/dist/objects/preparation-lock.d.ts +6 -0
  10. package/dist/objects/preparation-lock.d.ts.map +1 -0
  11. package/dist/objects/prepare-queries.d.ts +5 -0
  12. package/dist/objects/prepare-queries.d.ts.map +1 -0
  13. package/dist/objects/query-compiler.d.ts +19 -0
  14. package/dist/objects/query-compiler.d.ts.map +1 -1
  15. package/dist/objects/query-indexes.d.ts +43 -0
  16. package/dist/objects/query-indexes.d.ts.map +1 -0
  17. package/dist/objects/reader.d.ts +7 -1
  18. package/dist/objects/reader.d.ts.map +1 -1
  19. package/dist/objects/storage.d.ts +1 -1
  20. package/dist/objects/storage.d.ts.map +1 -1
  21. package/dist/objects/text-count-index.d.ts +24 -0
  22. package/dist/objects/text-count-index.d.ts.map +1 -0
  23. package/dist/ontology-storage/index.d.ts +2 -0
  24. package/dist/ontology-storage/index.d.ts.map +1 -1
  25. package/dist/ontology-storage/materialization-session.d.ts +18 -38
  26. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  27. package/dist/ontology-storage/materialization-state.d.ts +3 -26
  28. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  29. package/dist/ontology-storage/materialization-writer.d.ts +51 -9
  30. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  31. package/dist/ontology-storage/materializations.d.ts +5 -7
  32. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  33. package/dist/ontology-storage/outbox.d.ts.map +1 -1
  34. package/dist/ontology-storage/replacement-plans.d.ts +36 -0
  35. package/dist/ontology-storage/replacement-plans.d.ts.map +1 -0
  36. package/dist/ontology-storage/shared.d.ts +11 -5
  37. package/dist/ontology-storage/shared.d.ts.map +1 -1
  38. package/dist/ontology-storage/source-cleanup.d.ts.map +1 -1
  39. package/dist/ontology-storage/source-roots.d.ts +14 -1
  40. package/dist/ontology-storage/source-roots.d.ts.map +1 -1
  41. package/dist/ontology-storage/sources.d.ts +1 -2
  42. package/dist/ontology-storage/sources.d.ts.map +1 -1
  43. package/dist/pg-ai-cost-storage.d.ts.map +1 -1
  44. package/dist/pg-ai-limit-storage.d.ts.map +1 -1
  45. package/dist/pg-ai-usage-storage.d.ts +2 -0
  46. package/dist/pg-ai-usage-storage.d.ts.map +1 -1
  47. package/dist/pg-execution-storage.d.ts +28 -0
  48. package/dist/pg-execution-storage.d.ts.map +1 -1
  49. package/dist/pg-file-download-grant-storage.d.ts +11 -0
  50. package/dist/pg-file-download-grant-storage.d.ts.map +1 -0
  51. package/dist/pg-projection-run-storage.d.ts.map +1 -1
  52. package/dist/pg-rules-storage.d.ts.map +1 -1
  53. package/dist/transactions.d.ts +6 -2
  54. package/dist/transactions.d.ts.map +1 -1
  55. package/package.json +3 -3
  56. package/src/index.ts +53 -3
  57. package/src/migrations/053-replacement-plans.sql +72 -0
  58. package/src/migrations/054-slim-ontology-outbox.sql +108 -0
  59. package/src/migrations/055-object-query-preparation.sql +11 -0
  60. package/src/migrations/056-commit-touches.sql +41 -0
  61. package/src/migrations/057-audio-transcription.sql +10 -0
  62. package/src/migrations/058-reranking-model-kind.sql +11 -0
  63. package/src/migrations/059-file-download-grants.sql +16 -0
  64. package/src/migrations/060-drained-source-versions.sql +11 -0
  65. package/src/migrations.ts +20 -0
  66. package/src/objects/aggregate-admission.ts +21 -0
  67. package/src/objects/preparation-lock.ts +15 -0
  68. package/src/objects/prepare-queries.ts +181 -0
  69. package/src/objects/query-compiler.ts +404 -114
  70. package/src/objects/query-indexes.ts +348 -0
  71. package/src/objects/reader.ts +140 -51
  72. package/src/objects/storage.ts +15 -3
  73. package/src/objects/text-count-index.ts +455 -0
  74. package/src/ontology-storage/index.ts +3 -0
  75. package/src/ontology-storage/materialization-session.ts +116 -275
  76. package/src/ontology-storage/materialization-state.ts +8 -259
  77. package/src/ontology-storage/materialization-writer.ts +618 -508
  78. package/src/ontology-storage/materializations.ts +106 -289
  79. package/src/ontology-storage/outbox.ts +46 -15
  80. package/src/ontology-storage/replacement-plans.ts +1110 -0
  81. package/src/ontology-storage/shared.ts +27 -15
  82. package/src/ontology-storage/source-cleanup.ts +22 -10
  83. package/src/ontology-storage/source-roots.ts +35 -8
  84. package/src/ontology-storage/sources.ts +0 -30
  85. package/src/pg-ai-cost-storage.ts +12 -0
  86. package/src/pg-ai-limit-storage.ts +13 -2
  87. package/src/pg-ai-usage-storage.ts +8 -0
  88. package/src/pg-execution-storage.ts +8 -3
  89. package/src/pg-file-download-grant-storage.ts +76 -0
  90. package/src/pg-projection-run-storage.ts +6 -1
  91. package/src/pg-rules-storage.ts +7 -3
  92. package/src/transactions.ts +10 -3
@@ -22,6 +22,7 @@ export interface PgObjectQueryPageRow {
22
22
  }
23
23
 
24
24
  export interface CompiledPgObjectQuery {
25
+ traversalProbe?: { sql: string; args: unknown[]; limit: number }
25
26
  vectorProbe?: { sql: string; args: unknown[]; limit: number }
26
27
  sql: string
27
28
  args: unknown[]
@@ -52,6 +53,11 @@ interface CompiledHasMoreProbe {
52
53
 
53
54
  interface CompileContext {
54
55
  probeLimit: boolean
56
+ startPredicate?: string
57
+ isolateSortOverflow?: boolean
58
+ /** A consuming operator supplies ordering; bounded inputs still keep their own order. */
59
+ unordered?: boolean
60
+ correlatedTraversal?: boolean
55
61
  source: PgObjectQuerySource
56
62
  }
57
63
 
@@ -105,7 +111,12 @@ const DEFAULT_EXPANSION_FANOUT = 1_000
105
111
  export function compilePgObjectQuery(
106
112
  projectId: string,
107
113
  query: ObjectQuery,
108
- options: { includeTotal?: boolean; source?: PgObjectQuerySource } = {}
114
+ options: {
115
+ includeTotal?: boolean
116
+ source?: PgObjectQuerySource
117
+ correlatedTraversal?: boolean
118
+ sortIndexBound?: string
119
+ } = {}
109
120
  ): CompiledPgObjectQuery {
110
121
  if (hasVectorProfile(query) && !isVectorProfileQuery(query)) {
111
122
  throw new ObjectQueryExecutionError(
@@ -114,14 +125,43 @@ export function compilePgObjectQuery(
114
125
  )
115
126
  }
116
127
  const source = options.source ?? DEFAULT_OBJECT_QUERY_SOURCE
117
- const compiled = source.wrapQuery(
128
+ let compiled = source.wrapQuery(
118
129
  compileObjectQueryInternal(projectId, query, {
119
130
  probeLimit: options.includeTotal === false,
131
+ correlatedTraversal: options.correlatedTraversal,
120
132
  source,
121
133
  })
122
134
  )
135
+ if (options.sortIndexBound && source === DEFAULT_OBJECT_QUERY_SOURCE) {
136
+ let bounded = query
137
+ while (bounded.kind === "expand") bounded = bounded.input
138
+ if (bounded.kind === "page" || bounded.kind === "limit") {
139
+ const branches = [options.sortIndexBound, `NOT (${options.sortIndexBound})`].map(
140
+ (startPredicate, branch) =>
141
+ compileObjectQueryInternal(projectId, query, {
142
+ probeLimit: options.includeTotal === false,
143
+ source,
144
+ startPredicate,
145
+ isolateSortOverflow: branch === 1,
146
+ correlatedTraversal: options.correlatedTraversal,
147
+ })
148
+ )
149
+ const limit =
150
+ bounded.kind === "page"
151
+ ? bounded.pageSize + 1
152
+ : bounded.limit + (options.includeTotal === false ? 1 : 0)
153
+ compiled = {
154
+ ...compiled,
155
+ sql: `SELECT * FROM ((${branches[0]!.sql}) UNION ALL (${branches[1]!.sql})) AS input ORDER BY ${compiled.order.sql} LIMIT ?`,
156
+ args: [...branches[0]!.args, ...branches[1]!.args, ...compiled.order.args, limit],
157
+ }
158
+ }
159
+ }
160
+ const traversal = !options.correlatedTraversal && boundedTraversal(query)
161
+ const traversalProbe = traversal ? compileTraversalProbe(projectId, traversal, source) : undefined
123
162
  return {
124
163
  ...compiled,
164
+ ...(traversalProbe ? { traversalProbe } : {}),
125
165
  sql: numberPlaceholders(compiled.sql),
126
166
  totalSql: numberPlaceholders(compiled.totalSql),
127
167
  vectorProbe: compiled.vectorProbe
@@ -238,12 +278,72 @@ export function compilePgObjectFacetQuery(
238
278
  )
239
279
  }
240
280
 
281
+ /** A single aggregate computes every facet and its exact total under one SQL snapshot. */
282
+ export function compilePgObjectFacetsQuery(
283
+ projectId: string,
284
+ query: ObjectQuery,
285
+ facets: readonly { propertyId: string; limit: number }[],
286
+ options: { source?: PgObjectQuerySource } = {}
287
+ ): CompiledPgFacetQuery {
288
+ const querySource = options.source ?? DEFAULT_OBJECT_QUERY_SOURCE
289
+ const source = compileAggregateSource(projectId, query, querySource)
290
+ return compilePgFacetSummary(
291
+ numberCompiledQuery(querySource.wrapStatement(source.sql, source.args)),
292
+ facets
293
+ )
294
+ }
295
+
296
+ /** Input is a numbered statement returning properties; compact read paths can supply it too. */
297
+ export function compilePgFacetSummary(
298
+ source: CompiledPgFacetQuery,
299
+ facets: readonly { propertyId: string; limit: number }[],
300
+ nativeValues = false
301
+ ): CompiledPgFacetQuery {
302
+ const args = [...source.args]
303
+ const bind = (value: unknown) => {
304
+ args.push(value)
305
+ return `$${args.length}`
306
+ }
307
+ const fields = facets.map((facet, i) => {
308
+ if (nativeValues) return `f${i}_type, f${i}_value`
309
+ const property = bind(facet.propertyId)
310
+ // Group JSONB before rendering: text would split equal numeric values such as 1.0 / 1.00.
311
+ return `jsonb_typeof(input.properties -> ${property}::text) AS f${i}_type, input.properties -> ${property}::text AS f${i}_value`
312
+ })
313
+ if (!fields.length)
314
+ return {
315
+ sql: `SELECT -1 AS facet_index, NULL::text AS value_type, NULL::text AS value_text, count(*) AS count FROM (${source.sql}) AS input`,
316
+ args,
317
+ }
318
+ const columns = facets.map((_, i) => `f${i}_type, f${i}_value`)
319
+ const groups = facets.map((_, i) => `WHEN GROUPING(f${i}_type) = 0 THEN ${i}`).join(" ")
320
+ const buckets = facets.map(
321
+ (facet, i) =>
322
+ `(SELECT ${i} AS facet_index, f${i}_type AS value_type, ${nativeValues ? `f${i}_value` : `(f${i}_value #>> '{}')`} AS value_text, count FROM _sixb_facet_groups WHERE facet_index = ${i} AND f${i}_type IS NOT NULL ORDER BY count DESC, value_text ASC LIMIT ${bind(facet.limit)})`
323
+ )
324
+ return {
325
+ sql: `WITH _sixb_facet_groups AS MATERIALIZED (
326
+ SELECT CASE ${groups} ELSE -1 END AS facet_index, ${columns.join(", ")}, count(*) AS count
327
+ FROM (SELECT ${fields.join(", ")} FROM (${source.sql}) AS input) AS _sixb_values
328
+ GROUP BY GROUPING SETS ((), ${columns.map((column) => `(${column})`).join(", ")})
329
+ )
330
+ SELECT -1 AS facet_index, NULL::text AS value_type, NULL::text AS value_text, count FROM _sixb_facet_groups WHERE facet_index = -1
331
+ UNION ALL ${buckets.join(" UNION ALL ")}`,
332
+ args,
333
+ }
334
+ }
335
+
241
336
  function compileObjectQueryInternal(
242
337
  projectId: string,
243
338
  query: ObjectQuery,
244
339
  ctx: CompileContext
245
340
  ): CompiledPgObjectQuery {
246
341
  switch (query.kind) {
342
+ case "rerank":
343
+ throw new ObjectQueryExecutionError(
344
+ "reranking_requires_runtime",
345
+ "Reranking must execute in the model runtime, not storage."
346
+ )
247
347
  case "start":
248
348
  return compileStart(projectId, query, ctx)
249
349
  case "refs":
@@ -366,12 +466,14 @@ function compileStart(
366
466
  SELECT *, properties AS _cursor_properties
367
467
  FROM ${ctx.source.objectsTable}
368
468
  WHERE project_id = ? AND object_type_id = ?
369
- ORDER BY ${order.sql}
469
+ ${ctx.startPredicate ? `AND (${ctx.startPredicate})` : ""}
470
+ ${ctx.unordered ? "" : `ORDER BY ${order.sql}`}
471
+ ${ctx.isolateSortOverflow ? "OFFSET 0" : ""}
370
472
  `
371
473
 
372
474
  return {
373
475
  sql,
374
- args: [projectId, query.objectTypeId, ...order.args],
476
+ args: [projectId, query.objectTypeId, ...(ctx.unordered ? [] : order.args)],
375
477
  totalSql: `SELECT COUNT(*)::bigint AS total FROM ${ctx.source.objectsTable} WHERE project_id = ? AND object_type_id = ?`,
376
478
  totalArgs: [projectId, query.objectTypeId],
377
479
  order,
@@ -390,6 +492,13 @@ function compileRefs(
390
492
  throw new Error("[SixbPg] PostgreSQL object storage requires at least one ref")
391
493
  }
392
494
 
495
+ // Keep a concrete type visible to PostgreSQL even when identities arrive as JSON.
496
+ // Relationship joins can then use partial incoming-link indexes for that type.
497
+ const objectTypeId = query.refs[0]!.objectTypeId
498
+ const uniformType = query.refs.every((ref) => ref.objectTypeId === objectTypeId)
499
+ const typePredicate = uniformType ? "AND selected.object_type_id = ?" : ""
500
+ const typeArgs = uniformType ? [objectTypeId] : []
501
+
393
502
  const order = compileOrder(identityOrderFields())
394
503
  const selectedOrder = compileOrder(identityOrderFields(), "selected")
395
504
  const refsJson = JSON.stringify(query.refs)
@@ -406,12 +515,13 @@ function compileRefs(
406
515
  ON selected.project_id = ?
407
516
  AND selected.object_type_id = requested.object_type_id
408
517
  AND selected.primary_id = requested.primary_id
409
- ORDER BY ${selectedOrder.sql}
518
+ ${typePredicate}
519
+ ${ctx.unordered ? "" : `ORDER BY ${selectedOrder.sql}`}
410
520
  `
411
521
 
412
522
  return {
413
523
  sql,
414
- args: [refsJson, projectId, ...selectedOrder.args],
524
+ args: [refsJson, projectId, ...typeArgs, ...(ctx.unordered ? [] : selectedOrder.args)],
415
525
  totalSql: `
416
526
  SELECT COUNT(*)::bigint AS total
417
527
  FROM (${requested}) AS requested
@@ -419,8 +529,9 @@ function compileRefs(
419
529
  ON selected.project_id = ?
420
530
  AND selected.object_type_id = requested.object_type_id
421
531
  AND selected.primary_id = requested.primary_id
532
+ ${typePredicate}
422
533
  `,
423
- totalArgs: [refsJson, projectId],
534
+ totalArgs: [refsJson, projectId, ...typeArgs],
424
535
  order,
425
536
  hasMore: () => false,
426
537
  trimRows: identityRows,
@@ -443,17 +554,20 @@ function compileWhere(
443
554
  predicate: CompiledPredicate,
444
555
  ctx: CompileContext
445
556
  ): CompiledPgObjectQuery {
446
- const input = compileObjectQueryInternal(projectId, inputQuery, exactContext(ctx))
557
+ const input = compileObjectQueryInternal(projectId, inputQuery, {
558
+ ...exactContext(ctx),
559
+ unordered: true,
560
+ })
447
561
  const sql = `
448
562
  SELECT *
449
563
  FROM (${input.sql}) AS input
450
564
  WHERE ${predicate.sql}
451
- ORDER BY ${input.order.sql}
565
+ ${ctx.unordered ? "" : `ORDER BY ${input.order.sql}`}
452
566
  `
453
567
 
454
568
  return {
455
569
  sql,
456
- args: [...input.args, ...predicate.args, ...input.order.args],
570
+ args: [...input.args, ...predicate.args, ...(ctx.unordered ? [] : input.order.args)],
457
571
  totalSql: `
458
572
  SELECT COUNT(*)::bigint AS total
459
573
  FROM (${input.sql}) AS input
@@ -490,7 +604,10 @@ function compileSort(
490
604
  fields: readonly ObjectQuerySortField[],
491
605
  ctx: CompileContext
492
606
  ): CompiledPgObjectQuery {
493
- const input = compileObjectQueryInternal(projectId, inputQuery, exactContext(ctx))
607
+ const input = compileObjectQueryInternal(projectId, inputQuery, {
608
+ ...exactContext(ctx),
609
+ unordered: true,
610
+ })
494
611
  const probeInput =
495
612
  ctx.probeLimit && needsLimitProbe(inputQuery)
496
613
  ? compileObjectQueryInternal(projectId, inputQuery, ctx)
@@ -510,9 +627,9 @@ function compileSort(
510
627
  input.version,
511
628
  input.last_commit_id${input.vectorProbe ? ", input._vector_score" : ""}
512
629
  FROM (${input.sql}) AS input
513
- ORDER BY ${order.sql}
630
+ ${ctx.unordered ? "" : `ORDER BY ${order.sql}`}
514
631
  `,
515
- args: [...input.args, ...order.args],
632
+ args: [...input.args, ...(ctx.unordered ? [] : order.args)],
516
633
  totalSql: input.totalSql,
517
634
  totalArgs: input.totalArgs,
518
635
  order,
@@ -529,10 +646,18 @@ function compileLimit(
529
646
  rawLimit: number,
530
647
  ctx: CompileContext
531
648
  ): CompiledPgObjectQuery {
532
- const input = compileObjectQueryInternal(projectId, inputQuery, exactContext(ctx))
649
+ const input = compileObjectQueryInternal(projectId, inputQuery, {
650
+ ...exactContext(ctx),
651
+ unordered: true,
652
+ })
533
653
  const limit = Math.max(0, rawLimit)
534
654
  const rowLimit = ctx.probeLimit ? limit + 1 : limit
535
655
 
656
+ // A plan bounded for the page must not force correlated probes over the full total.
657
+ const totalInput = ctx.correlatedTraversal
658
+ ? compileAggregateSource(projectId, inputQuery, ctx.source)
659
+ : input
660
+
536
661
  return {
537
662
  sql: `
538
663
  SELECT *
@@ -543,9 +668,9 @@ function compileLimit(
543
668
  args: [...input.args, ...input.order.args, rowLimit],
544
669
  totalSql: `
545
670
  SELECT COUNT(*)::bigint AS total
546
- FROM (${input.sql}) AS input
671
+ FROM (${totalInput.sql}) AS input
547
672
  `,
548
- totalArgs: input.args,
673
+ totalArgs: totalInput.args,
549
674
  vectorProbe: input.vectorProbe,
550
675
  order: input.order,
551
676
  hasMore: (rowCount, total) =>
@@ -562,7 +687,10 @@ function compilePage(
562
687
  pageToken: string | undefined,
563
688
  ctx: CompileContext
564
689
  ): CompiledPgObjectQuery {
565
- const input = compileObjectQueryInternal(projectId, inputQuery, exactContext(ctx))
690
+ const input = compileObjectQueryInternal(projectId, inputQuery, {
691
+ ...exactContext(ctx),
692
+ unordered: true,
693
+ })
566
694
  const pageSize = Math.max(0, rawPageSize)
567
695
  const cursor = pageToken ? decodePageToken(pageToken, input.order.fields) : undefined
568
696
  const cursorPredicate = cursor
@@ -570,20 +698,50 @@ function compilePage(
570
698
  : undefined
571
699
  const whereSql = cursorPredicate ? `WHERE ${cursorPredicate.sql}` : ""
572
700
 
701
+ const ranges =
702
+ cursor && isIndexPageInput(inputQuery)
703
+ ? compileKeysetRanges(input.order.fields, cursor, input.order.propertyColumn)
704
+ : undefined
705
+ const rangeSql = ranges
706
+ ?.map(
707
+ (range) =>
708
+ `(SELECT * FROM (${input.sql}) AS input WHERE ${range.sql} ORDER BY ${input.order.sql} LIMIT ?)`
709
+ )
710
+ .join(" UNION ALL ")
711
+
712
+ // A plan bounded for the page must not force correlated probes over the full total.
713
+ const totalInput = ctx.correlatedTraversal
714
+ ? compileAggregateSource(projectId, inputQuery, ctx.source)
715
+ : input
716
+
573
717
  return {
574
- sql: `
718
+ sql: rangeSql
719
+ ? `SELECT * FROM (${rangeSql}) AS input ORDER BY ${input.order.sql} LIMIT ?`
720
+ : `
575
721
  SELECT *
576
722
  FROM (${input.sql}) AS input
577
723
  ${whereSql}
578
724
  ORDER BY ${input.order.sql}
579
725
  LIMIT ?
580
726
  `,
581
- args: [...input.args, ...(cursorPredicate?.args ?? []), ...input.order.args, pageSize + 1],
727
+ args:
728
+ rangeSql && ranges
729
+ ? [
730
+ ...ranges.flatMap((range) => [
731
+ ...input.args,
732
+ ...range.args,
733
+ ...input.order.args,
734
+ pageSize + 1,
735
+ ]),
736
+ ...input.order.args,
737
+ pageSize + 1,
738
+ ]
739
+ : [...input.args, ...(cursorPredicate?.args ?? []), ...input.order.args, pageSize + 1],
582
740
  totalSql: `
583
741
  SELECT COUNT(*)::bigint AS total
584
- FROM (${input.sql}) AS input
742
+ FROM (${totalInput.sql}) AS input
585
743
  `,
586
- totalArgs: input.args,
744
+ totalArgs: totalInput.args,
587
745
  vectorProbe: input.vectorProbe,
588
746
  order: input.order,
589
747
  hasMore: (rowCount) => rowCount > pageSize,
@@ -603,52 +761,28 @@ function compileTraversal(
603
761
  sourceObjectTypeId: string | undefined,
604
762
  ctx: CompileContext
605
763
  ): CompiledPgObjectQuery {
606
- const input = compileObjectQueryInternal(projectId, inputQuery, exactContext(ctx))
607
- const outputAlias = direction === "outgoing" ? "target_object" : "source_object"
608
- const joinSql =
609
- direction === "outgoing"
610
- ? `
611
- JOIN ${ctx.source.linksTable} AS edge
612
- ON edge.project_id = input.project_id
613
- AND edge.source_type_id = input.object_type_id
614
- AND edge.source_id = input.primary_id
615
- AND edge.link_id = ?
616
- JOIN ${ctx.source.objectsTable} AS target_object
617
- ON target_object.project_id = edge.project_id
618
- AND target_object.object_type_id = edge.target_type_id
619
- AND target_object.primary_id = edge.target_id
620
- `
621
- : `
622
- JOIN ${ctx.source.linksTable} AS edge
623
- ON edge.project_id = input.project_id
624
- AND edge.target_type_id = input.object_type_id
625
- AND edge.target_id = input.primary_id
626
- AND edge.link_id = ?${sourceObjectTypeId === undefined ? "" : "\n AND edge.source_type_id = ?"}
627
- JOIN ${ctx.source.objectsTable} AS source_object
628
- ON source_object.project_id = edge.project_id
629
- AND source_object.object_type_id = edge.source_type_id
630
- AND source_object.primary_id = edge.source_id
631
- `
632
- const order = compileOrder(identityOrderFields())
633
- const qualifiedOrder = compileOrder(identityOrderFields(), outputAlias)
634
- const sql = `
635
- SELECT DISTINCT ${outputAlias}.*, ${outputAlias}.properties AS _cursor_properties
636
- FROM (${input.sql}) AS input
637
- ${joinSql}
638
- ORDER BY ${qualifiedOrder.sql}
639
- `
640
- const args = [
641
- ...input.args,
764
+ const input = compileObjectQueryInternal(projectId, inputQuery, {
765
+ ...exactContext(ctx),
766
+ unordered: true,
767
+ startPredicate: undefined,
768
+ isolateSortOverflow: false,
769
+ })
770
+ const relation = compileTraversalRelation(
771
+ projectId,
772
+ input,
642
773
  linkId,
643
- ...(sourceObjectTypeId === undefined ? [] : [sourceObjectTypeId]),
644
- ...qualifiedOrder.args,
645
- ]
646
-
774
+ direction,
775
+ sourceObjectTypeId,
776
+ ctx.source,
777
+ ctx.correlatedTraversal,
778
+ ctx.startPredicate
779
+ )
780
+ const order = compileOrder(identityOrderFields())
647
781
  return {
648
- sql,
649
- args,
650
- totalSql: `SELECT COUNT(*)::bigint AS total FROM (${sql}) AS traversed`,
651
- totalArgs: args,
782
+ sql: `SELECT selected.*, selected.properties AS _cursor_properties ${relation.sql} ${ctx.unordered ? "" : `ORDER BY ${order.sql}`} ${ctx.isolateSortOverflow ? "OFFSET 0" : ""}`,
783
+ args: [...relation.args, ...(ctx.unordered ? [] : order.args)],
784
+ totalSql: `SELECT COUNT(*)::bigint AS total ${relation.sql}`,
785
+ totalArgs: relation.args,
652
786
  order,
653
787
  hasMore: () => false,
654
788
  trimRows: identityRows,
@@ -656,6 +790,96 @@ function compileTraversal(
656
790
  }
657
791
  }
658
792
 
793
+ /** A traversal is a set of endpoints. EXISTS avoids sorting/deduplicating full JSON objects. */
794
+ function compileTraversalRelation(
795
+ projectId: string,
796
+ input: CompiledAggregateSource,
797
+ linkId: string,
798
+ direction: "outgoing" | "incoming",
799
+ sourceObjectTypeId: string | undefined,
800
+ source: PgObjectQuerySource,
801
+ correlated = false,
802
+ endpointPredicate?: string
803
+ ): CompiledAggregateSource {
804
+ const incoming = direction === "incoming"
805
+ const parent = incoming ? "target" : "source"
806
+ const endpoint = incoming ? "source" : "target"
807
+ const constrainedType = incoming ? sourceObjectTypeId : undefined
808
+ return {
809
+ sql: `FROM ${source.objectsTable} AS selected
810
+ WHERE selected.project_id = ?
811
+ ${constrainedType === undefined ? "" : "AND selected.object_type_id = ?"}
812
+ ${endpointPredicate ? `AND (${endpointPredicate})` : ""}
813
+ AND EXISTS (
814
+ SELECT 1 FROM (${input.sql}) AS input
815
+ JOIN ${source.linksTable} AS edge
816
+ ON edge.project_id = input.project_id
817
+ AND edge.${parent}_type_id = input.object_type_id
818
+ AND edge.${parent}_id = input.primary_id
819
+ WHERE edge.project_id = selected.project_id
820
+ AND edge.${endpoint}_type_id = selected.object_type_id
821
+ AND edge.${endpoint}_id = selected.primary_id
822
+ AND edge.link_id = ?
823
+ ${correlated ? "OFFSET 0" : ""}
824
+ )`,
825
+ args: [
826
+ projectId,
827
+ ...(constrainedType === undefined ? [] : [constrainedType]),
828
+ ...input.args,
829
+ linkId,
830
+ ],
831
+ }
832
+ }
833
+
834
+ /** Only a bounded page benefits from choosing an ordered, correlated endpoint scan. */
835
+ function boundedTraversal(
836
+ query: ObjectQuery,
837
+ bounded = false
838
+ ): Extract<ObjectQuery, { kind: "traverse" }> | undefined {
839
+ switch (query.kind) {
840
+ case "page":
841
+ case "limit":
842
+ return boundedTraversal(query.input, true)
843
+ case "sort":
844
+ case "filter":
845
+ case "text":
846
+ case "project":
847
+ case "expand":
848
+ return boundedTraversal(query.input, bounded)
849
+ case "traverse":
850
+ return bounded ? query : undefined
851
+ default:
852
+ return undefined
853
+ }
854
+ }
855
+
856
+ function compileTraversalProbe(
857
+ projectId: string,
858
+ traversal: Extract<ObjectQuery, { kind: "traverse" }>,
859
+ source: PgObjectQuerySource
860
+ ): { sql: string; args: unknown[]; limit: number } {
861
+ const input = compileAggregateSource(projectId, traversal.input, source)
862
+ const parent = traversal.direction === "incoming" ? "target" : "source"
863
+ const limit = 1000
864
+ const statement = compilePgObjectStatement(
865
+ `SELECT 1 FROM (${input.sql}) AS input
866
+ JOIN ${source.linksTable} AS edge ON edge.project_id = input.project_id
867
+ AND edge.${parent}_type_id = input.object_type_id AND edge.${parent}_id = input.primary_id
868
+ WHERE edge.link_id = ? ${traversal.direction === "incoming" && traversal.sourceObjectTypeId ? "AND edge.source_type_id = ?" : ""}
869
+ LIMIT ?`,
870
+ [
871
+ ...input.args,
872
+ traversal.linkId,
873
+ ...(traversal.direction === "incoming" && traversal.sourceObjectTypeId
874
+ ? [traversal.sourceObjectTypeId]
875
+ : []),
876
+ limit + 1,
877
+ ],
878
+ source
879
+ )
880
+ return { ...statement, limit }
881
+ }
882
+
659
883
  /** Correlation handle for an expansion: the parent row's identity columns. */
660
884
  interface ExpansionParent {
661
885
  project: string
@@ -849,7 +1073,7 @@ function compileExpansionOrder(
849
1073
  `${
850
1074
  field.scalarKind === "decimal"
851
1075
  ? `(${jsonTextExpression(propertyColumn)})::numeric`
852
- : jsonValueExpression(propertyColumn)
1076
+ : `NULLIF(${jsonValueExpression(propertyColumn)}, 'null'::jsonb)`
853
1077
  } ${direction}`
854
1078
  )
855
1079
  args.push(field.propertyId, field.propertyId, field.propertyId)
@@ -958,6 +1182,11 @@ function compileAggregateSource(
958
1182
  source: PgObjectQuerySource
959
1183
  ): CompiledAggregateSource {
960
1184
  switch (query.kind) {
1185
+ case "rerank":
1186
+ throw new ObjectQueryExecutionError(
1187
+ "reranking_requires_runtime",
1188
+ "Reranking must execute in the model runtime, not storage."
1189
+ )
961
1190
  case "start":
962
1191
  return compileAggregateStart(projectId, query, source)
963
1192
  case "refs":
@@ -1029,6 +1258,13 @@ function compileAggregateRefs(
1029
1258
  throw new Error("[SixbPg] PostgreSQL object storage requires at least one ref")
1030
1259
  }
1031
1260
 
1261
+ // Keep a concrete type visible to PostgreSQL even when identities arrive as JSON.
1262
+ // Relationship joins can then use partial incoming-link indexes for that type.
1263
+ const objectTypeId = query.refs[0]!.objectTypeId
1264
+ const uniformType = query.refs.every((ref) => ref.objectTypeId === objectTypeId)
1265
+ const typePredicate = uniformType ? "AND selected.object_type_id = ?" : ""
1266
+ const typeArgs = uniformType ? [objectTypeId] : []
1267
+
1032
1268
  return {
1033
1269
  sql: `
1034
1270
  SELECT selected.project_id, selected.object_type_id, selected.primary_id, selected.properties
@@ -1042,8 +1278,9 @@ function compileAggregateRefs(
1042
1278
  ON selected.project_id = ?
1043
1279
  AND selected.object_type_id = requested.object_type_id
1044
1280
  AND selected.primary_id = requested.primary_id
1281
+ ${typePredicate}
1045
1282
  `,
1046
- args: [JSON.stringify(query.refs), projectId],
1283
+ args: [JSON.stringify(query.refs), projectId, ...typeArgs],
1047
1284
  }
1048
1285
  }
1049
1286
 
@@ -1073,47 +1310,40 @@ function compileAggregateTraversal(
1073
1310
  source: PgObjectQuerySource
1074
1311
  ): CompiledAggregateSource {
1075
1312
  const input = compileAggregateSource(projectId, inputQuery, source)
1076
- const outputAlias = direction === "outgoing" ? "target_object" : "source_object"
1077
- const joinSql =
1078
- direction === "outgoing"
1079
- ? `
1080
- JOIN ${source.linksTable} AS edge
1081
- ON edge.project_id = input.project_id
1082
- AND edge.source_type_id = input.object_type_id
1083
- AND edge.source_id = input.primary_id
1084
- AND edge.link_id = ?
1085
- JOIN ${source.objectsTable} AS target_object
1086
- ON target_object.project_id = edge.project_id
1087
- AND target_object.object_type_id = edge.target_type_id
1088
- AND target_object.primary_id = edge.target_id
1089
- `
1090
- : `
1091
- JOIN ${source.linksTable} AS edge
1092
- ON edge.project_id = input.project_id
1093
- AND edge.target_type_id = input.object_type_id
1094
- AND edge.target_id = input.primary_id
1095
- AND edge.link_id = ?${sourceObjectTypeId === undefined ? "" : "\n AND edge.source_type_id = ?"}
1096
- JOIN ${source.objectsTable} AS source_object
1097
- ON source_object.project_id = edge.project_id
1098
- AND source_object.object_type_id = edge.source_type_id
1099
- AND source_object.primary_id = edge.source_id
1100
- `
1101
-
1313
+ if (direction === "incoming" && sourceObjectTypeId !== undefined) {
1314
+ // Deduplicate endpoint identities before fetching objects. Bounded arrays let PostgreSQL
1315
+ // visit primary keys in groups instead of doing one random B-tree descent per edge.
1316
+ // Each array contains at most 1024 IDs; larger traversals can spill their sort/group work.
1317
+ return {
1318
+ sql: `SELECT selected.project_id, selected.object_type_id, selected.primary_id, selected.properties
1319
+ FROM (
1320
+ SELECT array_agg(primary_id) AS ids FROM (
1321
+ SELECT primary_id, (row_number() OVER (ORDER BY primary_id) - 1) / 1024 AS batch
1322
+ FROM (
1323
+ SELECT DISTINCT edge.source_id AS primary_id
1324
+ FROM (${input.sql}) AS input
1325
+ JOIN ${source.linksTable} AS edge ON edge.project_id = input.project_id
1326
+ AND edge.target_type_id = input.object_type_id AND edge.target_id = input.primary_id
1327
+ WHERE edge.link_id = ? AND edge.source_type_id = ?
1328
+ ) AS endpoints
1329
+ ) AS numbered GROUP BY batch
1330
+ ) AS batches
1331
+ JOIN ${source.objectsTable} AS selected ON selected.project_id = ?
1332
+ AND selected.object_type_id = ? AND selected.primary_id = ANY(batches.ids)`,
1333
+ args: [...input.args, linkId, sourceObjectTypeId, projectId, sourceObjectTypeId],
1334
+ }
1335
+ }
1336
+ const relation = compileTraversalRelation(
1337
+ projectId,
1338
+ input,
1339
+ linkId,
1340
+ direction,
1341
+ sourceObjectTypeId,
1342
+ source
1343
+ )
1102
1344
  return {
1103
- sql: `
1104
- SELECT DISTINCT
1105
- ${outputAlias}.project_id,
1106
- ${outputAlias}.object_type_id,
1107
- ${outputAlias}.primary_id,
1108
- ${outputAlias}.properties
1109
- FROM (${input.sql}) AS input
1110
- ${joinSql}
1111
- `,
1112
- args: [
1113
- ...input.args,
1114
- linkId,
1115
- ...(sourceObjectTypeId === undefined ? [] : [sourceObjectTypeId]),
1116
- ],
1345
+ sql: `SELECT selected.project_id, selected.object_type_id, selected.primary_id, selected.properties ${relation.sql}`,
1346
+ args: relation.args,
1117
1347
  }
1118
1348
  }
1119
1349
 
@@ -1349,8 +1579,8 @@ function compileEqualityPredicate(
1349
1579
  if (typeof value === "string") {
1350
1580
  if (op === "eq") {
1351
1581
  return {
1352
- sql: `(${jsonTypeExpression()} = 'string' AND ${jsonTextExpression()} = ?::text)`,
1353
- args: [propertyId, propertyId, value],
1582
+ sql: `(${jsonTypeExpression()} = 'string' AND ${jsonTextExpression()} = ?::text AND md5(${jsonTextExpression()}) = md5(?::text))`,
1583
+ args: [propertyId, propertyId, value, propertyId, value],
1354
1584
  }
1355
1585
  }
1356
1586
 
@@ -1391,9 +1621,11 @@ function compileInPredicate(propertyId: string, values: readonly unknown[]): Com
1391
1621
  clauses.push(
1392
1622
  `(${jsonTypeExpression()} = 'string' AND ${jsonTextExpression()} IN (${stringValues
1393
1623
  .map(() => "?::text")
1394
- .join(", ")}))`
1624
+ .join(
1625
+ ", "
1626
+ )}) AND md5(${jsonTextExpression()}) IN (${stringValues.map(() => "md5(?::text)").join(", ")}))`
1395
1627
  )
1396
- args.push(propertyId, propertyId, ...stringValues)
1628
+ args.push(propertyId, propertyId, ...stringValues, propertyId, ...stringValues)
1397
1629
  }
1398
1630
 
1399
1631
  if (jsonbValues.length > 0) {
@@ -1441,11 +1673,13 @@ function compileTextPredicate(query: string, fields: readonly string[]): Compile
1441
1673
 
1442
1674
  const clauses = terms.map(() => {
1443
1675
  const fieldClauses = fields.map(
1444
- () => `position(?::text in lower(coalesce(${jsonTextExpression()}, ''))) > 0`
1676
+ () => `lower(coalesce(${jsonTextExpression()}, '')) LIKE ?::text`
1445
1677
  )
1446
1678
  return `(${fieldClauses.join(" OR ")})`
1447
1679
  })
1448
- const args = terms.flatMap((term) => fields.flatMap((field) => [term, field]))
1680
+ const args = terms.flatMap((term) =>
1681
+ fields.flatMap((field) => [field, `%${term.replace(/[\\%_]/g, "\\$&")}%`])
1682
+ )
1449
1683
 
1450
1684
  return { sql: `(${clauses.join(" AND ")})`, args }
1451
1685
  }
@@ -1544,6 +1778,62 @@ function identityOrderFields(): readonly CompiledOrderField[] {
1544
1778
  ]
1545
1779
  }
1546
1780
 
1781
+ /** Bound each disjoint cursor range before merging: OR would scan all preceding index entries. */
1782
+ function compileKeysetRanges(
1783
+ fields: readonly CompiledOrderField[],
1784
+ cursor: readonly EncodedCursorValue[],
1785
+ propertyColumn: string
1786
+ ): CompiledPredicate[] {
1787
+ const ranges: CompiledPredicate[] = []
1788
+ const prefix: CompiledPredicate[] = []
1789
+ const addRange = (advance: CompiledPredicate) => {
1790
+ const parts = [...prefix, advance]
1791
+ ranges.push({
1792
+ sql: parts.map((part) => `(${part.sql})`).join(" AND "),
1793
+ args: parts.flatMap((part) => part.args),
1794
+ })
1795
+ }
1796
+ fields.forEach((field, index) => {
1797
+ const value = cursor[index]
1798
+ if (field.kind === "column") {
1799
+ const after = compileFieldAfter(field, value, propertyColumn)
1800
+ if (after) addRange(after)
1801
+ prefix.push(compileFieldEquality(field, value, propertyColumn))
1802
+ return
1803
+ }
1804
+ const rank = `CASE WHEN ${jsonTypeExpression(propertyColumn)} IS NULL OR ${jsonTypeExpression(propertyColumn)} = 'null' THEN 1 ELSE 0 END`
1805
+ if (!value.nullish) addRange({ sql: `${rank} = 1`, args: [field.propertyId, field.propertyId] })
1806
+ prefix.push({
1807
+ sql: `${rank} = ${value.nullish ? 1 : 0}`,
1808
+ args: [field.propertyId, field.propertyId],
1809
+ })
1810
+ if (value.nullish) return
1811
+ const expression = compiledPropertyValueExpression(field, propertyColumn)
1812
+ const parameter = field.scalarKind === "decimal" ? "?::numeric" : "?::text::jsonb"
1813
+ const args = [
1814
+ field.propertyId,
1815
+ field.scalarKind === "decimal" ? value.value : jsonbValue(value.value),
1816
+ ]
1817
+ addRange({ sql: `${expression} ${field.direction === "desc" ? "<" : ">"} ${parameter}`, args })
1818
+ prefix.push({ sql: `${expression} = ${parameter}`, args })
1819
+ })
1820
+ return ranges
1821
+ }
1822
+
1823
+ function isIndexPageInput(query: ObjectQuery): boolean {
1824
+ switch (query.kind) {
1825
+ case "start":
1826
+ return true
1827
+ case "filter":
1828
+ case "text":
1829
+ case "sort":
1830
+ case "traverse":
1831
+ return isIndexPageInput(query.input)
1832
+ default:
1833
+ return false
1834
+ }
1835
+ }
1836
+
1547
1837
  function compileKeysetPredicate(
1548
1838
  fields: readonly CompiledOrderField[],
1549
1839
  cursor: readonly EncodedCursorValue[],
@@ -1722,7 +2012,7 @@ function compiledPropertyValueExpression(
1722
2012
  ): string {
1723
2013
  return field.scalarKind === "decimal"
1724
2014
  ? `(${jsonTextExpression(propertyColumn)})::numeric`
1725
- : jsonValueExpression(propertyColumn)
2015
+ : `NULLIF(${jsonValueExpression(propertyColumn)}, 'null'::jsonb)`
1726
2016
  }
1727
2017
 
1728
2018
  function columnExpression(