@sixb/pg 0.1.20 → 0.1.22

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 (139) hide show
  1. package/README.md +1 -0
  2. package/dist/agents/agent-storage.d.ts +8 -2
  3. package/dist/agents/agent-storage.d.ts.map +1 -1
  4. package/dist/agents/approvals.d.ts +48 -0
  5. package/dist/agents/approvals.d.ts.map +1 -0
  6. package/dist/agents/checkpoints.d.ts +1 -0
  7. package/dist/agents/checkpoints.d.ts.map +1 -1
  8. package/dist/agents/index.d.ts +1 -0
  9. package/dist/agents/index.d.ts.map +1 -1
  10. package/dist/agents/memory.d.ts +19 -0
  11. package/dist/agents/memory.d.ts.map +1 -0
  12. package/dist/agents/messages.d.ts.map +1 -1
  13. package/dist/agents/rows.d.ts +15 -1
  14. package/dist/agents/rows.d.ts.map +1 -1
  15. package/dist/agents/runs.d.ts +3 -1
  16. package/dist/agents/runs.d.ts.map +1 -1
  17. package/dist/agents/settings.d.ts +29 -0
  18. package/dist/agents/settings.d.ts.map +1 -0
  19. package/dist/agents/threads.d.ts +10 -1
  20. package/dist/agents/threads.d.ts.map +1 -1
  21. package/dist/ai-cost-groups.d.ts.map +1 -1
  22. package/dist/auth-storage/auth-storage.d.ts +3 -2
  23. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  24. package/dist/auth-storage/sessions.d.ts +2 -1
  25. package/dist/auth-storage/sessions.d.ts.map +1 -1
  26. package/dist/auth-storage/users.d.ts +8 -4
  27. package/dist/auth-storage/users.d.ts.map +1 -1
  28. package/dist/index.d.ts +4 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +6001 -2923
  31. package/dist/index.js.map +45 -37
  32. package/dist/migrations.d.ts.map +1 -1
  33. package/dist/objects/conditional-reader.d.ts +17 -0
  34. package/dist/objects/conditional-reader.d.ts.map +1 -0
  35. package/dist/objects/query-compiler.d.ts +27 -1
  36. package/dist/objects/query-compiler.d.ts.map +1 -1
  37. package/dist/objects/query-indexes.d.ts.map +1 -1
  38. package/dist/objects/read-scope.d.ts +10 -1
  39. package/dist/objects/read-scope.d.ts.map +1 -1
  40. package/dist/objects/reader.d.ts +2 -0
  41. package/dist/objects/reader.d.ts.map +1 -1
  42. package/dist/objects/scoped-reader.d.ts +12 -0
  43. package/dist/objects/scoped-reader.d.ts.map +1 -0
  44. package/dist/objects/selected-reader.d.ts +1 -0
  45. package/dist/objects/selected-reader.d.ts.map +1 -1
  46. package/dist/objects/storage.d.ts +7 -1
  47. package/dist/objects/storage.d.ts.map +1 -1
  48. package/dist/ontology-storage/index.d.ts +2 -0
  49. package/dist/ontology-storage/index.d.ts.map +1 -1
  50. package/dist/ontology-storage/materialization-session.d.ts +6 -0
  51. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  52. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  53. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  54. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  55. package/dist/ontology-storage/replacement-plans.d.ts.map +1 -1
  56. package/dist/ontology-storage/sources.d.ts +2 -1
  57. package/dist/ontology-storage/sources.d.ts.map +1 -1
  58. package/dist/ontology-storage/unique-values.d.ts +60 -0
  59. package/dist/ontology-storage/unique-values.d.ts.map +1 -0
  60. package/dist/pg-action-run-storage.d.ts +4 -0
  61. package/dist/pg-action-run-storage.d.ts.map +1 -1
  62. package/dist/pg-activity-storage.d.ts +22 -0
  63. package/dist/pg-activity-storage.d.ts.map +1 -0
  64. package/dist/pg-ai-cost-storage.d.ts.map +1 -1
  65. package/dist/pg-ai-usage-storage.d.ts +8 -1
  66. package/dist/pg-ai-usage-storage.d.ts.map +1 -1
  67. package/dist/pg-artifact-storage.d.ts +26 -0
  68. package/dist/pg-artifact-storage.d.ts.map +1 -0
  69. package/dist/pg-execution-storage.d.ts +3 -1
  70. package/dist/pg-execution-storage.d.ts.map +1 -1
  71. package/dist/pg-workflow-run-storage.d.ts +4 -0
  72. package/dist/pg-workflow-run-storage.d.ts.map +1 -1
  73. package/dist/share-grant-storage.d.ts +2 -1
  74. package/dist/share-grant-storage.d.ts.map +1 -1
  75. package/dist/storage-errors.d.ts +2 -0
  76. package/dist/storage-errors.d.ts.map +1 -1
  77. package/package.json +3 -3
  78. package/src/agents/agent-storage.ts +11 -2
  79. package/src/agents/approvals.ts +400 -0
  80. package/src/agents/checkpoints.ts +8 -4
  81. package/src/agents/index.ts +1 -0
  82. package/src/agents/memory.ts +175 -0
  83. package/src/agents/messages.ts +14 -7
  84. package/src/agents/rows.ts +46 -1
  85. package/src/agents/runs.ts +154 -22
  86. package/src/agents/settings.ts +105 -0
  87. package/src/agents/threads.ts +379 -3
  88. package/src/ai-cost-groups.ts +11 -1
  89. package/src/auth-storage/auth-storage.ts +45 -13
  90. package/src/auth-storage/sessions.ts +20 -0
  91. package/src/auth-storage/users.ts +105 -43
  92. package/src/index.ts +10 -0
  93. package/src/migrations/063-artifacts.sql +67 -0
  94. package/src/migrations/064-drop-output-diagnostics.sql +14 -0
  95. package/src/migrations/065-plan-work-payload-statistics.sql +5 -0
  96. package/src/migrations/066-activity.sql +61 -0
  97. package/src/migrations/067-activity-timeline.sql +15 -0
  98. package/src/migrations/068-activity-interactions.sql +20 -0
  99. package/src/migrations/069-artifact-kinds.sql +4 -0
  100. package/src/migrations/070-artifact-state.sql +16 -0
  101. package/src/migrations/071-agent-thread-participants.sql +16 -0
  102. package/src/migrations/072-forked-context-checkpoints.sql +23 -0
  103. package/src/migrations/073-unique-values.sql +25 -0
  104. package/src/migrations/074-generated-media-usage.sql +21 -0
  105. package/src/migrations/075-model-call-files.sql +11 -0
  106. package/src/migrations/076-agent-memory.sql +33 -0
  107. package/src/migrations/077-artifact-threads.sql +21 -0
  108. package/src/migrations/078-agent-approvals.sql +58 -0
  109. package/src/migrations/079-agent-approval-resume.sql +20 -0
  110. package/src/migrations/080-subagent-threads.sql +145 -0
  111. package/src/migrations/081-subagent-questions.sql +6 -0
  112. package/src/migrations.ts +52 -0
  113. package/src/objects/conditional-reader.ts +44 -0
  114. package/src/objects/prepare-queries.ts +1 -1
  115. package/src/objects/query-compiler.ts +96 -33
  116. package/src/objects/query-indexes.ts +29 -10
  117. package/src/objects/read-scope.ts +140 -25
  118. package/src/objects/reader.ts +50 -6
  119. package/src/objects/scoped-reader.ts +101 -0
  120. package/src/objects/selected-reader.ts +16 -76
  121. package/src/objects/storage.ts +24 -5
  122. package/src/objects/text-count-index.ts +1 -0
  123. package/src/ontology-storage/index.ts +3 -0
  124. package/src/ontology-storage/materialization-session.ts +8 -0
  125. package/src/ontology-storage/materialization-writer.ts +76 -1
  126. package/src/ontology-storage/materializations.ts +20 -1
  127. package/src/ontology-storage/replacement-plans.ts +25 -7
  128. package/src/ontology-storage/sources.ts +37 -0
  129. package/src/ontology-storage/unique-values.ts +274 -0
  130. package/src/pg-action-run-storage.ts +23 -0
  131. package/src/pg-activity-storage.ts +486 -0
  132. package/src/pg-ai-cost-storage.ts +62 -12
  133. package/src/pg-ai-usage-storage.ts +90 -1
  134. package/src/pg-artifact-storage.ts +521 -0
  135. package/src/pg-execution-storage.ts +83 -0
  136. package/src/pg-workflow-run-storage.ts +23 -0
  137. package/src/share-grant-storage.ts +21 -0
  138. package/src/storage-errors.ts +7 -0
  139. package/src/transactions.ts +1 -1
