@sixb/sqlite 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 (126) hide show
  1. package/dist/action-run-storage.d.ts +4 -0
  2. package/dist/action-run-storage.d.ts.map +1 -1
  3. package/dist/activity-storage.d.ts +20 -0
  4. package/dist/activity-storage.d.ts.map +1 -0
  5. package/dist/agents/agent-storage.d.ts +8 -2
  6. package/dist/agents/agent-storage.d.ts.map +1 -1
  7. package/dist/agents/approvals.d.ts +51 -0
  8. package/dist/agents/approvals.d.ts.map +1 -0
  9. package/dist/agents/checkpoints.d.ts +1 -0
  10. package/dist/agents/checkpoints.d.ts.map +1 -1
  11. package/dist/agents/index.d.ts +1 -0
  12. package/dist/agents/index.d.ts.map +1 -1
  13. package/dist/agents/memory.d.ts +21 -0
  14. package/dist/agents/memory.d.ts.map +1 -0
  15. package/dist/agents/messages.d.ts.map +1 -1
  16. package/dist/agents/rows.d.ts +15 -1
  17. package/dist/agents/rows.d.ts.map +1 -1
  18. package/dist/agents/runs.d.ts +14 -1
  19. package/dist/agents/runs.d.ts.map +1 -1
  20. package/dist/agents/settings.d.ts +29 -0
  21. package/dist/agents/settings.d.ts.map +1 -0
  22. package/dist/agents/threads.d.ts +16 -1
  23. package/dist/agents/threads.d.ts.map +1 -1
  24. package/dist/ai-cost-groups.d.ts.map +1 -1
  25. package/dist/ai-cost-storage.d.ts.map +1 -1
  26. package/dist/ai-usage-storage.d.ts +4 -1
  27. package/dist/ai-usage-storage.d.ts.map +1 -1
  28. package/dist/artifact-storage.d.ts +31 -0
  29. package/dist/artifact-storage.d.ts.map +1 -0
  30. package/dist/auth-storage/auth-storage.d.ts +3 -2
  31. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  32. package/dist/auth-storage/sessions.d.ts +2 -1
  33. package/dist/auth-storage/sessions.d.ts.map +1 -1
  34. package/dist/auth-storage/users.d.ts +7 -3
  35. package/dist/auth-storage/users.d.ts.map +1 -1
  36. package/dist/execution-storage.d.ts +3 -1
  37. package/dist/execution-storage.d.ts.map +1 -1
  38. package/dist/index.d.ts +4 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +5216 -2517
  41. package/dist/index.js.map +39 -31
  42. package/dist/migrations.d.ts +0 -1
  43. package/dist/migrations.d.ts.map +1 -1
  44. package/dist/objects/conditional-reader.d.ts +8 -0
  45. package/dist/objects/conditional-reader.d.ts.map +1 -0
  46. package/dist/objects/query-compiler.d.ts +26 -1
  47. package/dist/objects/query-compiler.d.ts.map +1 -1
  48. package/dist/objects/read-scope.d.ts +10 -1
  49. package/dist/objects/read-scope.d.ts.map +1 -1
  50. package/dist/objects/reader.d.ts +2 -0
  51. package/dist/objects/reader.d.ts.map +1 -1
  52. package/dist/objects/scoped-reader.d.ts +15 -0
  53. package/dist/objects/scoped-reader.d.ts.map +1 -0
  54. package/dist/objects/selected-reader.d.ts.map +1 -1
  55. package/dist/objects/storage.d.ts +6 -1
  56. package/dist/objects/storage.d.ts.map +1 -1
  57. package/dist/ontology-storage/index.d.ts +2 -0
  58. package/dist/ontology-storage/index.d.ts.map +1 -1
  59. package/dist/ontology-storage/materialization-writer.d.ts +2 -0
  60. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  61. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  62. package/dist/ontology-storage/sources.d.ts +2 -1
  63. package/dist/ontology-storage/sources.d.ts.map +1 -1
  64. package/dist/ontology-storage/unique-values.d.ts +42 -0
  65. package/dist/ontology-storage/unique-values.d.ts.map +1 -0
  66. package/dist/share-grant-storage.d.ts +2 -1
  67. package/dist/share-grant-storage.d.ts.map +1 -1
  68. package/dist/workflow-run-storage.d.ts +4 -0
  69. package/dist/workflow-run-storage.d.ts.map +1 -1
  70. package/package.json +3 -3
  71. package/src/action-run-storage.ts +27 -0
  72. package/src/activity-storage.ts +588 -0
  73. package/src/agents/agent-storage.ts +11 -2
  74. package/src/agents/approvals.ts +456 -0
  75. package/src/agents/checkpoints.ts +9 -4
  76. package/src/agents/index.ts +1 -0
  77. package/src/agents/memory.ts +195 -0
  78. package/src/agents/messages.ts +21 -18
  79. package/src/agents/rows.ts +46 -1
  80. package/src/agents/runs.ts +202 -21
  81. package/src/agents/settings.ts +112 -0
  82. package/src/agents/threads.ts +412 -1
  83. package/src/ai-cost-groups.ts +11 -1
  84. package/src/ai-cost-storage.ts +54 -12
  85. package/src/ai-usage-storage.ts +79 -2
  86. package/src/artifact-storage.ts +553 -0
  87. package/src/auth-storage/auth-storage.ts +58 -26
  88. package/src/auth-storage/sessions.ts +27 -0
  89. package/src/auth-storage/users.ts +106 -60
  90. package/src/execution-storage.ts +95 -0
  91. package/src/index.ts +10 -0
  92. package/src/migrations/062-artifacts.sql +71 -0
  93. package/src/migrations/063-drop-output-diagnostics.sql +19 -0
  94. package/src/migrations/064-activity.sql +61 -0
  95. package/src/migrations/065-activity-timeline.sql +19 -0
  96. package/src/migrations/066-activity-interactions.sql +20 -0
  97. package/src/migrations/067-artifact-kinds.sql +32 -0
  98. package/src/migrations/068-artifact-state.sql +15 -0
  99. package/src/migrations/069-agent-thread-participants.sql +16 -0
  100. package/src/migrations/070-forked-context-checkpoints.sql +45 -0
  101. package/src/migrations/071-unique-values.sql +22 -0
  102. package/src/migrations/072-generated-media-usage.sql +32 -0
  103. package/src/migrations/073-model-call-files.sql +11 -0
  104. package/src/migrations/074-agent-memory.sql +32 -0
  105. package/src/migrations/075-artifact-threads.sql +21 -0
  106. package/src/migrations/076-agent-approvals.sql +59 -0
  107. package/src/migrations/077-agent-approval-resume.sql +20 -0
  108. package/src/migrations/078-subagent-threads.sql +241 -0
  109. package/src/migrations/079-subagent-questions.sql +6 -0
  110. package/src/migrations.ts +89 -1
  111. package/src/objects/README.md +7 -0
  112. package/src/objects/conditional-reader.ts +27 -0
  113. package/src/objects/query-compiler.ts +36 -6
  114. package/src/objects/read-scope.ts +140 -23
  115. package/src/objects/reader.ts +47 -14
  116. package/src/objects/scoped-reader.ts +103 -0
  117. package/src/objects/selected-reader.ts +12 -75
  118. package/src/objects/storage.ts +16 -0
  119. package/src/ontology-storage/index.ts +3 -0
  120. package/src/ontology-storage/materialization-writer.ts +14 -0
  121. package/src/ontology-storage/materializations.ts +16 -1
  122. package/src/ontology-storage/sources.ts +41 -0
  123. package/src/ontology-storage/unique-values.ts +187 -0
  124. package/src/share-grant-storage.ts +25 -0
  125. package/src/transactions.ts +1 -1
  126. package/src/workflow-run-storage.ts +27 -0
