@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.
- package/README.md +1 -0
- package/dist/agents/agent-storage.d.ts +8 -2
- package/dist/agents/agent-storage.d.ts.map +1 -1
- package/dist/agents/approvals.d.ts +48 -0
- package/dist/agents/approvals.d.ts.map +1 -0
- package/dist/agents/checkpoints.d.ts +1 -0
- package/dist/agents/checkpoints.d.ts.map +1 -1
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/memory.d.ts +19 -0
- package/dist/agents/memory.d.ts.map +1 -0
- package/dist/agents/messages.d.ts.map +1 -1
- package/dist/agents/rows.d.ts +15 -1
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts +3 -1
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/agents/settings.d.ts +29 -0
- package/dist/agents/settings.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +6 -1
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/ai-cost-groups.d.ts.map +1 -1
- package/dist/auth-storage/auth-storage.d.ts +3 -2
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/users.d.ts +8 -4
- package/dist/auth-storage/users.d.ts.map +1 -1
- package/dist/index.js +3720 -1594
- package/dist/index.js.map +37 -31
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/conditional-reader.d.ts +17 -0
- package/dist/objects/conditional-reader.d.ts.map +1 -0
- package/dist/objects/query-compiler.d.ts +27 -1
- package/dist/objects/query-compiler.d.ts.map +1 -1
- package/dist/objects/query-indexes.d.ts.map +1 -1
- package/dist/objects/read-scope.d.ts +10 -1
- package/dist/objects/read-scope.d.ts.map +1 -1
- package/dist/objects/reader.d.ts +2 -0
- package/dist/objects/reader.d.ts.map +1 -1
- package/dist/objects/scoped-reader.d.ts +12 -0
- package/dist/objects/scoped-reader.d.ts.map +1 -0
- package/dist/objects/selected-reader.d.ts +1 -0
- package/dist/objects/selected-reader.d.ts.map +1 -1
- package/dist/objects/storage.d.ts +7 -1
- package/dist/objects/storage.d.ts.map +1 -1
- package/dist/ontology-storage/index.d.ts +2 -0
- package/dist/ontology-storage/index.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-session.d.ts +6 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-writer.d.ts +15 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.d.ts.map +1 -1
- package/dist/ontology-storage/unique-values.d.ts +60 -0
- package/dist/ontology-storage/unique-values.d.ts.map +1 -0
- package/dist/pg-ai-cost-storage.d.ts.map +1 -1
- package/dist/pg-ai-usage-storage.d.ts +8 -1
- package/dist/pg-ai-usage-storage.d.ts.map +1 -1
- package/dist/pg-artifact-storage.d.ts +8 -1
- package/dist/pg-artifact-storage.d.ts.map +1 -1
- package/dist/pg-execution-storage.d.ts +2 -1
- package/dist/pg-execution-storage.d.ts.map +1 -1
- package/dist/storage-errors.d.ts +2 -0
- package/dist/storage-errors.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agents/agent-storage.ts +11 -2
- package/src/agents/approvals.ts +400 -0
- package/src/agents/checkpoints.ts +8 -4
- package/src/agents/index.ts +1 -0
- package/src/agents/memory.ts +175 -0
- package/src/agents/messages.ts +14 -7
- package/src/agents/rows.ts +46 -1
- package/src/agents/runs.ts +154 -22
- package/src/agents/settings.ts +105 -0
- package/src/agents/threads.ts +356 -3
- package/src/ai-cost-groups.ts +11 -1
- package/src/auth-storage/auth-storage.ts +45 -13
- package/src/auth-storage/users.ts +105 -43
- package/src/migrations/069-artifact-kinds.sql +4 -0
- package/src/migrations/070-artifact-state.sql +16 -0
- package/src/migrations/071-agent-thread-participants.sql +16 -0
- package/src/migrations/072-forked-context-checkpoints.sql +23 -0
- package/src/migrations/073-unique-values.sql +25 -0
- package/src/migrations/074-generated-media-usage.sql +21 -0
- package/src/migrations/075-model-call-files.sql +11 -0
- package/src/migrations/076-agent-memory.sql +33 -0
- package/src/migrations/077-artifact-threads.sql +21 -0
- package/src/migrations/078-agent-approvals.sql +58 -0
- package/src/migrations/079-agent-approval-resume.sql +20 -0
- package/src/migrations/080-subagent-threads.sql +145 -0
- package/src/migrations/081-subagent-questions.sql +6 -0
- package/src/migrations.ts +34 -0
- package/src/objects/conditional-reader.ts +44 -0
- package/src/objects/prepare-queries.ts +1 -1
- package/src/objects/query-compiler.ts +96 -33
- package/src/objects/query-indexes.ts +29 -10
- package/src/objects/read-scope.ts +140 -25
- package/src/objects/reader.ts +50 -6
- package/src/objects/scoped-reader.ts +101 -0
- package/src/objects/selected-reader.ts +16 -76
- package/src/objects/storage.ts +24 -5
- package/src/objects/text-count-index.ts +1 -0
- package/src/ontology-storage/index.ts +3 -0
- package/src/ontology-storage/materialization-session.ts +8 -0
- package/src/ontology-storage/materialization-writer.ts +76 -1
- package/src/ontology-storage/materializations.ts +20 -1
- package/src/ontology-storage/unique-values.ts +274 -0
- package/src/pg-ai-cost-storage.ts +62 -12
- package/src/pg-ai-usage-storage.ts +90 -1
- package/src/pg-artifact-storage.ts +205 -9
- package/src/pg-execution-storage.ts +31 -0
- package/src/storage-errors.ts +7 -0
- 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
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
|
|
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(
|
package/src/objects/storage.ts
CHANGED
|
@@ -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.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|