@@ -1,7 +1,11 @@
1
- import type { CompiledSelectedObjectReadScope } from "@sixb/core/storage"
1
+ import type {
2
+ CompiledConditionalObjectReadScope,
3
+ CompiledSelectedObjectReadScope,
4
+ } from "@sixb/core/storage"
2
5
  import {
3
6
  type CompiledPgObjectQuery,
4
7
  compilePgObjectStatement,
8
+ compilePredicate,
5
9
  type PgObjectQuerySource,
6
10
  } from "./query-compiler"
7
11
 
@@ -30,16 +34,78 @@ export function compilePgSelectedObjectReadSource(
30
34
  objects: scope.objects,
31
35
  steps: scope.steps,
32
36
  })
33
- const compiled = compileSelectedReadScopeCte(scopeJson, projectId)
37
+ const source: PgSelectedObjectReadSource = {
38
+ ...scopedQuerySource(compileSelectedReadScopeCte(scopeJson, projectId)),
39
+ objectPropertyPermissionsTable: "_sixb_scope_object_permissions",
40
+ traversalProbe: { sql: "", args: [] },
41
+ }
42
+
43
+ const traversalProbe = compilePgObjectStatement(
44
+ `
45
+ SELECT COUNT(*)::bigint AS total
46
+ FROM (
47
+ SELECT 1 AS traversal_fact
48
+ FROM _sixb_scope_root_facts
49
+
50
+ UNION ALL
51
+
52
+ SELECT 1 AS traversal_fact
53
+ FROM _sixb_scope_reached_links
54
+
55
+ LIMIT ?
56
+ ) AS bounded_traversal_facts
57
+ `,
58
+ [BigInt(maxTraversalFacts) + 1n],
59
+ source
60
+ )
61
+
62
+ return Object.freeze({ ...source, traversalProbe })
63
+ }
64
+
65
+ /**
66
+ * Prepare a conditional scope as PostgreSQL relations that filter stored rows as they are read.
67
+ *
68
+ * Both relations are NOT MATERIALIZED, so PostgreSQL inlines them into each statement: its
69
+ * filters, index scans and LIMIT reach the stored tables, and the conditions become one more
70
+ * filter. An object of a conditioned type is visible while its condition matches; a link while
71
+ * each of its conditioned endpoints is a visible object.
72
+ */
73
+ export function compilePgConditionalObjectReadSource(
74
+ projectId: string,
75
+ scope: CompiledConditionalObjectReadScope
76
+ ): PgObjectQuerySource {
77
+ const filters = new Map(
78
+ scope.conditions.map((condition) => [
79
+ condition.objectTypeId,
80
+ compilePredicate(condition.predicate),
81
+ ])
82
+ )
83
+
84
+ return Object.freeze({
85
+ ...scopedQuerySource(compileConditionalReadScopeCte(projectId, filters)),
86
+ // One type's visible objects are the stored ones its own condition matches.
87
+ objectsOfType: (objectTypeId: string) => {
88
+ const filter = filters.get(objectTypeId)
89
+ return filter ? { table: "objects", filter } : { table: "objects" }
90
+ },
91
+ })
92
+ }
93
+
94
+ interface CompiledReadScopeCte {
95
+ readonly sql: string
96
+ readonly args: readonly unknown[]
97
+ }
98
+
99
+ /** Prefix every statement of a query with the scope's relations, ahead of its own arguments. */
100
+ function scopedQuerySource(compiled: CompiledReadScopeCte): PgObjectQuerySource {
34
101
  const wrapStatement = (sql: string, args: readonly unknown[] = []) => ({
35
102
  sql: `${compiled.sql}\n${sql}`,
36
103
  args: [...compiled.args, ...args],
37
104
  })
38
105
 
39
- const source: PgSelectedObjectReadSource = {
106
+ return {
40
107
  objectsTable: "_sixb_scope_objects",
41
108
  linksTable: "_sixb_scope_links",
42
- objectPropertyPermissionsTable: "_sixb_scope_object_permissions",
43
109
  wrapStatement,
44
110
  wrapQuery: (query: CompiledPgObjectQuery): CompiledPgObjectQuery => ({
45
111
  ...query,
@@ -58,34 +124,83 @@ export function compilePgSelectedObjectReadSource(
58
124
  }
59
125
  : {}),
60
126
  }),
61
- traversalProbe: { sql: "", args: [] },
62
127
  }
128
+ }
63
129
 