@@ -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 objectsTable = source.objectsTable
154
+ const objects = sqliteObjectsOfType(source, params.objectTypeId)
154
155
  const statement = source.wrapStatement(
155
- `SELECT * FROM ${objectsTable} WHERE project_id = ? AND object_type_id = ? AND primary_id = ?`,
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
- let query = `SELECT * FROM ${source.objectsTable} WHERE project_id = ?`
403
- const args: (string | number | null)[] = [params.projectId]
404
-
405
- if (params.objectTypeId) {
406
- if (typeof params.objectTypeId === "string") {
407
- query += " AND object_type_id = ?"
408
- args.push(params.objectTypeId)
409
- } else {
410
- query += ` AND object_type_id IN (${params.objectTypeId.map(() => "?").join(", ")})`
411
- args.push(...params.objectTypeId)
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 { assertLinkQueryLimit, SqliteObjectReader } from "./reader"
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 Object.freeze({
58
- queryCapabilities,
59
- queryObjects: async (input) => {
60
- assertProject(input.projectId)
61
- return read(() => reader.queryObjects(input))
62
- },
63
- countObjects: async (input) => {
64
- assertProject(input.projectId)
65
- return read(() => reader.countObjects(input))
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(
@@ -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
 
@@ -20,6 +20,7 @@ import type {
20
20
  AbandonRunSourceMaterializationInput,
21
21
  AbandonSourceMaterializationCandidateInput,
22
22
  AbandonSourceMaterializationInput,
23
+ ActiveOntologySource,
23
24
  AdoptedSourceMaterialization,
24
25
  AdoptSourceMaterializationInput,
25
26
  AssertSourceMaterializationExecutionInput,
@@ -27,6 +28,7 @@ import type {
27
28
  CleanupTerminalSourceMaterializationsInput,
28
29
  CleanupTerminalSourceMaterializationsResult,
29
30
  GetActiveOntologySourceInput,
31
+ ListActiveOntologySourcesInput,
30
32
  MarkSourceMaterializationReadyInput,
31
33
  OntologySourceRecord,
32
34
  OntologySourceStorage,
@@ -238,6 +240,36 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
238
240
  })
239
241
  }
240
242
 
243
+ async listActive(
244
+ input: ListActiveOntologySourcesInput
245
+ ): Promise<readonly ActiveOntologySource[]> {
246
+ return this.runRootOperation(() => {
247
+ assertNonblank(input.projectId, "Source project id")
248
+
249
+ // Served by the partial index on active sources.
250
+ const rows = this.db
251
+ .query(
252
+ `
253
+ SELECT source_id, dataset_id, dataset_version_id, dataset_version_created_at,
254
+ projection_revision
255
+ FROM ontology_sources
256
+ WHERE project_id = ? AND status = 'active'
257
+ `
258
+ )
259
+ .all(input.projectId) as ActiveSourceRow[]
260
+
261
+ return rows.map((row) => ({
262
+ source: { projectionId: row.source_id },
263
+ datasetVersion: {
264
+ datasetId: row.dataset_id,
265
+ versionId: row.dataset_version_id,
266
+ createdAt: row.dataset_version_created_at,
267
+ },
268
+ projectionRevision: row.projection_revision,
269
+ }))
270
+ })
271
+ }
272
+
241
273
  async adopt(
242
274
  input: AdoptSourceMaterializationInput
243
275
  ): Promise<AdoptedSourceMaterialization | null> {
@@ -626,3 +658,12 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
626
658
  return row
627
659
  }
628
660
  }
661
+
662
+ type ActiveSourceRow = Pick<
663
+ SqliteOntologySourceRow,
664
+ | "source_id"
665
+ | "dataset_id"
666
+ | "dataset_version_id"
667
+ | "dataset_version_created_at"
668
+ | "projection_revision"
669
+ >
@@ -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
+ }