@sixb/pg 0.1.21 → 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 (110) 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 +6 -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/users.d.ts +8 -4
  25. package/dist/auth-storage/users.d.ts.map +1 -1
  26. package/dist/index.js +3720 -1594
  27. package/dist/index.js.map +37 -31
  28. package/dist/migrations.d.ts.map +1 -1
  29. package/dist/objects/conditional-reader.d.ts +17 -0
  30. package/dist/objects/conditional-reader.d.ts.map +1 -0
  31. package/dist/objects/query-compiler.d.ts +27 -1
  32. package/dist/objects/query-compiler.d.ts.map +1 -1
  33. package/dist/objects/query-indexes.d.ts.map +1 -1
  34. package/dist/objects/read-scope.d.ts +10 -1
  35. package/dist/objects/read-scope.d.ts.map +1 -1
  36. package/dist/objects/reader.d.ts +2 -0
  37. package/dist/objects/reader.d.ts.map +1 -1
  38. package/dist/objects/scoped-reader.d.ts +12 -0
  39. package/dist/objects/scoped-reader.d.ts.map +1 -0
  40. package/dist/objects/selected-reader.d.ts +1 -0
  41. package/dist/objects/selected-reader.d.ts.map +1 -1
  42. package/dist/objects/storage.d.ts +7 -1
  43. package/dist/objects/storage.d.ts.map +1 -1
  44. package/dist/ontology-storage/index.d.ts +2 -0
  45. package/dist/ontology-storage/index.d.ts.map +1 -1
  46. package/dist/ontology-storage/materialization-session.d.ts +6 -0
  47. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  48. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  49. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  50. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  51. package/dist/ontology-storage/unique-values.d.ts +60 -0
  52. package/dist/ontology-storage/unique-values.d.ts.map +1 -0
  53. package/dist/pg-ai-cost-storage.d.ts.map +1 -1
  54. package/dist/pg-ai-usage-storage.d.ts +8 -1
  55. package/dist/pg-ai-usage-storage.d.ts.map +1 -1
  56. package/dist/pg-artifact-storage.d.ts +8 -1
  57. package/dist/pg-artifact-storage.d.ts.map +1 -1
  58. package/dist/pg-execution-storage.d.ts +2 -1
  59. package/dist/pg-execution-storage.d.ts.map +1 -1
  60. package/dist/storage-errors.d.ts +2 -0
  61. package/dist/storage-errors.d.ts.map +1 -1
  62. package/package.json +3 -3
  63. package/src/agents/agent-storage.ts +11 -2
  64. package/src/agents/approvals.ts +400 -0
  65. package/src/agents/checkpoints.ts +8 -4
  66. package/src/agents/index.ts +1 -0
  67. package/src/agents/memory.ts +175 -0
  68. package/src/agents/messages.ts +14 -7
  69. package/src/agents/rows.ts +46 -1
  70. package/src/agents/runs.ts +154 -22
  71. package/src/agents/settings.ts +105 -0
  72. package/src/agents/threads.ts +356 -3
  73. package/src/ai-cost-groups.ts +11 -1
  74. package/src/auth-storage/auth-storage.ts +45 -13
  75. package/src/auth-storage/users.ts +105 -43
  76. package/src/migrations/069-artifact-kinds.sql +4 -0
  77. package/src/migrations/070-artifact-state.sql +16 -0
  78. package/src/migrations/071-agent-thread-participants.sql +16 -0
  79. package/src/migrations/072-forked-context-checkpoints.sql +23 -0
  80. package/src/migrations/073-unique-values.sql +25 -0
  81. package/src/migrations/074-generated-media-usage.sql +21 -0
  82. package/src/migrations/075-model-call-files.sql +11 -0
  83. package/src/migrations/076-agent-memory.sql +33 -0
  84. package/src/migrations/077-artifact-threads.sql +21 -0
  85. package/src/migrations/078-agent-approvals.sql +58 -0
  86. package/src/migrations/079-agent-approval-resume.sql +20 -0
  87. package/src/migrations/080-subagent-threads.sql +145 -0
  88. package/src/migrations/081-subagent-questions.sql +6 -0
  89. package/src/migrations.ts +34 -0
  90. package/src/objects/conditional-reader.ts +44 -0
  91. package/src/objects/prepare-queries.ts +1 -1
  92. package/src/objects/query-compiler.ts +96 -33
  93. package/src/objects/query-indexes.ts +29 -10
  94. package/src/objects/read-scope.ts +140 -25
  95. package/src/objects/reader.ts +50 -6
  96. package/src/objects/scoped-reader.ts +101 -0
  97. package/src/objects/selected-reader.ts +16 -76
  98. package/src/objects/storage.ts +24 -5
  99. package/src/objects/text-count-index.ts +1 -0
  100. package/src/ontology-storage/index.ts +3 -0
  101. package/src/ontology-storage/materialization-session.ts +8 -0
  102. package/src/ontology-storage/materialization-writer.ts +76 -1
  103. package/src/ontology-storage/materializations.ts +20 -1
  104. package/src/ontology-storage/unique-values.ts +274 -0
  105. package/src/pg-ai-cost-storage.ts +62 -12
  106. package/src/pg-ai-usage-storage.ts +90 -1
  107. package/src/pg-artifact-storage.ts +205 -9
  108. package/src/pg-execution-storage.ts +31 -0
  109. package/src/storage-errors.ts +7 -0
  110. package/src/transactions.ts +1 -1
