@sixb/sqlite 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/agent-storage.d.ts +8 -2
- package/dist/agents/agent-storage.d.ts.map +1 -1
- package/dist/agents/approvals.d.ts +51 -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 +21 -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 +14 -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 +12 -1
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/ai-cost-groups.d.ts.map +1 -1
- package/dist/ai-cost-storage.d.ts.map +1 -1
- package/dist/ai-usage-storage.d.ts +4 -1
- package/dist/ai-usage-storage.d.ts.map +1 -1
- package/dist/artifact-storage.d.ts +10 -1
- package/dist/artifact-storage.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 +7 -3
- package/dist/auth-storage/users.d.ts.map +1 -1
- package/dist/execution-storage.d.ts +2 -1
- package/dist/execution-storage.d.ts.map +1 -1
- package/dist/index.js +3348 -1515
- package/dist/index.js.map +32 -26
- package/dist/migrations.d.ts +0 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/conditional-reader.d.ts +8 -0
- package/dist/objects/conditional-reader.d.ts.map +1 -0
- package/dist/objects/query-compiler.d.ts +26 -1
- package/dist/objects/query-compiler.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 +15 -0
- package/dist/objects/scoped-reader.d.ts.map +1 -0
- package/dist/objects/selected-reader.d.ts.map +1 -1
- package/dist/objects/storage.d.ts +6 -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-writer.d.ts +2 -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 +42 -0
- package/dist/ontology-storage/unique-values.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/agents/agent-storage.ts +11 -2
- package/src/agents/approvals.ts +456 -0
- package/src/agents/checkpoints.ts +9 -4
- package/src/agents/index.ts +1 -0
- package/src/agents/memory.ts +195 -0
- package/src/agents/messages.ts +21 -18
- package/src/agents/rows.ts +46 -1
- package/src/agents/runs.ts +202 -21
- package/src/agents/settings.ts +112 -0
- package/src/agents/threads.ts +385 -1
- package/src/ai-cost-groups.ts +11 -1
- package/src/ai-cost-storage.ts +54 -12
- package/src/ai-usage-storage.ts +79 -2
- package/src/artifact-storage.ts +206 -9
- package/src/auth-storage/auth-storage.ts +58 -26
- package/src/auth-storage/users.ts +106 -60
- package/src/execution-storage.ts +39 -0
- package/src/migrations/067-artifact-kinds.sql +32 -0
- package/src/migrations/068-artifact-state.sql +15 -0
- package/src/migrations/069-agent-thread-participants.sql +16 -0
- package/src/migrations/070-forked-context-checkpoints.sql +45 -0
- package/src/migrations/071-unique-values.sql +22 -0
- package/src/migrations/072-generated-media-usage.sql +32 -0
- package/src/migrations/073-model-call-files.sql +11 -0
- package/src/migrations/074-agent-memory.sql +32 -0
- package/src/migrations/075-artifact-threads.sql +21 -0
- package/src/migrations/076-agent-approvals.sql +59 -0
- package/src/migrations/077-agent-approval-resume.sql +20 -0
- package/src/migrations/078-subagent-threads.sql +241 -0
- package/src/migrations/079-subagent-questions.sql +6 -0
- package/src/migrations.ts +75 -1
- package/src/objects/README.md +7 -0
- package/src/objects/conditional-reader.ts +27 -0
- package/src/objects/query-compiler.ts +36 -6
- package/src/objects/read-scope.ts +140 -23
- package/src/objects/reader.ts +47 -14
- package/src/objects/scoped-reader.ts +103 -0
- package/src/objects/selected-reader.ts +12 -75
- package/src/objects/storage.ts +16 -0
- package/src/ontology-storage/index.ts +3 -0
- package/src/ontology-storage/materialization-writer.ts +14 -0
- package/src/ontology-storage/materializations.ts +16 -1
- package/src/ontology-storage/unique-values.ts +187 -0
- package/src/transactions.ts +1 -1
package/src/objects/reader.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
compileObjectQuery,
|
|
27
27
|
type SqliteObjectQuerySource,
|
|
28
28
|
type SqliteValue,
|
|
29
|
+
sqliteObjectsOfType,
|
|
29
30
|
} from "./query-compiler"
|
|
30
31
|
import type { SqliteSelectedObjectReadSource } from "./read-scope"
|
|
31
32
|
import {
|
|
@@ -150,10 +151,40 @@ export class SqliteObjectReader {
|
|
|
150
151
|
primaryId: string
|
|
151
152
|
}): ObjectRow | null {
|
|
152
153
|
const source = this.source
|
|
153
|
-
const
|
|
154
|
+
const objects = sqliteObjectsOfType(source, params.objectTypeId)
|
|
154
155
|
const statement = source.wrapStatement(
|
|
155
|
-
`SELECT * FROM ${
|
|
156
|
-
[params.projectId, params.objectTypeId, params.primaryId]
|
|
156
|
+
`SELECT * FROM ${objects.table} WHERE project_id = ? AND object_type_id = ? AND primary_id = ? ${objects.filter}`,
|
|
157
|
+
[params.projectId, params.objectTypeId, params.primaryId, ...objects.args]
|
|
158
|
+
)
|
|
159
|
+
const row = this.db.query(statement.sql).get(...statement.args) as DatabaseRow | null
|
|
160
|
+
|
|
161
|
+
return row ? rowToObject(row) : null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** On a selected source, the holder must be in scope and selected for the property. */
|
|
165
|
+
getByUniqueValue(params: Parameters<ObjectReadStorage["getByUniqueValue"]>[0]): ObjectRow | null {
|
|
166
|
+
const source = this.source
|
|
167
|
+
const selected = "objectPropertyPermissionsTable" in source
|
|
168
|
+
const permitted = selected
|
|
169
|
+
? `AND EXISTS (
|
|
170
|
+
SELECT 1 FROM ${source.objectPropertyPermissionsTable} AS permitted
|
|
171
|
+
WHERE permitted.project_id = objects.project_id
|
|
172
|
+
AND permitted.object_type_id = objects.object_type_id
|
|
173
|
+
AND permitted.primary_id = objects.primary_id
|
|
174
|
+
AND permitted.property_id = held.property_id
|
|
175
|
+
)`
|
|
176
|
+
: ""
|
|
177
|
+
const statement = source.wrapStatement(
|
|
178
|
+
`SELECT objects.*
|
|
179
|
+
FROM object_unique_values AS held
|
|
180
|
+
JOIN ${source.objectsTable} AS objects
|
|
181
|
+
ON objects.project_id = held.project_id
|
|
182
|
+
AND objects.object_type_id = held.object_type_id
|
|
183
|
+
AND objects.primary_id = held.primary_id
|
|
184
|
+
WHERE held.project_id = ? AND held.object_type_id = ? AND held.property_id = ?
|
|
185
|
+
AND held.value_key = ?
|
|
186
|
+
${permitted}`,
|
|
187
|
+
[params.projectId, params.objectTypeId, params.propertyId, params.valueKey]
|
|
157
188
|
)
|
|
158
189
|
const row = this.db.query(statement.sql).get(...statement.args) as DatabaseRow | null
|
|
159
190
|
|
|
@@ -399,17 +430,19 @@ export class SqliteObjectReader {
|
|
|
399
430
|
total: number
|
|
400
431
|
} {
|
|
401
432
|
const source = this.source
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
433
|
+
const typed =
|
|
434
|
+
typeof params.objectTypeId === "string"
|
|
435
|
+
? sqliteObjectsOfType(source, params.objectTypeId)
|
|
436
|
+
: { table: source.objectsTable, filter: "", args: [] }
|
|
437
|
+
let query = `SELECT * FROM ${typed.table} WHERE project_id = ?`
|
|
438
|
+
const args: SqliteValue[] = [params.projectId]
|
|
439
|
+
|
|
440
|
+
if (typeof params.objectTypeId === "string") {
|
|
441
|
+
query += ` AND object_type_id = ? ${typed.filter}`
|
|
442
|
+
args.push(params.objectTypeId, ...typed.args)
|
|
443
|
+
} else if (params.objectTypeId !== undefined) {
|
|
444
|
+
query += ` AND object_type_id IN (${params.objectTypeId.map(() => "?").join(", ")})`
|
|
445
|
+
args.push(...params.objectTypeId)
|
|
413
446
|
}
|
|
414
447
|
|
|
415
448
|
if (params.primaryIdPrefix) {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
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 { runDeferredReadTransaction } from "../transactions"
|
|
13
|
+
import type { SqliteObjectQuerySource } from "./query-compiler"
|
|
14
|
+
import { assertLinkQueryLimit, SqliteObjectReader } from "./reader"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Answer every terminal from one scoped source, each in its own synchronous snapshot so the
|
|
18
|
+
* statements of one read, and the scope's own checks before them, agree.
|
|
19
|
+
*/
|
|
20
|
+
export function createSqliteScopedReader(
|
|
21
|
+
db: Database,
|
|
22
|
+
params: {
|
|
23
|
+
projectId: string
|
|
24
|
+
source: SqliteObjectQuerySource
|
|
25
|
+
/** Runs first in each read's snapshot. */
|
|
26
|
+
beforeRead?: () => void
|
|
27
|
+
limits?: ObjectReadExecutionLimits
|
|
28
|
+
},
|
|
29
|
+
queryCapabilities: () => ObjectQueryCapabilities
|
|
30
|
+
): ObjectReadStorage {
|
|
31
|
+
const { projectId, source, beforeRead, limits } = params
|
|
32
|
+
const reader = new SqliteObjectReader(db, source)
|
|
33
|
+
const assertProject = (actualProjectId: string): void =>
|
|
34
|
+
assertObjectReaderProject(projectId, actualProjectId)
|
|
35
|
+
const read = <T>(run: () => T): T =>
|
|
36
|
+
runDeferredReadTransaction(db, () => {
|
|
37
|
+
beforeRead?.()
|
|
38
|
+
const value = run()
|
|
39
|
+
if (limits) assertObjectReadOutputWithinLimit(value, limits)
|
|
40
|
+
return value
|
|
41
|
+
})
|
|
42
|
+
const readMap = <TKey, TValue>(run: () => Map<TKey, TValue>): Map<TKey, TValue> =>
|
|
43
|
+
runDeferredReadTransaction(db, () => {
|
|
44
|
+
beforeRead?.()
|
|
45
|
+
const value = run()
|
|
46
|
+
if (limits) assertObjectReadOutputWithinLimit([...value.entries()], limits)
|
|
47
|
+
return value
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
queryCapabilities,
|
|
52
|
+
queryObjects: async (input) => {
|
|
53
|
+
assertProject(input.projectId)
|
|
54
|
+
return read(() => reader.queryObjects(input))
|
|
55
|
+
},
|
|
56
|
+
countObjects: async (input) => {
|
|
57
|
+
assertProject(input.projectId)
|
|
58
|
+
return read(() => reader.countObjects(input))
|
|
59
|
+
},
|
|
60
|
+
existsObjects: async (input) => {
|
|
61
|
+
assertProject(input.projectId)
|
|
62
|
+
return read(() => reader.existsObjects(input))
|
|
63
|
+
},
|
|
64
|
+
facetObjects: async (input) => {
|
|
65
|
+
assertProject(input.projectId)
|
|
66
|
+
assertObjectReadFacetCount(input.facets.length)
|
|
67
|
+
return read(() => reader.facetObjects(input))
|
|
68
|
+
},
|
|
69
|
+
getByPrimaryId: async (input) => {
|
|
70
|
+
assertProject(input.projectId)
|
|
71
|
+
return read(() => reader.getByPrimaryId(input))
|
|
72
|
+
},
|
|
73
|
+
getByUniqueValue: async (input) => {
|
|
74
|
+
assertProject(input.projectId)
|
|
75
|
+
return read(() => reader.getByUniqueValue(input))
|
|
76
|
+
},
|
|
77
|
+
selectsObjectProperties: async (input) => {
|
|
78
|
+
assertProject(input.projectId)
|
|
79
|
+
return read(() => reader.selectsObjectProperties(input))
|
|
80
|
+
},
|
|
81
|
+
listLinks: async (input) => {
|
|
82
|
+
assertProject(input.projectId)
|
|
83
|
+
return read(() => reader.listLinks(input))
|
|
84
|
+
},
|
|
85
|
+
getByPrimaryIdBatch: async (input) => {
|
|
86
|
+
assertProject(input.projectId)
|
|
87
|
+
return readMap(() => reader.getByPrimaryIdBatch(input))
|
|
88
|
+
},
|
|
89
|
+
listLinksBatch: async (input) => {
|
|
90
|
+
assertProject(input.projectId)
|
|
91
|
+
return readMap(() => reader.listLinksBatch(input))
|
|
92
|
+
},
|
|
93
|
+
queryLinks: async (input) => {
|
|
94
|
+
assertProject(input.projectId)
|
|
95
|
+
assertLinkQueryLimit(input.limit)
|
|
96
|
+
return read(() => reader.queryLinks(input))
|
|
97
|
+
},
|
|
98
|
+
list: async (input) => {
|
|
99
|
+
assertProject(input.projectId)
|
|
100
|
+
return read(() => reader.list(input))
|
|
101
|
+
},
|
|
102
|
+
} satisfies ObjectReadStorage)
|
|
103
|
+
}
|
|
@@ -5,19 +5,12 @@ import type {
|
|
|
5
5
|
ObjectReadExecutionLimits,
|
|
6
6
|
ObjectReadStorage,
|
|
7
7
|
} from "@sixb/core/storage"
|
|
8
|
-
import {
|
|
9
|
-
assertObjectReaderProject,
|
|
10
|
-
assertObjectReadFacetCount,
|
|
11
|
-
assertObjectReadOutputWithinLimit,
|
|
12
|
-
ObjectReadLimitExceededError,
|
|
13
|
-
snapshotObjectReadExecutionLimits,
|
|
14
|
-
} from "@sixb/core/storage"
|
|
15
|
-
import { runDeferredReadTransaction } from "../transactions"
|
|
8
|
+
import { ObjectReadLimitExceededError, snapshotObjectReadExecutionLimits } from "@sixb/core/storage"
|
|
16
9
|
import {
|
|
17
10
|
compileSqliteSelectedObjectReadSource,
|
|
18
11
|
type SqliteSelectedObjectReadSource,
|
|
19
12
|
} from "./read-scope"
|
|
20
|
-
import {
|
|
13
|
+
import { createSqliteScopedReader } from "./scoped-reader"
|
|
21
14
|
|
|
22
15
|
/** Keep the traversal probe and every terminal statement in the same synchronous snapshot. */
|
|
23
16
|
export function createSqliteSelectedReader(
|
|
@@ -36,73 +29,17 @@ export function createSqliteSelectedReader(
|
|
|
36
29
|
params.scope,
|
|
37
30
|
limits.maxTraversalFacts
|
|
38
31
|
)
|
|
39
|
-
const reader = new SqliteObjectReader(db, source)
|
|
40
|
-
const assertProject = (actualProjectId: string): void =>
|
|
41
|
-
assertObjectReaderProject(projectId, actualProjectId)
|
|
42
|
-
const read = <T>(run: () => T): T =>
|
|
43
|
-
runDeferredReadTransaction(db, () => {
|
|
44
|
-
assertTraversalBudget(db, source, limits.maxTraversalFacts)
|
|
45
|
-
const value = run()
|
|
46
|
-
assertObjectReadOutputWithinLimit(value, limits)
|
|
47
|
-
return value
|
|
48
|
-
})
|
|
49
|
-
const readMap = <TKey, TValue>(run: () => Map<TKey, TValue>): Map<TKey, TValue> =>
|
|
50
|
-
runDeferredReadTransaction(db, () => {
|
|
51
|
-
assertTraversalBudget(db, source, limits.maxTraversalFacts)
|
|
52
|
-
const value = run()
|
|
53
|
-
assertObjectReadOutputWithinLimit([...value.entries()], limits)
|
|
54
|
-
return value
|
|
55
|
-
})
|
|
56
32
|
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
existsObjects: async (input) => {
|
|
68
|
-
assertProject(input.projectId)
|
|
69
|
-
return read(() => reader.existsObjects(input))
|
|
70
|
-
},
|
|
71
|
-
facetObjects: async (input) => {
|
|
72
|
-
assertProject(input.projectId)
|
|
73
|
-
assertObjectReadFacetCount(input.facets.length)
|
|
74
|
-
return read(() => reader.facetObjects(input))
|
|
75
|
-
},
|
|
76
|
-
getByPrimaryId: async (input) => {
|
|
77
|
-
assertProject(input.projectId)
|
|
78
|
-
return read(() => reader.getByPrimaryId(input))
|
|
79
|
-
},
|
|
80
|
-
selectsObjectProperties: async (input) => {
|
|
81
|
-
assertProject(input.projectId)
|
|
82
|
-
return read(() => reader.selectsObjectProperties(input))
|
|
83
|
-
},
|
|
84
|
-
listLinks: async (input) => {
|
|
85
|
-
assertProject(input.projectId)
|
|
86
|
-
return read(() => reader.listLinks(input))
|
|
87
|
-
},
|
|
88
|
-
getByPrimaryIdBatch: async (input) => {
|
|
89
|
-
assertProject(input.projectId)
|
|
90
|
-
return readMap(() => reader.getByPrimaryIdBatch(input))
|
|
91
|
-
},
|
|
92
|
-
listLinksBatch: async (input) => {
|
|
93
|
-
assertProject(input.projectId)
|
|
94
|
-
return readMap(() => reader.listLinksBatch(input))
|
|
95
|
-
},
|
|
96
|
-
queryLinks: async (input) => {
|
|
97
|
-
assertProject(input.projectId)
|
|
98
|
-
assertLinkQueryLimit(input.limit)
|
|
99
|
-
return read(() => reader.queryLinks(input))
|
|
100
|
-
},
|
|
101
|
-
list: async (input) => {
|
|
102
|
-
assertProject(input.projectId)
|
|
103
|
-
return read(() => reader.list(input))
|
|
104
|
-
},
|
|
105
|
-
} satisfies ObjectReadStorage)
|
|
33
|
+
return createSqliteScopedReader(
|
|
34
|
+
db,
|
|
35
|
+
{
|
|
36
|
+
projectId,
|
|
37
|
+
source,
|
|
38
|
+
beforeRead: () => assertTraversalBudget(db, source, limits.maxTraversalFacts),
|
|
39
|
+
limits,
|
|
40
|
+
},
|
|
41
|
+
queryCapabilities
|
|
42
|
+
)
|
|
106
43
|
}
|
|
107
44
|
|
|
108
45
|
function assertTraversalBudget(
|
package/src/objects/storage.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite"
|
|
2
2
|
import { hasVectorProfile } from "@sixb/core/internal/query"
|
|
3
3
|
import type {
|
|
4
|
+
CompiledConditionalObjectReadScope,
|
|
4
5
|
CompiledSelectedObjectReadScope,
|
|
5
6
|
CountObjectsInput,
|
|
6
7
|
CountObjectsResult,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
runDeferredReadTransaction,
|
|
29
30
|
type SqliteStoreConnection,
|
|
30
31
|
} from "../transactions"
|
|
32
|
+
import { createSqliteConditionalReader } from "./conditional-reader"
|
|
31
33
|
import { DEFAULT_OBJECT_QUERY_SOURCE } from "./query-compiler"
|
|
32
34
|
import { assertLinkQueryLimit, SqliteObjectReader } from "./reader"
|
|
33
35
|
import { type DatabaseRow, type LinkDatabaseRow, rowToLink, rowToObject } from "./rows"
|
|
@@ -72,6 +74,7 @@ const SQLITE_OBJECT_QUERY_CAPABILITIES: ObjectQueryCapabilities = {
|
|
|
72
74
|
in: true,
|
|
73
75
|
exists: true,
|
|
74
76
|
contains: true,
|
|
77
|
+
constant: true,
|
|
75
78
|
},
|
|
76
79
|
sortKinds: {
|
|
77
80
|
property: true,
|
|
@@ -137,6 +140,13 @@ export class SqliteObjectStorage implements ObjectStorage {
|
|
|
137
140
|
return createSqliteSelectedReader(this.db, params, () => this.queryCapabilities())
|
|
138
141
|
}
|
|
139
142
|
|
|
143
|
+
createConditionalReadScope(params: {
|
|
144
|
+
projectId: string
|
|
145
|
+
scope: CompiledConditionalObjectReadScope
|
|
146
|
+
}): ObjectReadStorage {
|
|
147
|
+
return createSqliteConditionalReader(this.db, params, () => this.queryCapabilities())
|
|
148
|
+
}
|
|
149
|
+
|
|
140
150
|
async queryObjects(params: QueryObjectsInput): Promise<QueryObjectsResult> {
|
|
141
151
|
if (hasVectorProfile(params.query))
|
|
142
152
|
return runDeferredReadTransaction(this.db, () => this.reader.queryObjects(params))
|
|
@@ -167,6 +177,12 @@ export class SqliteObjectStorage implements ObjectStorage {
|
|
|
167
177
|
return this.reader.getByPrimaryId(params)
|
|
168
178
|
}
|
|
169
179
|
|
|
180
|
+
async getByUniqueValue(
|
|
181
|
+
params: Parameters<ObjectReadStorage["getByUniqueValue"]>[0]
|
|
182
|
+
): Promise<ObjectRow | null> {
|
|
183
|
+
return this.reader.getByUniqueValue(params)
|
|
184
|
+
}
|
|
185
|
+
|
|
170
186
|
async selectsObjectProperties(
|
|
171
187
|
params: Parameters<ObjectReadStorage["selectsObjectProperties"]>[0]
|
|
172
188
|
): Promise<readonly boolean[]> {
|
|
@@ -7,6 +7,7 @@ import { SqliteOntologyOutboxStorage } from "./outbox"
|
|
|
7
7
|
import { SqliteOntologyReplacementPlanStorage } from "./replacement-plans"
|
|
8
8
|
import type { SqliteRootOperation } from "./shared"
|
|
9
9
|
import { SqliteOntologySourceStorage } from "./sources"
|
|
10
|
+
import { SqliteOntologyUniqueValueStorage } from "./unique-values"
|
|
10
11
|
import { SqliteVectorIndexingStorage } from "./vector-indexing"
|
|
11
12
|
import { SqliteOntologyVectorStorage } from "./vectors"
|
|
12
13
|
|
|
@@ -18,6 +19,7 @@ export class SqliteOntologyStorage implements OntologyStorage {
|
|
|
18
19
|
readonly vectorIndexing: SqliteVectorIndexingStorage
|
|
19
20
|
readonly vectors: SqliteOntologyVectorStorage
|
|
20
21
|
readonly outbox: SqliteOntologyOutboxStorage
|
|
22
|
+
readonly uniqueValues: SqliteOntologyUniqueValueStorage
|
|
21
23
|
|
|
22
24
|
constructor(input: {
|
|
23
25
|
readonly db: Database
|
|
@@ -46,6 +48,7 @@ export class SqliteOntologyStorage implements OntologyStorage {
|
|
|
46
48
|
this.materializations.assertVectorSession(session, projectId, commitId)
|
|
47
49
|
)
|
|
48
50
|
this.outbox = new SqliteOntologyOutboxStorage(input.db, input.runRootOperation)
|
|
51
|
+
this.uniqueValues = new SqliteOntologyUniqueValueStorage(input.db, input.runRootOperation)
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
deactivateSessions(): void {
|
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
} from "@sixb/core/internal/ontology-storage-provider"
|
|
12
12
|
import type { ExactEffectiveLinkWrite, ExactEffectiveObjectWrite } from "@sixb/core/storage"
|
|
13
13
|
import { assertTimestamp, canonicalJson, isSqliteConstraintError, requireChanges } from "./shared"
|
|
14
|
+
import {
|
|
15
|
+
assertSqliteUniquePrepared,
|
|
16
|
+
claimSqliteUniqueValue,
|
|
17
|
+
releaseSqliteUniqueValue,
|
|
18
|
+
} from "./unique-values"
|
|
14
19
|
|
|
15
20
|
function linkOverrideConflictMessage(identityKind: "edge" | "slot"): string {
|
|
16
21
|
return identityKind === "edge"
|
|
@@ -30,9 +35,18 @@ export class SqliteMaterializationWriter {
|
|
|
30
35
|
this.applyOverrides(projectId, chunk)
|
|
31
36
|
this.applyEffective(projectId, chunk)
|
|
32
37
|
this.applyTimeseries(projectId, chunk)
|
|
38
|
+
this.applyUniqueValues(projectId, chunk)
|
|
33
39
|
this.applyOutbox(projectId, commitId, chunk)
|
|
34
40
|
}
|
|
35
41
|
|
|
42
|
+
/** A chunk holds one phase: the commit's releases all apply before its first claim. */
|
|
43
|
+
private applyUniqueValues(projectId: string, chunk: MaterializationPlanChunk): void {
|
|
44
|
+
for (const item of chunk.unique.releases) releaseSqliteUniqueValue(this.db, projectId, item)
|
|
45
|
+
|
|
46
|
+
assertSqliteUniquePrepared(this.db, projectId, chunk.unique.claims)
|
|
47
|
+
for (const item of chunk.unique.claims) claimSqliteUniqueValue(this.db, projectId, item)
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
private applyOverrides(projectId: string, chunk: MaterializationPlanChunk): void {
|
|
37
51
|
for (const item of chunk.overrides.objects.upserts) {
|
|
38
52
|
if (item.expectedLastCommitId === null) {
|
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
type SqliteOntologySourceRow,
|
|
66
66
|
} from "./shared"
|
|
67
67
|
import { activateSourceRoots } from "./source-roots"
|
|
68
|
+
import { sqliteUniqueHolder } from "./unique-values"
|
|
68
69
|
|
|
69
70
|
/** Rows the provider reads and writes at once while it applies a staged plan. */
|
|
70
71
|
const APPLY_PAGE_ROWS = 1_000
|
|
@@ -111,7 +112,9 @@ function touchAppliedWorkStatement(workTable: string): string {
|
|
|
111
112
|
END AS ref
|
|
112
113
|
FROM ${workTable}
|
|
113
114
|
WHERE work_id = $workId AND lane = 'apply'
|
|
114
|
-
)
|
|
115
|
+
)
|
|
116
|
+
-- Unique values are no plan input: a stale claim fails when it applies.
|
|
117
|
+
WHERE kind IS NOT NULL`
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
export class SqliteOntologyMaterializationStorage implements OntologyMaterializationStorage {
|
|
@@ -225,6 +228,18 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
|
|
|
225
228
|
const page = reader.exactPoints(points.slice(offset, offset + input.pageRows))
|
|
226
229
|
if (page.length > 0) yield { objects: [], links: [], linkScopes: [], points: page }
|
|
227
230
|
}
|
|
231
|
+
const uniqueValues = request.uniqueValues ?? []
|
|
232
|
+
for (let offset = 0; offset < uniqueValues.length; offset += input.pageRows) {
|
|
233
|
+
const uniqueHolders = uniqueValues
|
|
234
|
+
.slice(offset, offset + input.pageRows)
|
|
235
|
+
.flatMap((value) => {
|
|
236
|
+
const primaryId = sqliteUniqueHolder(this.db, session.header.commit.projectId, value)
|
|
237
|
+
return primaryId === null ? [] : [{ ...value, primaryId }]
|
|
238
|
+
})
|
|
239
|
+
if (uniqueHolders.length > 0) {
|
|
240
|
+
yield { objects: [], links: [], linkScopes: [], points: [], uniqueHolders }
|
|
241
|
+
}
|
|
242
|
+
}
|
|
228
243
|
}
|
|
229
244
|
}
|
|
230
245
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import {
|
|
3
|
+
uniquePropertyNotPrepared,
|
|
4
|
+
uniqueValueViolation,
|
|
5
|
+
} from "@sixb/core/internal/ontology-storage-provider"
|
|
6
|
+
import type {
|
|
7
|
+
ExactUniqueValueClaim,
|
|
8
|
+
ExactUniqueValueWrite,
|
|
9
|
+
OntologyUniqueValueStorage,
|
|
10
|
+
PreparedUniqueProperty,
|
|
11
|
+
UniqueValueEntry,
|
|
12
|
+
UniqueValueRef,
|
|
13
|
+
} from "@sixb/core/storage"
|
|
14
|
+
import type { SqliteRootOperation } from "./shared"
|
|
15
|
+
|
|
16
|
+
/** The object holding a unique value, or null when none does. */
|
|
17
|
+
export function sqliteUniqueHolder(
|
|
18
|
+
db: Database,
|
|
19
|
+
projectId: string,
|
|
20
|
+
value: UniqueValueRef
|
|
21
|
+
): string | null {
|
|
22
|
+
const row = db
|
|
23
|
+
.query(
|
|
24
|
+
`SELECT primary_id FROM object_unique_values
|
|
25
|
+
WHERE project_id = ? AND object_type_id = ? AND property_id = ? AND value_key = ?`
|
|
26
|
+
)
|
|
27
|
+
.get(projectId, value.objectTypeId, value.propertyId, value.valueKey) as {
|
|
28
|
+
readonly primary_id: string
|
|
29
|
+
} | null
|
|
30
|
+
return row?.primary_id ?? null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Gives the value to its object, unless another object holds it. */
|
|
34
|
+
export function claimSqliteUniqueValue(
|
|
35
|
+
db: Database,
|
|
36
|
+
projectId: string,
|
|
37
|
+
value: ExactUniqueValueWrite
|
|
38
|
+
): void {
|
|
39
|
+
const holder = sqliteUniqueHolder(db, projectId, value)
|
|
40
|
+
if (holder === value.primaryId) return
|
|
41
|
+
if (holder !== null) throw uniqueValueViolation(value, holder, value.primaryId)
|
|
42
|
+
|
|
43
|
+
db.query(
|
|
44
|
+
`INSERT INTO object_unique_values (
|
|
45
|
+
project_id, object_type_id, property_id, value_key, primary_id
|
|
46
|
+
) VALUES (?, ?, ?, ?, ?)`
|
|
47
|
+
).run(projectId, value.objectTypeId, value.propertyId, value.valueKey, value.primaryId)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Refuses claims on a property whose values were not built with the claims' keys. Each property
|
|
52
|
+
* is checked once per call, however many of its values the claims take.
|
|
53
|
+
*/
|
|
54
|
+
export function assertSqliteUniquePrepared(
|
|
55
|
+
db: Database,
|
|
56
|
+
projectId: string,
|
|
57
|
+
claims: readonly ExactUniqueValueClaim[]
|
|
58
|
+
): void {
|
|
59
|
+
const checked = new Set<string>()
|
|
60
|
+
const prepared = db.query(
|
|
61
|
+
`SELECT 1 FROM object_unique_properties
|
|
62
|
+
WHERE project_id = ? AND object_type_id = ? AND property_id = ?
|
|
63
|
+
AND key_kind = ? AND key_version = ?`
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
for (const claim of claims) {
|
|
67
|
+
const key = JSON.stringify([
|
|
68
|
+
claim.objectTypeId,
|
|
69
|
+
claim.propertyId,
|
|
70
|
+
claim.keyKind,
|
|
71
|
+
claim.keyVersion,
|
|
72
|
+
])
|
|
73
|
+
if (checked.has(key)) continue
|
|
74
|
+
checked.add(key)
|
|
75
|
+
|
|
76
|
+
const row = prepared.get(
|
|
77
|
+
projectId,
|
|
78
|
+
claim.objectTypeId,
|
|
79
|
+
claim.propertyId,
|
|
80
|
+
claim.keyKind,
|
|
81
|
+
claim.keyVersion
|
|
82
|
+
)
|
|
83
|
+
if (!row) throw uniquePropertyNotPrepared(claim)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Drops the value if its object still holds it. */
|
|
88
|
+
export function releaseSqliteUniqueValue(
|
|
89
|
+
db: Database,
|
|
90
|
+
projectId: string,
|
|
91
|
+
value: ExactUniqueValueWrite
|
|
92
|
+
): void {
|
|
93
|
+
db.query(
|
|
94
|
+
`DELETE FROM object_unique_values
|
|
95
|
+
WHERE project_id = ? AND object_type_id = ? AND property_id = ? AND value_key = ?
|
|
96
|
+
AND primary_id = ?`
|
|
97
|
+
).run(projectId, value.objectTypeId, value.propertyId, value.valueKey, value.primaryId)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class SqliteOntologyUniqueValueStorage implements OntologyUniqueValueStorage {
|
|
101
|
+
constructor(
|
|
102
|
+
private readonly db: Database,
|
|
103
|
+
private readonly runRootOperation: SqliteRootOperation
|
|
104
|
+
) {}
|
|
105
|
+
|
|
106
|
+
/** A SQLite database has one writer at a time: preparers already take turns. */
|
|
107
|
+
async lockPreparation(_input: { readonly projectId: string }): Promise<void> {}
|
|
108
|
+
|
|
109
|
+
async listPrepared(input: {
|
|
110
|
+
readonly projectId: string
|
|
111
|
+
}): Promise<readonly PreparedUniqueProperty[]> {
|
|
112
|
+
return this.runRootOperation(() => {
|
|
113
|
+
const rows = this.db
|
|
114
|
+
.query(
|
|
115
|
+
`SELECT object_type_id, property_id, key_kind, key_version FROM object_unique_properties
|
|
116
|
+
WHERE project_id = ?
|
|
117
|
+
ORDER BY object_type_id, property_id`
|
|
118
|
+
)
|
|
119
|
+
.all(input.projectId) as {
|
|
120
|
+
readonly object_type_id: string
|
|
121
|
+
readonly property_id: string
|
|
122
|
+
readonly key_kind: PreparedUniqueProperty["keyKind"]
|
|
123
|
+
readonly key_version: number
|
|
124
|
+
}[]
|
|
125
|
+
return rows.map((row) => ({
|
|
126
|
+
objectTypeId: row.object_type_id,
|
|
127
|
+
propertyId: row.property_id,
|
|
128
|
+
keyKind: row.key_kind,
|
|
129
|
+
keyVersion: row.key_version,
|
|
130
|
+
}))
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async clear(input: {
|
|
135
|
+
readonly projectId: string
|
|
136
|
+
readonly objectTypeId: string
|
|
137
|
+
readonly propertyId: string
|
|
138
|
+
}): Promise<void> {
|
|
139
|
+
await this.runRootOperation(() => {
|
|
140
|
+
for (const table of ["object_unique_values", "object_unique_properties"]) {
|
|
141
|
+
this.db
|
|
142
|
+
.query(
|
|
143
|
+
`DELETE FROM ${table} WHERE project_id = ? AND object_type_id = ? AND property_id = ?`
|
|
144
|
+
)
|
|
145
|
+
.run(input.projectId, input.objectTypeId, input.propertyId)
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async insert(input: {
|
|
151
|
+
readonly projectId: string
|
|
152
|
+
readonly objectTypeId: string
|
|
153
|
+
readonly propertyId: string
|
|
154
|
+
readonly entries: readonly UniqueValueEntry[]
|
|
155
|
+
}): Promise<void> {
|
|
156
|
+
const { projectId, objectTypeId, propertyId } = input
|
|
157
|
+
await this.runRootOperation(() => {
|
|
158
|
+
for (const entry of input.entries) {
|
|
159
|
+
claimSqliteUniqueValue(this.db, projectId, { objectTypeId, propertyId, ...entry })
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async markPrepared(
|
|
165
|
+
input: PreparedUniqueProperty & { readonly projectId: string; readonly preparedAt: string }
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
await this.runRootOperation(() => {
|
|
168
|
+
this.db
|
|
169
|
+
.query(
|
|
170
|
+
`INSERT INTO object_unique_properties (
|
|
171
|
+
project_id, object_type_id, property_id, key_kind, key_version, prepared_at
|
|
172
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
173
|
+
ON CONFLICT (project_id, object_type_id, property_id)
|
|
174
|
+
DO UPDATE SET key_kind = excluded.key_kind, key_version = excluded.key_version,
|
|
175
|
+
prepared_at = excluded.prepared_at`
|
|
176
|
+
)
|
|
177
|
+
.run(
|
|
178
|
+
input.projectId,
|
|
179
|
+
input.objectTypeId,
|
|
180
|
+
input.propertyId,
|
|
181
|
+
input.keyKind,
|
|
182
|
+
input.keyVersion,
|
|
183
|
+
input.preparedAt
|
|
184
|
+
)
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
}
|
package/src/transactions.ts
CHANGED
|
@@ -108,7 +108,7 @@ export function runDeferredReadTransaction<T>(db: Database, run: () => T): T {
|
|
|
108
108
|
}
|
|
109
109
|
if (db.inTransaction) {
|
|
110
110
|
throw new Error(
|
|
111
|
-
"[SixbSqlite]
|
|
111
|
+
"[SixbSqlite] Scoped object reads cannot join an unverified SQLite transaction."
|
|
112
112
|
)
|
|
113
113
|
}
|
|
114
114
|
|