@sixb/sqlite 0.1.15 → 0.1.17

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 (79) hide show
  1. package/dist/ai-limit-storage.d.ts.map +1 -1
  2. package/dist/ai-usage-storage.d.ts.map +1 -1
  3. package/dist/execution-storage.d.ts +28 -0
  4. package/dist/execution-storage.d.ts.map +1 -1
  5. package/dist/file-download-grant-storage.d.ts +11 -0
  6. package/dist/file-download-grant-storage.d.ts.map +1 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1428 -599
  10. package/dist/index.js.map +29 -27
  11. package/dist/migrations.d.ts.map +1 -1
  12. package/dist/objects/query-compiler.d.ts +3 -0
  13. package/dist/objects/query-compiler.d.ts.map +1 -1
  14. package/dist/objects/read-scope.d.ts +2 -0
  15. package/dist/objects/read-scope.d.ts.map +1 -1
  16. package/dist/objects/reader.d.ts +1 -0
  17. package/dist/objects/reader.d.ts.map +1 -1
  18. package/dist/objects/rows.d.ts +1 -0
  19. package/dist/objects/rows.d.ts.map +1 -1
  20. package/dist/objects/storage.d.ts.map +1 -1
  21. package/dist/ontology-storage/index.d.ts +2 -0
  22. package/dist/ontology-storage/index.d.ts.map +1 -1
  23. package/dist/ontology-storage/materialization-session.d.ts +17 -37
  24. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  25. package/dist/ontology-storage/materialization-state.d.ts +3 -26
  26. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  27. package/dist/ontology-storage/materialization-writer.d.ts +1 -1
  28. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  29. package/dist/ontology-storage/materializations.d.ts +3 -6
  30. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  31. package/dist/ontology-storage/outbox.d.ts.map +1 -1
  32. package/dist/ontology-storage/replacement-plans.d.ts +88 -0
  33. package/dist/ontology-storage/replacement-plans.d.ts.map +1 -0
  34. package/dist/ontology-storage/shared.d.ts +8 -5
  35. package/dist/ontology-storage/shared.d.ts.map +1 -1
  36. package/dist/ontology-storage/source-cleanup.d.ts.map +1 -1
  37. package/dist/ontology-storage/source-roots.d.ts +15 -2
  38. package/dist/ontology-storage/source-roots.d.ts.map +1 -1
  39. package/dist/ontology-storage/sources.d.ts +1 -2
  40. package/dist/ontology-storage/sources.d.ts.map +1 -1
  41. package/dist/projection-run-storage.d.ts.map +1 -1
  42. package/dist/rules-storage.d.ts.map +1 -1
  43. package/dist/testing.d.ts +4 -1
  44. package/dist/testing.d.ts.map +1 -1
  45. package/package.json +3 -3
  46. package/src/ai-cost-storage.ts +10 -0
  47. package/src/ai-limit-storage.ts +13 -2
  48. package/src/ai-usage-storage.ts +9 -1
  49. package/src/execution-storage.ts +7 -2
  50. package/src/file-download-grant-storage.ts +76 -0
  51. package/src/index.ts +10 -3
  52. package/src/migrations/053-replacement-plans.sql +73 -0
  53. package/src/migrations/054-slim-ontology-outbox.sql +21 -0
  54. package/src/migrations/055-commit-touches.sql +31 -0
  55. package/src/migrations/056-audio-transcription.sql +10 -0
  56. package/src/migrations/057-reranking-model-kind.sql +16 -0
  57. package/src/migrations/058-file-download-grants.sql +21 -0
  58. package/src/migrations/059-drained-source-versions.sql +11 -0
  59. package/src/migrations.ts +115 -0
  60. package/src/objects/README.md +19 -0
  61. package/src/objects/query-compiler.ts +49 -7
  62. package/src/objects/read-scope.ts +10 -8
  63. package/src/objects/reader.ts +44 -8
  64. package/src/objects/rows.ts +1 -0
  65. package/src/objects/storage.ts +5 -11
  66. package/src/ontology-storage/index.ts +6 -1
  67. package/src/ontology-storage/materialization-session.ts +120 -211
  68. package/src/ontology-storage/materialization-state.ts +5 -261
  69. package/src/ontology-storage/materialization-writer.ts +11 -13
  70. package/src/ontology-storage/materializations.ts +133 -210
  71. package/src/ontology-storage/outbox.ts +50 -16
  72. package/src/ontology-storage/replacement-plans.ts +1064 -0
  73. package/src/ontology-storage/shared.ts +17 -12
  74. package/src/ontology-storage/source-cleanup.ts +24 -9
  75. package/src/ontology-storage/source-roots.ts +38 -14
  76. package/src/ontology-storage/sources.ts +0 -26
  77. package/src/projection-run-storage.ts +7 -1
  78. package/src/rules-storage.ts +9 -14
  79. package/src/testing.ts +6 -4