@@ -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
  /**
@@ -16,6 +16,7 @@ import {
16
16
  import type { MaterializationPlanWorkItem } from "@sixb/core/storage"
17
17
  import type postgres from "postgres"
18
18
  import type { SQL, SQLClient } from "../pg-client"
19
+ import { claimPgUniqueValues, heldValue } from "./unique-values"
19
20
 
20
21
  export interface PgStagedPlanTarget {
21
22
  /** The work table and the id its rows carry: a session's temp table, or a durable plan. */
@@ -24,11 +25,19 @@ export interface PgStagedPlanTarget {
24
25
  readonly projectId: string
25
26
  readonly commitId: string
26
27
  readonly committedAt: string
28
+ /** Whether unique value items were staged; probed in the work table when unknown. */
29
+ readonly uniqueWork?: PgUniqueWork
30
+ }
31
+
32
+ export interface PgUniqueWork {
33
+ readonly releases: boolean
34
+ readonly claims: boolean
27
35
  }
28
36
 
29
37
  export interface PgAppliedPlan {
30
38
  readonly objectWrites: number
31
39
  readonly linkWrites: number
40
+ readonly uniqueValueWrites: number
32
41
  readonly eventCount: number
33
42
  }
34
43
 
@@ -55,13 +64,32 @@ export class PgMaterializationWriter {
55
64
  await this.applyObjectOverrides(target)
56
65
  await this.applyLinkOverrides(target)
57
66
  await this.applyPoints(target)
67
+ const uniqueWork = target.uniqueWork ?? (await this.stagedUniqueWork(target))
68
+ if (uniqueWork.releases) await this.releaseUniqueValues(target)
58
69
  let linkWrites = await this.deleteLinks(target)
59
70
  let objectWrites = await this.deleteObjects(target)
60
71
  objectWrites += await this.upsertObjects(target)
61
72
  linkWrites += await this.upsertLinks(target)
73
+ const uniqueValueWrites = uniqueWork.claims ? await this.claimUniqueValues(target) : 0
62
74
  const eventCount = await this.writeOutbox(target)
63
75
  await this.writeTouches(target)
64
- return { objectWrites, linkWrites, eventCount }
76
+ return { objectWrites, linkWrites, uniqueValueWrites, eventCount }
77
+ }
78
+
79
+ /** Two probes of the work table's lane index: a commit with no unique values runs nothing else. */
80
+ private async stagedUniqueWork(target: PgStagedPlanTarget): Promise<PgUniqueWork> {
81
+ const probe = (kind: "unique-value-release" | "unique-value-claim") => this.sql`
82
+ EXISTS (
83
+ SELECT 1 FROM ${this.sql(target.workTable)}
84
+ WHERE work_id = ${target.workId} AND lane = 'apply'
85
+ AND major_order = ${materializationApplyPhase(kind)}
86
+ AND minor_order = ${materializationPlanKindRank(kind)}
87
+ )
88
+ `
89
+ const [row] = await this.sql<{ readonly releases: boolean; readonly claims: boolean }[]>`
90
+ SELECT ${probe("unique-value-release")} AS releases, ${probe("unique-value-claim")} AS claims
91
+ `
92
+ return { releases: row?.releases ?? false, claims: row?.claims ?? false }
65
93
  }
66
94
 
67
95
  /** The staged values of one plan item kind, as `value`. */
@@ -330,6 +358,51 @@ export class PgMaterializationWriter {
330
358
  )
331
359
  }
332
360
 