64
- const traversalProbe = compilePgObjectStatement(
65
- `
66
- SELECT COUNT(*)::bigint AS total
67
- FROM (
68
- SELECT 1 AS traversal_fact
69
- FROM _sixb_scope_root_facts
130
+ /** Conditions keyed by their concrete type, each compiled over that type's `properties`. */
131
+ type ConditionFilters = ReadonlyMap<
132
+ string,
133
+ { readonly sql: string; readonly args: readonly unknown[] }
134
+ >
70
135
 
71
- UNION ALL
136
+ function compileConditionalReadScopeCte(
137
+ projectId: string,
138
+ filters: ConditionFilters
139
+ ): CompiledReadScopeCte {
140
+ if (filters.size === 0) {
141
+ return {
142
+ sql: `
143
+ WITH
144
+ _sixb_scope_objects AS NOT MATERIALIZED (
145
+ SELECT * FROM objects WHERE project_id = ?
146
+ ),
147
+ _sixb_scope_links AS NOT MATERIALIZED (
148
+ SELECT * FROM links WHERE project_id = ?
149
+ )
150
+ `,
151
+ args: [projectId, projectId],
152
+ }
153
+ }
72
154
 
73
- SELECT 1 AS traversal_fact
74
- FROM _sixb_scope_reached_links
155
+ const conditionedTypeArgs = [...filters.keys()]
156
+ const conditionedTypes = conditionedTypeArgs.map(() => "?::text").join(", ")
75
157
 
76
- LIMIT ?
77
- ) AS bounded_traversal_facts
78
- `,
79
- [BigInt(maxTraversalFacts) + 1n],
80
- source
81
- )
158
+ const matches: string[] = []
159
+ const matchArgs: unknown[] = []
160
+ for (const [objectTypeId, filter] of filters) {
161
+ matches.push(`(object_type_id = ?::text AND ${filter.sql})`)
162
+ matchArgs.push(objectTypeId, ...filter.args)
163
+ }
82
164
 