@@ -2,6 +2,7 @@ import type { Database } from "bun:sqlite"
2
2
  import { parseSixbFailure } from "@sixb/core/internal/errors"
3
3
  import { MaterializationConflictError } from "@sixb/core/internal/materialization"
4
4
  import {
5
+ type ClaimedOutboxDraft,
5
6
  sourceAssertionFromColumns,
6
7
  sourceEntityFromKey,
7
8
  } from "@sixb/core/internal/ontology-storage-provider"
@@ -9,7 +10,6 @@ import type {
9
10
  AssertSourceMaterializationExecutionInput,
10
11
  OntologyCommitRecord,
11
12
  OntologyCommitWrite,
12
- OntologyOutboxRecord,
13
13
  OntologySourceRecord,
14
14
  StageSourceAssertion,
15
15
  } from "@sixb/core/storage"
@@ -40,8 +40,6 @@ export interface SqliteOntologyCommitRow {
40
40
  readonly origin_run_id: string | null
41
41
  readonly origin_batch_ordinal: number | null
42
42
  readonly origin: string
43
- readonly requested_by: string | null
44
- readonly executor: string
45
43
  readonly ontology_revision: string
46
44
  readonly projection_revision: string | null
47
45
  readonly ownership_hash: string | null
@@ -102,7 +100,10 @@ export interface SqliteStoredOverrideRow {
102
100
  }
103
101
 
104
102
  export interface SqliteOntologyOutboxRow {
105
- readonly envelope: string
103
+ readonly id: string
104
+ readonly commit_id: string
105
+ readonly commit_ordinal: number
106
+ readonly event: string
106
107
  readonly available_at: string
107
108
  readonly attempts: number
108
109
  readonly lease_id: string | null
@@ -172,12 +173,6 @@ export function commitRecord(row: SqliteOntologyCommitRow): OntologyCommitRecord
172
173
  requestHash: row.request_hash,
173
174
  executionId: row.execution_id,
174
175
  origin: parseJson<OntologyCommitWrite["origin"]>(row.origin),
175
- ...(row.requested_by === null
176
- ? {}
177
- : {
178
- requestedBy: parseJson<NonNullable<OntologyCommitWrite["requestedBy"]>>(row.requested_by),
179
- }),
180
- executor: parseJson<OntologyCommitWrite["executor"]>(row.executor),
181
176
  ontologyRevision: row.ontology_revision,
182
177
  ...(row.projection_revision === null ? {} : { projectionRevision: row.projection_revision }),
183
178
  ...(row.ownership_hash === null ? {} : { ownershipHash: row.ownership_hash }),
@@ -188,9 +183,19 @@ export function commitRecord(row: SqliteOntologyCommitRow): OntologyCommitRecord
188
183
  return { ...base, intent, result } as OntologyCommitRecord
189
184
  }
190
185
 
191
- export function outboxRecord(row: SqliteOntologyOutboxRow): OntologyOutboxRecord {
186
+ /** A claimed row as stored: its draft, to rebuild from its commit. */
187
+ export function claimedOutboxDraft(row: SqliteOntologyOutboxRow): ClaimedOutboxDraft {
188
+ if (row.lease_id === null || row.lease_expires_at === null) {
189
+ throw new MaterializationConflictError(
190
+ "outbox-lease",
191
+ "Ontology outbox claim returned an unpaired lease."
192
+ )
193
+ }
192
194
  return {
193
- envelope: parseJson<OntologyOutboxRecord["envelope"]>(row.envelope),
195
+ id: row.id,
196
+ commitId: row.commit_id,
197
+ commitOrdinal: row.commit_ordinal,
198
+ event: parseJson<ClaimedOutboxDraft["event"]>(row.event),
194
199
  availableAt: row.available_at,
195
200
  attempts: row.attempts,
196
201
  leaseId: row.lease_id,
@@ -5,6 +5,14 @@ import type {
5
5
  PurgeAbandonedSourceMaterializationsInput,
6
6
  } from "@sixb/core/storage"
7
7
 
8
+ /**
9
+ * A version outlives its replacement plan, which references it: the next version may take a
10
+ * deleted one's id, and would then resume the plan left under it.
11
+ */
12
+ const PLAN_PURGED = `NOT EXISTS (
13
+ SELECT 1 FROM ontology_replacement_plans AS plans WHERE plans.version_id = versions.version_id
14
+ )`
15
+
8
16
  /**
9
17
  * Superseded versions can still hold live roots (a delta keeps unchanged roots where they were),
10
18
  * so they are cleaned root by root once retired. An abandoned candidate was never published:
@@ -18,8 +26,8 @@ export function purgeAbandonedSourceVersions(
18
26
  let rowsDeleted = 0
19
27
  let materializationsDeleted = 0
20
28
  const nextCandidate = db.query(`
21
- SELECT version_id FROM ontology_sources
22
- WHERE project_id = ? AND status = 'abandoned'
29
+ SELECT version_id FROM ontology_sources AS versions
30
+ WHERE project_id = ? AND status = 'abandoned' AND ${PLAN_PURGED}
23
31
  ORDER BY terminal_at, source_id, materialization_id LIMIT 1
24
32
  `)
25
33
  const deleteRows = db.query(`DELETE FROM ontology_source_rows WHERE rowid IN (
@@ -75,10 +83,18 @@ export function cleanupSourceVersions(
75
83
  WHERE ${retired}
76
84
  AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows WHERE rows.root_id = roots.id)
77
85
  ORDER BY roots.retired_at, roots.id LIMIT ?
78
- )`)
79
- .run(input.projectId, input.terminalBefore, remaining).changes
80
- rowsDeleted += removed
81
- remaining -= removed
86
+ ) RETURNING version_id`)
87
+ .all(input.projectId, input.terminalBefore, remaining) as { readonly version_id: number }[]
88
+ // Each version's root_count follows its roots down, so a drained version is found by index.
89
+ const counts = new Map<number, number>()
90
+ for (const root of removed) counts.set(root.version_id, (counts.get(root.version_id) ?? 0) + 1)
91
+ const drain = db.query(
92
+ "UPDATE ontology_sources SET root_count = root_count - ? WHERE version_id = ?"
93
+ )
94
+ // These commit with the delete: every ontology storage call runs in the caller's BEGIN IMMEDIATE.
95
+ for (const [versionId, count] of counts) drain.run(count, versionId)
96
+ rowsDeleted += removed.length
97
+ remaining -= removed.length
82
98
  }
83
99
  const materializationsDeleted =
84
100
  remaining === 0
@@ -86,9 +102,8 @@ export function cleanupSourceVersions(
86
102
  : db
87
103
  .query(`DELETE FROM ontology_sources WHERE version_id IN (
88
104
  SELECT versions.version_id FROM ontology_sources AS versions
89
- WHERE project_id = ? AND status = 'superseded' AND terminal_at < ?
90
- AND NOT EXISTS (SELECT 1 FROM ontology_source_roots AS roots
91
- WHERE roots.version_id = versions.version_id)
105
+ WHERE project_id = ? AND status = 'superseded' AND root_count = 0 AND terminal_at < ?
106
+ AND ${PLAN_PURGED}
92
107
  ORDER BY terminal_at, source_id, materialization_id LIMIT ?
93
108
  )`)
94
109
  .run(input.projectId, input.terminalBefore, remaining).changes
@@ -22,7 +22,7 @@ import { canonicalJson, type SqliteOntologySourceRow } from "./shared"
22
22
  * version; the candidate's own roots are never rewritten. One live root per source and key holds
23
23
  * because activation, under the source fence, retires every root the candidate replaces.
24
24
  */
25
- const PUBLISHED = "('active', 'superseded')"
25
+ export const PUBLISHED = "('active', 'superseded')"
26
26
 
27
27
  /** The columns `sourceAssertion` reads, over `versions`, `roots` and `rows`. */
28
28
  export const SOURCE_ASSERTION_COLUMNS = `versions.source_id, versions.materialization_id,
@@ -162,10 +162,14 @@ export function stageSourceRoots(
162
162
  }
163
163
  }
164
164
 
165
- /** Retires what the candidate replaces; its own roots go live when its version turns active. */
165
+ /**
166
+ * Retires what the candidate replaces; its own roots go live when its version turns active. Every
167
+ * link those roots assert changes its live source, which commit `commitId` records as touched.
168
+ */
166
169
  export function activateSourceRoots(
167
170
  db: Database,
168
171
  projectId: string,
172
+ commitId: string,
169
173
  candidate: SqliteOntologySourceRow,
170
174
  activation: SourceActivationWrite
171
175
  ): void {
@@ -180,20 +184,40 @@ export function activateSourceRoots(
180
184
  )
181
185
  if (candidate.base_materialization_id !== null && candidate.root_count === 0) return
182
186
  const delta = candidate.base_materialization_id !== null
183
- db.query(`UPDATE ontology_source_roots SET retired_at = ?
184
- WHERE retired_at IS NULL AND deleted = 0
185
- AND version_id IN (SELECT version_id FROM ontology_sources
186
- WHERE project_id = ? AND source_id = ? AND status IN ${PUBLISHED})
187
- ${
188
- delta
189
- ? `AND project_id = ? AND root_key IN (
190
- SELECT root_key FROM ontology_source_roots WHERE version_id = ?)`
191
- : ""
192
- }`).run(
193
- activation.updatedAt,
187
+ const replaced = `retired_at IS NULL AND deleted = 0
188
+ AND version_id IN (SELECT version_id FROM ontology_sources
189
+ WHERE project_id = ? AND source_id = ? AND status IN ${PUBLISHED})
190
+ ${
191
+ delta
192
+ ? `AND project_id = ? AND root_key IN (
193
+ SELECT root_key FROM ontology_source_roots WHERE version_id = ?)`
194
+ : ""
195
+ }`
196
+ const replacedParams = [
194
197
  projectId,
195
198
  activation.source.projectionId,
196
- ...(delta ? [projectId, candidate.version_id] : [])
199
+ ...(delta ? [projectId, candidate.version_id] : []),
200
+ ]
201
+ db.query(`INSERT INTO ontology_commit_touches (project_id, commit_id, entity_kind, identity_key)
202
+ SELECT DISTINCT ?, ?, 'link', json_array(
203
+ rows.source_type_id, rows.source_primary_id, rows.link_id, rows.target_type_id,
204
+ rows.target_primary_id
205
+ )
206
+ FROM (
207
+ SELECT id FROM ontology_source_roots WHERE ${replaced}
208
+ UNION ALL
209
+ SELECT id FROM ontology_source_roots WHERE version_id = ?
210
+ ) AS changed
211
+ CROSS JOIN ontology_source_rows AS rows ON rows.root_id = changed.id
212
+ WHERE rows.entity_kind = 'link'`).run(
213
+ projectId,
214
+ commitId,
215
+ ...replacedParams,
216
+ candidate.version_id
217
+ )
218
+ db.query(`UPDATE ontology_source_roots SET retired_at = ? WHERE ${replaced}`).run(
219
+ activation.updatedAt,
220
+ ...replacedParams
197
221
  )
198
222
  // Deletions are never live; retiring them lets cleanup remove them with their version.
199
223
  db.query(`UPDATE ontology_source_roots SET retired_at = ?
@@ -33,8 +33,6 @@ import type {
33
33
  PurgeAbandonedSourceMaterializationsInput,
34
34
  StageSourceRowsInput,
35
35
  StageSourceRowsResult,
36
- SummarizeTerminalSourceMaterializationsInput,
37
- TerminalSourceMaterializationSummary,
38
36
  } from "@sixb/core/storage"
39
37
  import { yieldSqliteEventLoop } from "../transactions"
40
38
  import {
@@ -317,30 +315,6 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
317
315
  })
318
316
  }
319
317
 
320
- async summarizeTerminal(
321
- input: SummarizeTerminalSourceMaterializationsInput
322
- ): Promise<TerminalSourceMaterializationSummary> {
323
- return this.runRootOperation(() => {
324
- assertNonblank(input.projectId, "Terminal source summary project id")
325
- const summary = this.db
326
- .query(
327
- `
328
- SELECT COUNT(*) AS terminal_count, MIN(terminal_at) AS oldest_terminal_at
329
- FROM ontology_sources
330
- WHERE project_id = ? AND status IN ('superseded', 'abandoned')
331
- `
332
- )
333
- .get(input.projectId) as {
334
- readonly terminal_count: number
335
- readonly oldest_terminal_at: string | null
336
- }
337
- return {
338
- count: summary.terminal_count,
339
- oldestTerminalAt: summary.oldest_terminal_at,
340
- }
341
- })
342
- }
343
-
344
318
  private abandonCandidate(
345
319
  input: AbandonSourceMaterializationCandidateInput
346
320
  ): OntologySourceRecord {
@@ -396,7 +396,7 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
396
396
  async list(input: ListProjectionRunsInput): Promise<ListProjectionRunsResult> {
397
397
  assertProjectionRunNonEmpty(input.projectId, "projectId")
398
398
 
399
- if (input.statuses && input.statuses.length === 0) {
399
+ if (input.statuses?.length === 0 || input.errorCodes?.length === 0) {
400
400
  return { runs: [], hasMore: false, total: 0 }
401
401
  }
402
402
 
@@ -435,6 +435,12 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
435
435
  whereClauses.push(`status IN (${input.statuses.map(() => "?").join(", ")})`)
436
436
  args.push(...input.statuses)
437
437
  }
438
+ if (input.errorCodes) {
439
+ whereClauses.push(
440
+ `json_extract(error, '$.code') IN (${input.errorCodes.map(() => "?").join(", ")})`
441
+ )
442
+ args.push(...input.errorCodes)
443
+ }
438
444
  if (input.startedAfter) {
439
445
  whereClauses.push("COALESCE(started_at, queued_at) >= ?")
440
446
  args.push(input.startedAfter.toISOString())
@@ -175,28 +175,23 @@ export class SqliteRulesStorage implements RulesStorage {
175
175
  input: ListRuleStatesReconciliationPageInput
176
176
  ): Promise<ListRuleStatesReconciliationPageResult> {
177
177
  assertPositiveLimit(input.limit)
178
+ const { after } = input
179
+ const cursor = after ? [after.ruleId, after.objectTypeId, after.primaryId] : []
180
+ // The cursor and order follow the primary key column for column so each page seeks to its
181
+ // start. `subject_kind` is always 'object' (CHECK); filtering on it steers SQLite onto
182
+ // idx_rule_states_project_rule and a full sort per page.
178
183
  const rows = this.db
179
184
  .query(
180
185
  `
181
186
  SELECT project_id, rule_id, subject_kind, object_type_id, primary_id, triggered_at
182
187
  FROM rule_states
183
- WHERE project_id = ? AND subject_kind = 'object'
184
- AND (
185
- ? IS NULL OR
186
- (rule_id, object_type_id, primary_id) > (?, ?, ?)
187
- )
188
- ORDER BY rule_id ASC, object_type_id ASC, primary_id ASC
188
+ WHERE project_id = ?
189
+ ${cursor.length > 0 ? "AND (rule_id, subject_kind, object_type_id, primary_id) > (?, 'object', ?, ?)" : ""}
190
+ ORDER BY rule_id ASC, subject_kind ASC, object_type_id ASC, primary_id ASC
189
191
  LIMIT ?
190
192
  `
191
193
  )
192
- .all(
193
- input.projectId,
194
- input.after?.ruleId ?? null,
195
- input.after?.ruleId ?? null,
196
- input.after?.objectTypeId ?? null,
197
- input.after?.primaryId ?? null,
198
- input.limit + 1
199
- ) as SqliteRuleStateRow[]
194
+ .all(input.projectId, ...cursor, input.limit + 1) as SqliteRuleStateRow[]
200
195
  return reconciliationPage(rows, input.limit)
201
196
  }
202
197
 
package/src/testing.ts CHANGED
@@ -1,16 +1,18 @@
1
+ import type { Database } from "bun:sqlite"
2
+
1
3
  export interface SqliteStorageTestingAdapter {
2
4
  advanceConnectorConnectionTime(durationMs: number): void
5
+ /** The store's write connection, for tests that inspect tables no public read exposes. */
6
+ readonly db: Database
3
7
  }
4
8
 
5
9
  const adapters = new WeakMap<object, SqliteStorageTestingAdapter>()
6
10
 
7
11
  export function registerSqliteStorageTestingAdapter(
8
12
  storage: object,
9
- advanceConnectorConnectionTime: (durationMs: number) => void
13
+ adapter: SqliteStorageTestingAdapter
10
14
  ): void {
11
- adapters.set(storage, {
12
- advanceConnectorConnectionTime,
13
- })
15
+ adapters.set(storage, adapter)
14
16
  }
15
17
 
16
18
  export function getSqliteStorageTestingAdapter(storage: object): SqliteStorageTestingAdapter {