361
+ /** The staged unique values of one item kind, as the columns their table keys them by. */
362
+ private stagedUniqueValues(
363
+ target: PgStagedPlanTarget,
364
+ kind: "unique-value-release" | "unique-value-claim"
365
+ ) {
366
+ return this.sql`
367
+ SELECT value->>'objectTypeId' AS object_type_id, value->>'propertyId' AS property_id,
368
+ value->>'valueKey' AS value_key, value->>'primaryId' AS primary_id,
369
+ value->>'keyKind' AS key_kind, (value->>'keyVersion')::integer AS key_version
370
+ FROM (${this.staged(target, kind)}) AS staged
371
+ `
372
+ }
373
+
374
+ /** A value whose row already names another object, or none, is simply no longer this one's. */
375
+ private async releaseUniqueValues(target: PgStagedPlanTarget): Promise<void> {
376
+ await this.sql`
377
+ WITH candidates AS MATERIALIZED (
378
+ ${this.stagedUniqueValues(target, "unique-value-release")}
379
+ ), released AS (
380
+ SELECT candidates.object_type_id, candidates.property_id, held.value_hash
381
+ FROM candidates
382
+ CROSS JOIN LATERAL (
383
+ ${heldValue(this.sql, target.projectId)}
384
+ AND stored.primary_id = candidates.primary_id
385
+ LIMIT 1
386
+ ) AS held
387
+ )
388
+ DELETE FROM object_unique_values AS stored USING released
389
+ WHERE stored.project_id = ${target.projectId}
390
+ AND stored.object_type_id = released.object_type_id
391
+ AND stored.property_id = released.property_id
392
+ AND stored.value_hash = released.value_hash
393
+ `
394
+ }
395
+
396
+ /** Runs once every release of the commit has applied, so values can move between objects. */
397
+ private async claimUniqueValues(target: PgStagedPlanTarget): Promise<number> {
398
+ return claimPgUniqueValues(
399
+ this.sql,
400
+ target.projectId,
401
+ this.stagedUniqueValues(target, "unique-value-claim"),
402
+ { requirePrepared: true }
403
+ )
404
+ }
405
+
333
406
  private async deleteLinks(target: PgStagedPlanTarget): Promise<number> {
334
407
  const candidates = this.staged(target, "link-delete")
335
408
  return this.write(
@@ -586,6 +659,8 @@ export class PgMaterializationWriter {
586
659
  END AS ref
587
660
  ) AS entity
588
661
  WHERE work.work_id = ${target.workId} AND work.lane = 'apply'
662
+ -- Unique values are no plan input: a stale claim fails when it applies.
663
+ AND entity.kind IS NOT NULL
589
664
  `
590
665
  }
591
666
 
@@ -63,6 +63,7 @@ import {
63
63
  toIsoString,
64
64
  } from "./shared"
65
65
  import { activateSourceRoots } from "./source-roots"
66
+ import { pgUniqueHolders } from "./unique-values"
66
67
 
67
68
  interface TelemetrySummaryRow {
68
69
  readonly classified_points: number | string
@@ -203,6 +204,17 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
203
204
  yield { objects: [], links: [], linkScopes: [], points: stored }
204
205
  }
205
206
  }
207
+ const uniqueValues = request.uniqueValues ?? []
208
+ for (let offset = 0; offset < uniqueValues.length; offset += input.pageRows) {
209
+ const uniqueHolders = await pgUniqueHolders(
210
+ this.sql,
211
+ session.header.commit.projectId,
212
+ uniqueValues.slice(offset, offset + input.pageRows)
213
+ )
214
+ if (uniqueHolders.length > 0) {
215
+ yield { objects: [], links: [], linkScopes: [], points: [], uniqueHolders }
216
+ }
217
+ }
206
218
  }
207
219
  }
208
220
 
@@ -238,9 +250,11 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
238
250
  projectId: commit.projectId,
239
251
  commitId: commit.id,
240
252
  committedAt: commit.committedAt,
253
+ ...(session.plan ? {} : { uniqueWork: session.uniqueWork }),
241
254
  })
242
255
  session.changedObjects = applied.objectWrites
243
256
  session.changedLinks = applied.linkWrites
257
+ session.changedUniqueValues = applied.uniqueValueWrites
244
258
  session.appliedEventCount = applied.eventCount
245
259
  return { eventCount: applied.eventCount }
246
260
  }
@@ -256,6 +270,11 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
256
270
  // With empty/stale statistics, a link lookup can scan every edge sharing its target.
257
271
  await refreshMaterializedStatistics(this.sql, "objects", session.changedObjects)
258
272
  await refreshMaterializedStatistics(this.sql, "links", session.changedLinks)
273
+ await refreshMaterializedStatistics(
274
+ this.sql,
275
+ "object_unique_values",
276
+ session.changedUniqueValues
277
+ )
259
278
  const record = await this.insertCommit(session.header, input)
260
279
  await this.sessions.release(session)
261
280
  return { commit: record }
@@ -743,7 +762,7 @@ function databaseCount(value: number | string | undefined): number {
743
762
  * count can still include a previous transaction's manually analyzed writes at this point. */
744
763
  async function refreshMaterializedStatistics(
745
764
  sql: SQLClient,
746
- table: "objects" | "links",
765
+ table: "objects" | "links" | "object_unique_values",
747
766
  changed: number
748
767
  ): Promise<void> {
749
768
  if (changed < 1_000) return