83
- return Object.freeze({ ...source, traversalProbe })
84
- }
165
+ const visibleEndpoint = (end: "source" | "target") => `
166
+ (
167
+ stored.${end}_type_id NOT IN (${conditionedTypes})
168
+ OR EXISTS (
169
+ SELECT 1
170
+ FROM _sixb_scope_objects AS endpoint
171
+ WHERE endpoint.project_id = stored.project_id
172
+ AND endpoint.object_type_id = stored.${end}_type_id
173
+ AND endpoint.primary_id = stored.${end}_id
174
+ )
175
+ )
176
+ `
85
177
 
86
- interface CompiledReadScopeCte {
87
- readonly sql: string
88
- readonly args: readonly unknown[]
178
+ return {
179
+ sql: `
180
+ WITH
181
+ _sixb_scope_objects AS NOT MATERIALIZED (
182
+ SELECT *
183
+ FROM objects
184
+ WHERE project_id = ?
185
+ AND (object_type_id NOT IN (${conditionedTypes}) OR ${matches.join(" OR ")})
186
+ ),
187
+ _sixb_scope_links AS NOT MATERIALIZED (
188
+ SELECT stored.*
189
+ FROM links AS stored
190
+ WHERE stored.project_id = ?
191
+ AND ${visibleEndpoint("source")}
192
+ AND ${visibleEndpoint("target")}
193
+ )
194
+ `,
195
+ args: [
196
+ projectId,
197
+ ...conditionedTypeArgs,
198
+ ...matchArgs,
199
+ projectId,
200
+ ...conditionedTypeArgs,
201
+ ...conditionedTypeArgs,
202
+ ],
203
+ }
89
204
  }
90
205
 
91
206
  function compileSelectedReadScopeCte(scopeJson: string, projectId: string): CompiledReadScopeCte {
@@ -30,6 +30,7 @@ import {
30
30
  compilePgObjectStatement,
31
31
  DEFAULT_OBJECT_QUERY_SOURCE,
32
32
  type PgObjectQuerySource,
33
+ pgObjectsOfType,
33
34
  } from "./query-compiler"
34
35
  import { findPgSortIndexBound } from "./query-indexes"
35
36
  import type { PgSelectedObjectReadSource } from "./read-scope"
@@ -258,13 +259,14 @@ export class PgObjectReader {
258
259
  }): Promise<ObjectRow | null> {
259
260
  const sql = this.sql
260
261
  const source = this.source
262
+ const objects = pgObjectsOfType(source, params.objectTypeId)
261
263
  const statement = compilePgObjectStatement(
262
264
  `
263
265
  SELECT *
264
- FROM ${source.objectsTable}
265
- WHERE project_id = ? AND object_type_id = ? AND primary_id = ?
266
+ FROM ${objects.table}
267
+ WHERE project_id = ? AND object_type_id = ? AND primary_id = ? ${objects.filter}
266
268
  `,
267
- [params.projectId, params.objectTypeId, params.primaryId],
269
+ [params.projectId, params.objectTypeId, params.primaryId, ...objects.args],
268
270
  source
269
271
  )
270
272
  const [row] = await sql.unsafe<ObjectDatabaseRow[]>(
@@ -275,6 +277,44 @@ export class PgObjectReader {
275
277
  return row ? rowToObject(row) : null
276
278
  }
277
279
 
280
+ /** On a selected source, the holder must be in scope and selected for the property. */
281
+ async getByUniqueValue(
282
+ params: Parameters<ObjectReadStorage["getByUniqueValue"]>[0]
283
+ ): Promise<ObjectRow | null> {
284
+ const source = this.source
285
+ const selected = "objectPropertyPermissionsTable" in source
286
+ const permitted = selected
287
+ ? `AND EXISTS (
288
+ SELECT 1 FROM ${source.objectPropertyPermissionsTable} AS permitted
289
+ WHERE permitted.project_id = objects.project_id
290
+ AND permitted.object_type_id = objects.object_type_id
291
+ AND permitted.primary_id = objects.primary_id
292
+ AND permitted.property_id = held.property_id
293
+ )`
294
+ : ""
295
+ const statement = compilePgObjectStatement(
296
+ `
297
+ SELECT objects.*
298
+ FROM object_unique_values AS held
299
+ JOIN ${source.objectsTable} AS objects
300
+ ON objects.project_id = held.project_id
301
+ AND objects.object_type_id = held.object_type_id
302
+ AND objects.primary_id = held.primary_id
303
+ WHERE held.project_id = ? AND held.object_type_id = ? AND held.property_id = ?
304
+ AND held.value_hash = sha256(convert_to(?, 'UTF8')) AND held.value_key = ?
305
+ ${permitted}
306
+ `,
307
+ [params.projectId, params.objectTypeId, params.propertyId, params.valueKey, params.valueKey],
308
+ source
309
+ )
310
+ const [row] = await this.sql.unsafe<ObjectDatabaseRow[]>(
311
+ statement.sql,
312
+ statement.args as SqlParameter[]
313
+ )
314
+
315
+ return row ? rowToObject(row) : null
316
+ }
317
+
278
318
  async selectsObjectProperties(
279
319
  params: Parameters<ObjectReadStorage["selectsObjectProperties"]>[0]
280
320
  ): Promise<readonly boolean[]> {
@@ -592,11 +632,15 @@ export class PgObjectReader {
592
632
  const order = params.order ?? "desc"
593
633
  const orderColumn =
594
634
  orderBy === "primaryId" ? "primary_id" : orderBy === "createdAt" ? "created_at" : "updated_at"
635
+ const typed =
636
+ typeof params.objectTypeId === "string"
637
+ ? pgObjectsOfType(source, params.objectTypeId)
638
+ : { table: source.objectsTable, filter: "", args: [] }
595
639
  const filters = ["project_id = ?"]
596
640
  const args: unknown[] = [params.projectId]
597
641
  if (typeof params.objectTypeId === "string") {
598
- filters.push("object_type_id = ?")
599
- args.push(params.objectTypeId)
642
+ filters.push(`object_type_id = ? ${typed.filter}`)
643
+ args.push(params.objectTypeId, ...typed.args)
600
644
  } else if (params.objectTypeId !== undefined) {
601
645
  filters.push("object_type_id IN (SELECT jsonb_array_elements_text(?::text::jsonb))")
602
646
  args.push(JSON.stringify(params.objectTypeId))
@@ -626,7 +670,7 @@ export class PgObjectReader {
626
670
  args.push(params.createdBefore)
627
671
  }
628
672
 
629
- const objectsTable = source.objectsTable
673
+ const objectsTable = typed.table
630
674
  const whereSql = filters.join(" AND ")
631
675
  const countStatement = compilePgObjectStatement(
632
676
  `SELECT COUNT(*)::int AS total FROM ${objectsTable} WHERE ${whereSql}`,
@@ -0,0 +1,101 @@
1
+ import type { ObjectQuery } from "@sixb/core"
2
+ import type {
3
+ ObjectQueryCapabilities,
4
+ ObjectReadExecutionLimits,
5
+ ObjectReadStorage,
6
+ } from "@sixb/core/storage"
7
+ import {
8
+ assertObjectReaderProject,
9
+ assertObjectReadFacetCount,
10
+ assertObjectReadOutputWithinLimit,
11
+ } from "@sixb/core/storage"
12
+ import { assertLinkQueryLimit, type PgObjectReader } from "./reader"
13
+
14
+ /** Run one terminal on a reader over the scope's source; a query terminal passes its query. */
15
+ export type PgScopedRead = <T>(
16
+ run: (reader: PgObjectReader) => Promise<T>,
17
+ query?: ObjectQuery
18
+ ) => Promise<T>
19
+
20
+ /** Answer every terminal through one scoped source; the scope decides how each read runs. */
21
+ export function createPgScopedReader(
22
+ params: {
23
+ projectId: string
24
+ read: PgScopedRead
25
+ limits?: ObjectReadExecutionLimits
26
+ },
27
+ queryCapabilities: () => ObjectQueryCapabilities
28
+ ): ObjectReadStorage {
29
+ const { projectId, limits } = params
30
+ const assertProject = (actualProjectId: string): void =>
31
+ assertObjectReaderProject(projectId, actualProjectId)
32
+ const read = async <T>(
33
+ run: (reader: PgObjectReader) => Promise<T>,
34
+ query?: ObjectQuery
35
+ ): Promise<T> => {
36
+ const value = await params.read(run, query)
37
+ if (limits) assertObjectReadOutputWithinLimit(value, limits)
38
+ return value
39
+ }
40
+ const readMap = async <TKey, TValue>(
41
+ run: (reader: PgObjectReader) => Promise<Map<TKey, TValue>>
42
+ ): Promise<Map<TKey, TValue>> => {
43
+ const value = await params.read(run)
44
+ if (limits) assertObjectReadOutputWithinLimit([...value.entries()], limits)
45
+ return value
46
+ }
47
+
48
+ return Object.freeze({
49
+ queryCapabilities,
50
+ queryObjects: async (input) => {
51
+ assertProject(input.projectId)
52
+ return read((reader) => reader.queryObjects(input), input.query)
53
+ },
54
+ countObjects: async (input) => {
55
+ assertProject(input.projectId)
56
+ return read((reader) => reader.countObjects(input), input.query)
57
+ },
58
+ existsObjects: async (input) => {
59
+ assertProject(input.projectId)
60
+ return read((reader) => reader.existsObjects(input), input.query)
61
+ },
62
+ facetObjects: async (input) => {
63
+ assertProject(input.projectId)
64
+ assertObjectReadFacetCount(input.facets.length)
65
+ return read((reader) => reader.facetObjects(input), input.query)
66
+ },
67
+ getByPrimaryId: async (input) => {
68
+ assertProject(input.projectId)
69
+ return read((reader) => reader.getByPrimaryId(input))
70
+ },
71
+ getByUniqueValue: async (input) => {
72
+ assertProject(input.projectId)
73
+ return read((reader) => reader.getByUniqueValue(input))
74
+ },
75
+ selectsObjectProperties: async (input) => {
76
+ assertProject(input.projectId)
77
+ return read((reader) => reader.selectsObjectProperties(input))
78
+ },
79
+ listLinks: async (input) => {
80
+ assertProject(input.projectId)
81
+ return read((reader) => reader.listLinks(input))
82
+ },
83
+ getByPrimaryIdBatch: async (input) => {
84
+ assertProject(input.projectId)
85
+ return readMap((reader) => reader.getByPrimaryIdBatch(input))
86
+ },
87
+ listLinksBatch: async (input) => {
88
+ assertProject(input.projectId)
89
+ return readMap((reader) => reader.listLinksBatch(input))
90
+ },
91
+ queryLinks: async (input) => {
92
+ assertProject(input.projectId)
93
+ assertLinkQueryLimit(input.limit)
94
+ return read((reader) => reader.queryLinks(input))
95
+ },
96
+ list: async (input) => {
97
+ assertProject(input.projectId)
98
+ return read((reader) => reader.list(input))
99
+ },
100
+ } satisfies ObjectReadStorage)
101
+ }
@@ -4,19 +4,14 @@ import type {
4
4
  ObjectReadExecutionLimits,
5
5
  ObjectReadStorage,
6
6
  } from "@sixb/core/storage"
7
- import {
8
- assertObjectReaderProject,
9
- assertObjectReadFacetCount,
10
- assertObjectReadOutputWithinLimit,
11
- ObjectReadLimitExceededError,
12
- snapshotObjectReadExecutionLimits,
13
- } from "@sixb/core/storage"
7
+ import { ObjectReadLimitExceededError, snapshotObjectReadExecutionLimits } from "@sixb/core/storage"
14
8
  import type { SQLClient, SqlParameter } from "../pg-client"
15
9
  import { type PgStoreClient, runPgRepeatableReadTransaction } from "../transactions"
16
10
  import { compilePgSelectedObjectReadSource, type PgSelectedObjectReadSource } from "./read-scope"
11
+ import { PgObjectReader } from "./reader"
12
+ import { createPgScopedReader } from "./scoped-reader"
17
13
 
18
- import { assertLinkQueryLimit, PgObjectReader } from "./reader"
19
-
14
+ /** Keep the traversal probe and every terminal statement on one repeatable-read snapshot. */
20
15
  export function createPgSelectedReader(
21
16
  sql: PgStoreClient,
22
17
  params: {
@@ -33,74 +28,19 @@ export function createPgSelectedReader(
33
28
  params.scope,
34
29
  limits.maxTraversalFacts
35
30
  )
36
- const assertProject = (actualProjectId: string): void =>
37
- assertObjectReaderProject(projectId, actualProjectId)
38
- const read = <T>(run: (reader: PgObjectReader) => Promise<T>): Promise<T> =>
39
- runPgRepeatableReadTransaction(sql, async (tx) => {
40
- await assertTraversalBudget(tx, source, limits.maxTraversalFacts)
41
- const value = await run(new PgObjectReader(tx, source))
42
- assertObjectReadOutputWithinLimit(value, limits)
43
- return value
44
- })
45
- const readMap = <TKey, TValue>(
46
- run: (reader: PgObjectReader) => Promise<Map<TKey, TValue>>
47
- ): Promise<Map<TKey, TValue>> =>
48
- runPgRepeatableReadTransaction(sql, async (tx) => {
49
- await assertTraversalBudget(tx, source, limits.maxTraversalFacts)
50
- const value = await run(new PgObjectReader(tx, source))
51
- assertObjectReadOutputWithinLimit([...value.entries()], limits)
52
- return value
53
- })
54
31
 
55
- return Object.freeze({
56
- queryCapabilities,
57
- queryObjects: async (input) => {
58
- assertProject(input.projectId)
59
- return read((reader) => reader.queryObjects(input))
60
- },
61
- countObjects: async (input) => {
62
- assertProject(input.projectId)
63
- return read((reader) => reader.countObjects(input))
64
- },
65
- existsObjects: async (input) => {
66
- assertProject(input.projectId)
67
- return read((reader) => reader.existsObjects(input))
68
- },
69
- facetObjects: async (input) => {
70
- assertProject(input.projectId)
71
- assertObjectReadFacetCount(input.facets.length)
72
- return read((reader) => reader.facetObjects(input))
73
- },
74
- getByPrimaryId: async (input) => {
75
- assertProject(input.projectId)
76
- return read((reader) => reader.getByPrimaryId(input))
77
- },
78
- selectsObjectProperties: async (input) => {
79
- assertProject(input.projectId)
80
- return read((reader) => reader.selectsObjectProperties(input))
81
- },
82
- listLinks: async (input) => {
83
- assertProject(input.projectId)
84
- return read((reader) => reader.listLinks(input))
85
- },
86
- getByPrimaryIdBatch: async (input) => {
87
- assertProject(input.projectId)
88
- return readMap((reader) => reader.getByPrimaryIdBatch(input))
89
- },
90
- listLinksBatch: async (input) => {
91
- assertProject(input.projectId)
92
- return readMap((reader) => reader.listLinksBatch(input))
93
- },
94
- queryLinks: async (input) => {
95
- assertProject(input.projectId)
96
- assertLinkQueryLimit(input.limit)
97
- return read((reader) => reader.queryLinks(input))
98
- },
99
- list: async (input) => {
100
- assertProject(input.projectId)
101
- return read((reader) => reader.list(input))
102
- },
103
- } satisfies ObjectReadStorage)
32
+ return createPgScopedReader(
33
+ {
34
+ projectId,
35
+ read: (run) =>
36
+ runPgRepeatableReadTransaction(sql, async (tx) => {
37
+ await assertTraversalBudget(tx, source, limits.maxTraversalFacts)
38
+ return run(new PgObjectReader(tx, source))
39
+ }),
40
+ limits,
41
+ },
42
+ queryCapabilities
43
+ )
104
44
  }
105
45
 
106
46
  async function assertTraversalBudget(
@@ -1,5 +1,6 @@
1
1
  import { hasVectorProfile } from "@sixb/core/internal/query"
2
2
  import type {
3
+ CompiledConditionalObjectReadScope,
3
4
  CompiledSelectedObjectReadScope,
4
5
  CountObjectsInput,
5
6
  CountObjectsResult,
@@ -28,6 +29,7 @@ import {
28
29
  runPgRepeatableReadTransaction,
29
30
  } from "../transactions"
30
31
  import { PgAggregateAdmission } from "./aggregate-admission"
32
+ import { createPgConditionalReader } from "./conditional-reader"
31
33
  import { DEFAULT_OBJECT_QUERY_SOURCE } from "./query-compiler"
32
34
  import { assertLinkQueryLimit, PgObjectReader } from "./reader"
33
35
  import {
@@ -71,6 +73,7 @@ const PG_OBJECT_QUERY_CAPABILITIES: ObjectQueryCapabilities = {
71
73
  in: true,
72
74
  exists: true,
73
75
  contains: true,
76
+ constant: true,
74
77
  },
75
78
  sortKinds: {
76
79
  property: true,
@@ -110,17 +113,17 @@ const PG_OBJECT_QUERY_CAPABILITIES: ObjectQueryCapabilities = {
110
113
 
111
114
  /** PostgreSQL object provider; interactive reads share one source-aware reader. */
112
115
  export class PgObjectStorage implements ObjectStorage {
116
+ private readonly aggregateAdmission?: PgAggregateAdmission
113
117
  private readonly reader: PgObjectReader
114
118
 
115
119
  constructor(
116
120
  private readonly sql: PgStoreClient,
117
121
  maxConcurrentAggregates = 1
118
122
  ) {
119
- this.reader = new PgObjectReader(
120
- sql,
121
- DEFAULT_OBJECT_QUERY_SOURCE,
122
- canStartPgTransaction(sql) ? new PgAggregateAdmission(maxConcurrentAggregates) : undefined
123
- )
123
+ this.aggregateAdmission = canStartPgTransaction(sql)
124
+ ? new PgAggregateAdmission(maxConcurrentAggregates)
125
+ : undefined
126
+ this.reader = new PgObjectReader(sql, DEFAULT_OBJECT_QUERY_SOURCE, this.aggregateAdmission)
124
127
  }
125
128
 
126
129
  queryCapabilities(): ObjectQueryCapabilities {
@@ -135,6 +138,16 @@ export class PgObjectStorage implements ObjectStorage {
135
138
  return createPgSelectedReader(this.sql, params, () => this.queryCapabilities())
136
139
  }
137
140
 
141
+ createConditionalReadScope(params: {
142
+ projectId: string
143
+ scope: CompiledConditionalObjectReadScope
144
+ }): ObjectReadStorage {
145
+ return createPgConditionalReader(this.sql, params, {
146
+ aggregateAdmission: this.aggregateAdmission,
147
+ queryCapabilities: () => this.queryCapabilities(),
148
+ })
149
+ }
150
+
138
151
  async queryObjects(params: QueryObjectsInput): Promise<QueryObjectsResult> {
139
152
  if (hasVectorProfile(params.query))
140
153
  return runPgRepeatableReadTransaction(this.sql, (tx) =>
@@ -175,6 +188,12 @@ export class PgObjectStorage implements ObjectStorage {
175
188
  return this.reader.getByPrimaryId(params)
176
189
  }
177
190
 
191
+ async getByUniqueValue(
192
+ params: Parameters<ObjectReadStorage["getByUniqueValue"]>[0]
193
+ ): Promise<ObjectRow | null> {
194
+ return this.reader.getByUniqueValue(params)
195
+ }
196
+
178
197
  async selectsObjectProperties(
179
198
  params: Parameters<ObjectReadStorage["selectsObjectProperties"]>[0]
180
199
  ): Promise<readonly boolean[]> {
@@ -401,6 +401,7 @@ function stringPredicate(
401
401
  ? `(${items.join(predicate.op === "and" ? " AND " : " OR ")})`
402
402
  : undefined
403
403
  }
404
+ if (predicate.op === "constant") return predicate.value ? "true" : "false"
404
405
  if ((predicate.op !== "eq" && predicate.op !== "in") || predicate.scalarKind === "decimal")
405
406
  return undefined
406
407
  const column = columns.get(predicate.propertyId)
@@ -7,6 +7,7 @@ import { PgOntologyOutboxStorage } from "./outbox"
7
7
  import { PgOntologyReplacementPlanStorage } from "./replacement-plans"
8
8
  import type { PgRootOperation } from "./shared"
9
9
  import { PgOntologySourceStorage } from "./sources"
10
+ import { PgOntologyUniqueValueStorage } from "./unique-values"
10
11
  import { PgVectorIndexingStorage } from "./vector-indexing"
11
12
  import { PgOntologyVectorStorage } from "./vectors"
12
13
 
@@ -18,6 +19,7 @@ export class PgOntologyStorage implements OntologyStorage {
18
19
  readonly outbox: PgOntologyOutboxStorage
19
20
  readonly vectorIndexing: PgVectorIndexingStorage
20
21
  readonly vectors: PgOntologyVectorStorage
22
+ readonly uniqueValues: PgOntologyUniqueValueStorage
21
23
 
22
24
  constructor(input: {
23
25
  readonly sql: SQLClient
@@ -32,6 +34,7 @@ export class PgOntologyStorage implements OntologyStorage {
32
34
  )
33
35
  this.replacementPlans = new PgOntologyReplacementPlanStorage(input.sql)
34
36
  this.outbox = new PgOntologyOutboxStorage(input.runRootOperation)
37
+ this.uniqueValues = new PgOntologyUniqueValueStorage(input.runRootOperation)
35
38
  this.vectorIndexing = new PgVectorIndexingStorage(
36
39
  input.sql,
37
40
  input.runRootOperation,
@@ -35,6 +35,9 @@ export class PgMaterializationSessionState extends ProviderMaterializationSessio
35
35
  stagedWorkCount = 0
36
36
  changedObjects = 0
37
37
  changedLinks = 0
38
+ changedUniqueValues = 0
39
+ /** What unique value items this session staged, known without reading its work back. */
40
+ readonly uniqueWork = { releases: false, claims: false }
38
41
  workAnalyzed = false
39
42
  /** Work staged on the session lives in a temp table; a plan-bound session reads its plan's. */
40
43
  readonly workTable: string
@@ -156,6 +159,11 @@ export class PgMaterializationSessions {
156
159
  throw error
157
160
  }
158
161
  session.stagedWorkCount += records.length
162
+ for (const { record } of records) {
163
+ if (record.kind !== "plan") continue
164
+ if (record.item.kind === "unique-value-release") session.uniqueWork.releases = true
165
+ if (record.item.kind === "unique-value-claim") session.uniqueWork.claims = true
166
+ }
159
167
  }
160
168
 
161
169
  /**