@sixb/sqlite 0.1.16 → 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 (70) 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 +1362 -579
  10. package/dist/index.js.map +27 -25
  11. package/dist/migrations.d.ts.map +1 -1
  12. package/dist/objects/query-compiler.d.ts.map +1 -1
  13. package/dist/objects/reader.d.ts.map +1 -1
  14. package/dist/objects/storage.d.ts.map +1 -1
  15. package/dist/ontology-storage/index.d.ts +2 -0
  16. package/dist/ontology-storage/index.d.ts.map +1 -1
  17. package/dist/ontology-storage/materialization-session.d.ts +17 -37
  18. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  19. package/dist/ontology-storage/materialization-state.d.ts +3 -26
  20. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  21. package/dist/ontology-storage/materialization-writer.d.ts +1 -1
  22. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  23. package/dist/ontology-storage/materializations.d.ts +3 -6
  24. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  25. package/dist/ontology-storage/outbox.d.ts.map +1 -1
  26. package/dist/ontology-storage/replacement-plans.d.ts +88 -0
  27. package/dist/ontology-storage/replacement-plans.d.ts.map +1 -0
  28. package/dist/ontology-storage/shared.d.ts +8 -5
  29. package/dist/ontology-storage/shared.d.ts.map +1 -1
  30. package/dist/ontology-storage/source-cleanup.d.ts.map +1 -1
  31. package/dist/ontology-storage/source-roots.d.ts +15 -2
  32. package/dist/ontology-storage/source-roots.d.ts.map +1 -1
  33. package/dist/ontology-storage/sources.d.ts +1 -2
  34. package/dist/ontology-storage/sources.d.ts.map +1 -1
  35. package/dist/projection-run-storage.d.ts.map +1 -1
  36. package/dist/rules-storage.d.ts.map +1 -1
  37. package/dist/testing.d.ts +4 -1
  38. package/dist/testing.d.ts.map +1 -1
  39. package/package.json +3 -3
  40. package/src/ai-cost-storage.ts +10 -0
  41. package/src/ai-limit-storage.ts +13 -2
  42. package/src/ai-usage-storage.ts +9 -1
  43. package/src/execution-storage.ts +7 -2
  44. package/src/file-download-grant-storage.ts +76 -0
  45. package/src/index.ts +10 -3
  46. package/src/migrations/053-replacement-plans.sql +73 -0
  47. package/src/migrations/054-slim-ontology-outbox.sql +21 -0
  48. package/src/migrations/055-commit-touches.sql +31 -0
  49. package/src/migrations/056-audio-transcription.sql +10 -0
  50. package/src/migrations/057-reranking-model-kind.sql +16 -0
  51. package/src/migrations/058-file-download-grants.sql +21 -0
  52. package/src/migrations/059-drained-source-versions.sql +11 -0
  53. package/src/migrations.ts +115 -0
  54. package/src/objects/query-compiler.ts +5 -0
  55. package/src/objects/reader.ts +1 -0
  56. package/src/objects/storage.ts +5 -11
  57. package/src/ontology-storage/index.ts +6 -1
  58. package/src/ontology-storage/materialization-session.ts +120 -211
  59. package/src/ontology-storage/materialization-state.ts +5 -261
  60. package/src/ontology-storage/materialization-writer.ts +11 -13
  61. package/src/ontology-storage/materializations.ts +133 -210
  62. package/src/ontology-storage/outbox.ts +50 -16
  63. package/src/ontology-storage/replacement-plans.ts +1064 -0
  64. package/src/ontology-storage/shared.ts +17 -12
  65. package/src/ontology-storage/source-cleanup.ts +24 -9
  66. package/src/ontology-storage/source-roots.ts +38 -14
  67. package/src/ontology-storage/sources.ts +0 -26
  68. package/src/projection-run-storage.ts +7 -1
  69. package/src/rules-storage.ts +9 -14
  70. package/src/testing.ts +6 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sixb/sqlite",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "SQLite storage provider for Sixb",
5
5
  "keywords": [
6
6
  "sixb",
@@ -37,10 +37,10 @@
37
37
  "prepack": "bun run build"
38
38
  },
39
39
  "peerDependencies": {
40
- "@sixb/core": "0.1.16"
40
+ "@sixb/core": "0.1.17"
41
41
  },
42
42
  "devDependencies": {
43
- "@sixb/core": "0.1.16",
43
+ "@sixb/core": "0.1.17",
44
44
  "@types/bun": "latest",
45
45
  "typescript": "^5.7.0"
46
46
  },
@@ -246,6 +246,7 @@ const SQLITE_ACCOUNTING_OVERVIEW_SQL = `
246
246
  usage.cache_write_input_tokens,
247
247
  usage.text_output_tokens,
248
248
  usage.reasoning_output_tokens,
249
+ usage.audio_duration_ms,
249
250
  usage.reporting_status,
250
251
  CASE
251
252
  WHEN direct_agent.kind = 'conversation' THEN 'agent'
@@ -330,6 +331,8 @@ const SQLITE_ACCOUNTING_OVERVIEW_SQL = `
330
331
  CAST(SUM(text_output_tokens) AS TEXT) AS text_output_tokens_sum,
331
332
  CAST(COUNT(reasoning_output_tokens) AS TEXT) AS reasoning_output_tokens_count,
332
333
  CAST(SUM(reasoning_output_tokens) AS TEXT) AS reasoning_output_tokens_sum,
334
+ CAST(COUNT(audio_duration_ms) AS TEXT) AS audio_duration_ms_count,
335
+ CAST(SUM(audio_duration_ms) AS TEXT) AS audio_duration_ms_sum,
333
336
  CAST(SUM(CASE WHEN valuation_status = 'rated' THEN 1 ELSE 0 END) AS TEXT)
334
337
  AS rated_call_count,
335
338
  CAST(SUM(CASE WHEN valuation_status = 'unpriceable' THEN 1 ELSE 0 END) AS TEXT)
@@ -382,6 +385,7 @@ interface UsageRow {
382
385
  readonly call_id: string
383
386
  readonly provider_id: string
384
387
  readonly requested_model_id: string
388
+ readonly model_kind: AiModelCallUsageRecord["modelKind"] | null
385
389
  readonly requested_reasoning: string | null
386
390
  readonly response_model_id: string | null
387
391
  readonly response_id: string
@@ -393,6 +397,7 @@ interface UsageRow {
393
397
  readonly cache_write_input_tokens: number | null
394
398
  readonly text_output_tokens: number | null
395
399
  readonly reasoning_output_tokens: number | null
400
+ readonly audio_duration_ms: number | null
396
401
  readonly reporting_status: AiModelCallUsageRecord["usage"]["reportingStatus"]
397
402
  readonly raw_usage: string | null
398
403
  readonly provider_ids: string | null
@@ -447,6 +452,8 @@ interface AggregateRow {
447
452
  readonly text_output_tokens_sum: string | null
448
453
  readonly reasoning_output_tokens_count: string
449
454
  readonly reasoning_output_tokens_sum: string | null
455
+ readonly audio_duration_ms_count: string
456
+ readonly audio_duration_ms_sum: string | null
450
457
  readonly rated_call_count: string
451
458
  readonly unpriceable_call_count: string
452
459
  readonly unvalued_call_count: string
@@ -508,6 +515,7 @@ function aggregateFragmentFromRow(row: AggregateRow): AiAccountingAggregateFragm
508
515
  row.reasoning_output_tokens_count,
509
516
  row.reasoning_output_tokens_sum
510
517
  ),
518
+ audioDurationMs: meterFragment(row.audio_duration_ms_count, row.audio_duration_ms_sum),
511
519
  },
512
520
  costs: {
513
521
  ...(row.amount_currency === null ||
@@ -587,6 +595,7 @@ function usageFromRow(row: UsageRow, requesterGroupIds: readonly string[]): AiMo
587
595
  ? {}
588
596
  : { providerIds: normalizeModelProviderIds(JSON.parse(row.provider_ids)) }),
589
597
  requestedModelId: row.requested_model_id,
598
+ ...(row.model_kind === null ? {} : { modelKind: row.model_kind }),
590
599
  ...(row.requested_reasoning === null
591
600
  ? {}
592
601
  : { requestedReasoning: requestedReasoningFromRow(row.requested_reasoning) }),
@@ -609,6 +618,7 @@ function usageFromRow(row: UsageRow, requesterGroupIds: readonly string[]): AiMo
609
618
  ...(row.reasoning_output_tokens === null
610
619
  ? {}
611
620
  : { reasoningOutputTokens: row.reasoning_output_tokens }),
621
+ ...(row.audio_duration_ms === null ? {} : { audioDurationMs: row.audio_duration_ms }),
612
622
  reportingStatus: row.reporting_status,
613
623
  },
614
624
  ...(row.raw_usage === null
@@ -2,6 +2,7 @@ import type { SQLQueryBindings } from "bun:sqlite"
2
2
  import {
3
3
  type AiLimitAccountingEntry,
4
4
  aiLimitAmountKey,
5
+ aiLimitMeterApplies,
5
6
  aiLimitQuantityFromAmount,
6
7
  aiLimitReservationBuckets,
7
8
  aiLimitReservationRequestKey,
@@ -194,7 +195,12 @@ export class SqliteAiLimitStorage implements AiLimitStorage {
194
195
  const unavailablePolicies: AiLimitPolicyStatus[] = []
195
196
  const unavailableReasons = new Set<"missingEstimate" | "incompleteAccounting">()
196
197
  for (const policy of enabledPolicies) {
197
- if (!subjectKeys.has(aiLimitSubjectKey(policy.subject))) continue
198
+ if (
199
+ !subjectKeys.has(aiLimitSubjectKey(policy.subject)) ||
200
+ !aiLimitMeterApplies(policy.limit.meter, request.modelKind)
201
+ ) {
202
+ continue
203
+ }
198
204
  const limit = normalizeAiLimitAmount(policy.limit)
199
205
  const estimate = estimates.get(aiLimitAmountKey(limit))
200
206
  const status = this.statusForPolicy(policy, request.period)
@@ -235,7 +241,8 @@ export class SqliteAiLimitStorage implements AiLimitStorage {
235
241
  const buckets = aiLimitReservationBuckets(
236
242
  enabledPolicies,
237
243
  request.subjects,
238
- request.estimates
244
+ request.estimates,
245
+ request.modelKind
239
246
  )
240
247
  if (buckets.length === 0) return { status: "notRequired" }
241
248
  for (const bucket of buckets) {
@@ -637,6 +644,7 @@ export class SqliteAiLimitStorage implements AiLimitStorage {
637
644
  SELECT usage.project_id, usage.id AS usage_record_id, usage.execution_id,
638
645
  usage.attempt, usage.call_id, usage.occurred_at,
639
646
  CAST(usage.total_tokens AS TEXT) AS total_tokens,
647
+ usage.model_kind,
640
648
  executions.requested_by_user_id,
641
649
  executions.requested_by_service_account_id,
642
650
  valuations.status AS valuation_status,
@@ -691,6 +699,7 @@ export class SqliteAiLimitStorage implements AiLimitStorage {
691
699
  SELECT usage.project_id, usage.id AS usage_record_id, usage.execution_id,
692
700
  usage.attempt, usage.call_id, usage.occurred_at,
693
701
  CAST(usage.total_tokens AS TEXT) AS total_tokens,
702
+ usage.model_kind,
694
703
  executions.requested_by_user_id,
695
704
  executions.requested_by_service_account_id,
696
705
  valuations.status AS valuation_status,
@@ -776,6 +785,7 @@ interface AiLimitAccountingRow {
776
785
  readonly call_id: string
777
786
  readonly occurred_at: string
778
787
  readonly total_tokens: string | null
788
+ readonly model_kind: AiLimitAccountingEntry["modelKind"] | null
779
789
  readonly requested_by_user_id: string | null
780
790
  readonly requested_by_service_account_id: string | null
781
791
  readonly valuation_status: "rated" | "unpriceable" | null
@@ -863,6 +873,7 @@ function accountingEntryFromRow(
863
873
  attempt: row.attempt,
864
874
  callId: row.call_id,
865
875
  occurredAt: new Date(row.occurred_at),
876
+ ...(row.model_kind === null ? {} : { modelKind: row.model_kind }),
866
877
  ...(totalTokens !== undefined && Number.isSafeInteger(totalTokens) ? { totalTokens } : {}),
867
878
  requesterGroupIds,
868
879
  ...(requester ? { requester } : {}),
@@ -131,6 +131,7 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
131
131
  provider_id,
132
132
  provider_ids,
133
133
  requested_model_id,
134
+ model_kind,
134
135
  requested_reasoning,
135
136
  response_model_id,
136
137
  response_id,
@@ -142,12 +143,13 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
142
143
  cache_write_input_tokens,
143
144
  text_output_tokens,
144
145
  reasoning_output_tokens,
146
+ audio_duration_ms,
145
147
  reporting_status,
146
148
  raw_usage,
147
149
  occurred_at,
148
150
  recorded_at
149
151
  ) VALUES (
150
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
152
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
151
153
  )
152
154
  `
153
155
  )
@@ -160,6 +162,7 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
160
162
  record.providerId,
161
163
  record.providerIds === undefined ? null : JSON.stringify(record.providerIds),
162
164
  record.requestedModelId,
165
+ record.modelKind ?? null,
163
166
  record.requestedReasoning === undefined ? null : JSON.stringify(record.requestedReasoning),
164
167
  record.responseModelId ?? null,
165
168
  record.responseId,
@@ -171,6 +174,7 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
171
174
  record.usage.cacheWriteInputTokens ?? null,
172
175
  record.usage.textOutputTokens ?? null,
173
176
  record.usage.reasoningOutputTokens ?? null,
177
+ record.usage.audioDurationMs ?? null,
174
178
  record.usage.reportingStatus,
175
179
  record.rawUsage === undefined ? null : JSON.stringify(record.rawUsage),
176
180
  record.occurredAt.toISOString(),
@@ -236,6 +240,7 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
236
240
  ? {}
237
241
  : { providerIds: normalizeModelProviderIds(JSON.parse(row.provider_ids)) }),
238
242
  requestedModelId: row.requested_model_id,
243
+ ...(row.model_kind === null ? {} : { modelKind: row.model_kind }),
239
244
  ...(row.requested_reasoning === null
240
245
  ? {}
241
246
  : { requestedReasoning: requestedReasoningFromRow(row.requested_reasoning) }),
@@ -293,6 +298,7 @@ interface AiUsageRow {
293
298
  readonly provider_id: string
294
299
  readonly provider_ids: string | null
295
300
  readonly requested_model_id: string
301
+ readonly model_kind: AiModelCallUsageRecord["modelKind"] | null
296
302
  readonly requested_reasoning: string | null
297
303
  readonly response_model_id: string | null
298
304
  readonly response_id: string
@@ -304,6 +310,7 @@ interface AiUsageRow {
304
310
  readonly cache_write_input_tokens: number | null
305
311
  readonly text_output_tokens: number | null
306
312
  readonly reasoning_output_tokens: number | null
313
+ readonly audio_duration_ms: number | null
307
314
  readonly reporting_status: AiModelCallUsageRecord["usage"]["reportingStatus"]
308
315
  readonly raw_usage: string | null
309
316
  readonly occurred_at: string
@@ -345,6 +352,7 @@ function usageFromRow(row: AiUsageRow): AiModelCallUsageInput {
345
352
  ...(row.reasoning_output_tokens === null
346
353
  ? {}
347
354
  : { reasoningOutputTokens: row.reasoning_output_tokens }),
355
+ ...(row.audio_duration_ms === null ? {} : { audioDurationMs: row.audio_duration_ms }),
348
356
  }
349
357
  }
350
358
 
@@ -113,10 +113,15 @@ export class SqliteExecutionStorage implements ExecutionStorage {
113
113
  )
114
114
  .get(params.projectId, params.id) as SqliteExecutionRow | null
115
115
 
116
- return row ? executionRecordFromStorageRow(toStorageRow(row)) : null
116
+ return row ? sqliteExecutionRecord(row) : null
117
117
  }
118
118
  }
119
119
 
120
+ /** An `executions` row as its record, for any store that reads executions alongside its own rows. */
121
+ export function sqliteExecutionRecord(row: SqliteExecutionRow): ExecutionRecord {
122
+ return executionRecordFromStorageRow(toStorageRow(row))
123
+ }
124
+
120
125
  function toStorageRow(row: SqliteExecutionRow): ExecutionStorageRow {
121
126
  return {
122
127
  projectId: row.project_id,
@@ -152,7 +157,7 @@ function isDuplicateExecutionError(error: unknown): boolean {
152
157
  )
153
158
  }
154
159
 
155
- interface SqliteExecutionRow {
160
+ export interface SqliteExecutionRow {
156
161
  readonly project_id: string
157
162
  readonly id: string
158
163
  readonly executor_kind: ExecutionStorageRow["executorKind"]
@@ -0,0 +1,76 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import { parseFileDownloadGrantRow } from "@sixb/core/internal/storage"
3
+ import type {
4
+ DeleteExpiredFileDownloadGrantsInput,
5
+ FileDownloadGrantRecord,
6
+ FileDownloadGrantStorage,
7
+ FindFileDownloadGrantInput,
8
+ RevokeFileDownloadGrantInput,
9
+ } from "@sixb/core/storage"
10
+ import type { SqliteStoreConnection } from "./transactions"
11
+
12
+ const GRANT_COLUMNS = `
13
+ project_id AS projectId, id, token_hash AS tokenHash, file, execution_id AS executionId,
14
+ created_at AS createdAt, expires_at AS expiresAt, revoked_at AS revokedAt
15
+ `
16
+
17
+ export class SqliteFileDownloadGrantStorage implements FileDownloadGrantStorage {
18
+ private readonly db: Database
19
+
20
+ constructor(connection: SqliteStoreConnection) {
21
+ this.db = connection.db
22
+ }
23
+
24
+ async create(record: FileDownloadGrantRecord): Promise<void> {
25
+ this.db
26
+ .query(
27
+ `INSERT INTO file_download_grants (
28
+ project_id, id, token_hash, file, execution_id, created_at, expires_at
29
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`
30
+ )
31
+ .run(
32
+ record.projectId,
33
+ record.id,
34
+ record.tokenHash,
35
+ JSON.stringify(record.file),
36
+ record.executionId,
37
+ record.createdAt.toISOString(),
38
+ record.expiresAt.toISOString()
39
+ )
40
+ }
41
+
42
+ async findByTokenHash(
43
+ input: FindFileDownloadGrantInput
44
+ ): Promise<FileDownloadGrantRecord | null> {
45
+ const row = this.db
46
+ .query(
47
+ `SELECT ${GRANT_COLUMNS} FROM file_download_grants WHERE project_id = ? AND token_hash = ?`
48
+ )
49
+ .get(input.projectId, input.tokenHash)
50
+ return row ? parseFileDownloadGrantRow(row as Record<string, unknown>) : null
51
+ }
52
+
53
+ async revoke(input: RevokeFileDownloadGrantInput): Promise<FileDownloadGrantRecord | null> {
54
+ const row = this.db
55
+ .query(
56
+ `UPDATE file_download_grants SET revoked_at = COALESCE(revoked_at, ?)
57
+ WHERE project_id = ? AND id = ?
58
+ RETURNING ${GRANT_COLUMNS}`
59
+ )
60
+ .get(input.revokedAt.toISOString(), input.projectId, input.id)
61
+ return row ? parseFileDownloadGrantRow(row as Record<string, unknown>) : null
62
+ }
63
+
64
+ async deleteExpired(input: DeleteExpiredFileDownloadGrantsInput): Promise<number> {
65
+ return this.db
66
+ .query(
67
+ `DELETE FROM file_download_grants WHERE rowid IN (
68
+ SELECT rowid FROM file_download_grants
69
+ WHERE project_id = ? AND expires_at < ?
70
+ ORDER BY expires_at
71
+ LIMIT ?
72
+ )`
73
+ )
74
+ .run(input.projectId, input.expiredBefore.toISOString(), input.limit).changes
75
+ }
76
+ }
package/src/index.ts CHANGED
@@ -39,6 +39,7 @@ import { SqliteAiUsageStorage } from "./ai-usage-storage"
39
39
  import { SqliteAuthStorage } from "./auth-storage"
40
40
  import { SqliteConnectorConnectionStorage } from "./connector-connection-storage"
41
41
  import { SqliteExecutionStorage } from "./execution-storage"
42
+ import { SqliteFileDownloadGrantStorage } from "./file-download-grant-storage"
42
43
  import { SqliteFileUploadSessionStorage } from "./file-upload-session-storage"
43
44
  import {
44
45
  createSqliteStorageMigrators,
@@ -108,6 +109,7 @@ export class SqliteStorage implements MigrationCapableStorage {
108
109
  readonly shareSessions: ShareSessionStorage
109
110
  readonly connectorConnections: SqliteConnectorConnectionStorage
110
111
  readonly fileUploadSessions: SqliteFileUploadSessionStorage
112
+ readonly fileDownloadGrants: SqliteFileDownloadGrantStorage
111
113
  readonly migrators: readonly StorageMigrator[]
112
114
 
113
115
  private readonly connection: SqliteStoreConnection
@@ -175,10 +177,13 @@ export class SqliteStorage implements MigrationCapableStorage {
175
177
  this.shareSessions = createOperationScopedFacade(stores.shareSessions, scope)
176
178
  this.connectorConnections = createOperationScopedFacade(stores.connectorConnections, scope)
177
179
  this.fileUploadSessions = createOperationScopedFacade(stores.fileUploadSessions, scope)
180
+ this.fileDownloadGrants = createOperationScopedFacade(stores.fileDownloadGrants, scope)
178
181
  this.migrators = options.path ? createSqliteStorageMigrators(options.path) : []
179
- registerSqliteStorageTestingAdapter(this, (durationMs) =>
180
- stores.connectorConnections.advanceTimeForTesting(durationMs)
181
- )
182
+ registerSqliteStorageTestingAdapter(this, {
183
+ advanceConnectorConnectionTime: (durationMs) =>
184
+ stores.connectorConnections.advanceTimeForTesting(durationMs),
185
+ db: this.connection.db,
186
+ })
182
187
  }
183
188
 
184
189
  async ping(): Promise<void> {
@@ -337,6 +342,7 @@ function createSqliteStores(
337
342
  shareSessions,
338
343
  connectorConnections: new SqliteConnectorConnectionStorage(connection),
339
344
  fileUploadSessions: new SqliteFileUploadSessionStorage(connection),
345
+ fileDownloadGrants: new SqliteFileDownloadGrantStorage(connection),
340
346
  }
341
347
  }
342
348
 
@@ -362,6 +368,7 @@ interface SqliteStoreSet {
362
368
  readonly shareSessions: SqliteShareSessionStorage
363
369
  readonly connectorConnections: SqliteConnectorConnectionStorage
364
370
  readonly fileUploadSessions: SqliteFileUploadSessionStorage
371
+ readonly fileDownloadGrants: SqliteFileDownloadGrantStorage
365
372
  }
366
373
 
367
374
  export { migrateSqliteStorage } from "./migrations"
@@ -0,0 +1,73 @@
1
+ -- The plan of a ready replacement candidate, built outside its commit transaction. Its rows are a
2
+ -- cache the commit verifies again, keyed by the candidate's source version, which outlives it:
3
+ -- maintenance deletes a plan once its candidate is no longer ready, and only then the version.
4
+ CREATE TABLE ontology_replacement_plans (
5
+ version_id INTEGER PRIMARY KEY REFERENCES ontology_sources (version_id) ON DELETE RESTRICT,
6
+ project_id TEXT NOT NULL,
7
+ -- The commit the plan's work carries, at the time fixed when the plan opened.
8
+ commit_id TEXT NOT NULL,
9
+ committed_at TEXT NOT NULL,
10
+ -- The active source the plan replaces, NULL when none was: a plan resumed once that moved
11
+ -- starts over.
12
+ replaced_materialization_id TEXT,
13
+ replaced_last_commit_id TEXT,
14
+ -- The last commit row every planned identity is known fresh against.
15
+ watermark INTEGER NOT NULL
16
+ );
17
+
18
+ -- One row per entity the plan decides. `read_revision` is the revision of the inputs its state was
19
+ -- last read at; `planned_revision` the one its work was planned from, NULL while still to plan.
20
+ -- `classified` and `change` summarize that work for the commit's change counts.
21
+ CREATE TABLE ontology_replacement_plan_identities (
22
+ version_id INTEGER NOT NULL,
23
+ entity_kind TEXT NOT NULL CHECK (entity_kind IN ('object', 'link')),
24
+ identity_key TEXT NOT NULL,
25
+ sort_key TEXT NOT NULL,
26
+ diff_required INTEGER NOT NULL CHECK (diff_required IN (0, 1)),
27
+ read_revision TEXT,
28
+ planned_revision TEXT,
29
+ classified INTEGER NOT NULL DEFAULT 0 CHECK (classified IN (0, 1)),
30
+ change TEXT CHECK (change IN ('created', 'updated', 'deleted')),
31
+ PRIMARY KEY (version_id, entity_kind, identity_key)
32
+ );
33
+ -- The identities still to plan, in planning order: a round that plans a few again finds them
34
+ -- without walking the rest.
35
+ CREATE INDEX idx_ontology_replacement_plan_identities_unplanned
36
+ ON ontology_replacement_plan_identities (version_id, entity_kind, sort_key)
37
+ WHERE planned_revision IS NULL;
38
+
39
+ -- Planned work, with the same columns as a session's work table plus the identity it belongs to.
40
+ CREATE TABLE ontology_replacement_plan_work (
41
+ work_id INTEGER NOT NULL,
42
+ entity_kind TEXT NOT NULL,
43
+ identity_key TEXT NOT NULL,
44
+ record_key TEXT NOT NULL,
45
+ unique_key TEXT NOT NULL,
46
+ kind TEXT NOT NULL,
47
+ lane TEXT NOT NULL,
48
+ major_order INTEGER NOT NULL,
49
+ minor_order INTEGER NOT NULL,
50
+ sort_one TEXT NOT NULL,
51
+ sort_two TEXT NOT NULL,
52
+ classification_entity_kind TEXT,
53
+ classification_identity_key TEXT,
54
+ cardinality_view TEXT,
55
+ cardinality_occupied INTEGER,
56
+ cardinality_source_type_id TEXT,
57
+ cardinality_source_primary_id TEXT,
58
+ cardinality_link_id TEXT,
59
+ cardinality_target_type_id TEXT,
60
+ cardinality_target_primary_id TEXT,
61
+ payload TEXT NOT NULL CHECK (json_valid(payload)),
62
+ PRIMARY KEY (work_id, record_key),
63
+ UNIQUE (work_id, unique_key)
64
+ );
65
+ CREATE INDEX idx_ontology_replacement_plan_work_identity
66
+ ON ontology_replacement_plan_work (work_id, entity_kind, identity_key);
67
+ -- The few objects whose existence the plan flips, which link expansion starts from.
68
+ CREATE INDEX idx_ontology_replacement_plan_work_incident
69
+ ON ontology_replacement_plan_work (work_id) WHERE kind = 'incident-object';
70
+ CREATE INDEX idx_ontology_replacement_plan_work_lane
71
+ ON ontology_replacement_plan_work (
72
+ work_id, lane, major_order, minor_order, sort_one, sort_two, record_key
73
+ );
@@ -0,0 +1,21 @@
1
+ -- An outbox event stores what changed and nothing it can be rebuilt from: delivery rebuilds the
2
+ -- event from its commit and the execution that commit ran under. The commit also stops copying
3
+ -- `requested_by` and `executor` from that execution (047): the execution is canonical, immutable,
4
+ -- and already referenced by the commit. The migration step checks beforehand that every stored
5
+ -- event rebuilds exactly as it was stored.
6
+ --
7
+ -- SQLite cannot add a NOT NULL column without a default; every insert supplies the event, and the
8
+ -- UPDATE below replaces the placeholder on every row.
9
+ ALTER TABLE ontology_outbox ADD COLUMN event TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(event));
10
+ UPDATE ontology_outbox SET event = json_object(
11
+ 'payload', CASE
12
+ WHEN envelope ->> '$.type' IN ('object.created', 'link.created')
13
+ THEN json_remove(envelope -> '$.payload', '$.propertyChanges')
14
+ ELSE envelope -> '$.payload'
15
+ END,
16
+ 'type', envelope ->> '$.type'
17
+ );
18
+ ALTER TABLE ontology_outbox DROP COLUMN envelope;
19
+
20
+ ALTER TABLE ontology_commits DROP COLUMN requested_by;
21
+ ALTER TABLE ontology_commits DROP COLUMN executor;
@@ -0,0 +1,31 @@
1
+ -- Every commit records the entities whose plan inputs it changed, so a replacement plan's refresh
2
+ -- rechecks only what the commits since it was last fresh touched, instead of the whole plan.
3
+ CREATE TABLE ontology_commit_touches (
4
+ project_id TEXT NOT NULL,
5
+ commit_id TEXT NOT NULL,
6
+ entity_kind TEXT NOT NULL CHECK (entity_kind IN ('object', 'link', 'scope')),
7
+ -- `objectRefKey`, `linkRefKey` or `linkScopeKey`.
8
+ identity_key TEXT NOT NULL
9
+ );
10
+ CREATE INDEX idx_ontology_commit_touches_commit ON ontology_commit_touches (project_id, commit_id);
11
+
12
+ -- Existing plans predate the touches of the commits since their watermark: they plan again.
13
+ DELETE FROM ontology_replacement_plan_work;
14
+ DELETE FROM ontology_replacement_plan_identities;
15
+ DELETE FROM ontology_replacement_plans;
16
+
17
+ -- Link expansion starts only from what changed since it last ran: `expansions` counts the runs,
18
+ -- and an identity's `expand_at` is the run that has yet to start from it (an object once planned,
19
+ -- a link once it needs a diff). `source_key` and `target_key` find a link from either endpoint.
20
+ ALTER TABLE ontology_replacement_plans ADD COLUMN expansions INTEGER NOT NULL DEFAULT 0;
21
+ ALTER TABLE ontology_replacement_plan_identities ADD COLUMN expand_at INTEGER;
22
+ ALTER TABLE ontology_replacement_plan_identities ADD COLUMN source_key TEXT;
23
+ ALTER TABLE ontology_replacement_plan_identities ADD COLUMN target_key TEXT;
24
+ CREATE INDEX idx_ontology_replacement_plan_identities_expand
25
+ ON ontology_replacement_plan_identities (version_id, entity_kind, expand_at);
26
+ CREATE INDEX idx_ontology_replacement_plan_identities_source
27
+ ON ontology_replacement_plan_identities (version_id, source_key)
28
+ WHERE source_key IS NOT NULL;
29
+ CREATE INDEX idx_ontology_replacement_plan_identities_target
30
+ ON ontology_replacement_plan_identities (version_id, target_key)
31
+ WHERE target_key IS NOT NULL;
@@ -0,0 +1,10 @@
1
+ ALTER TABLE ai_model_call_usage
2
+ ADD COLUMN audio_duration_ms INTEGER
3
+ CHECK (audio_duration_ms IS NULL OR audio_duration_ms >= 0);
4
+
5
+ ALTER TABLE ai_model_call_usage
6
+ ADD COLUMN model_kind TEXT
7
+ CHECK (
8
+ model_kind IS NULL
9
+ OR model_kind IN ('language', 'image', 'video', 'embedding', 'decision', 'transcription')
10
+ );
@@ -0,0 +1,16 @@
1
+ -- SQLite cannot alter a CHECK constraint. Swapping the column avoids rebuilding a table that
2
+ -- usage groups, costs and limit reservations reference.
3
+ ALTER TABLE ai_model_call_usage
4
+ ADD COLUMN model_kind_next TEXT
5
+ CHECK (
6
+ model_kind_next IS NULL
7
+ OR model_kind_next IN (
8
+ 'language', 'image', 'video', 'embedding', 'decision', 'transcription', 'reranking'
9
+ )
10
+ );
11
+
12
+ UPDATE ai_model_call_usage SET model_kind_next = model_kind;
13
+
14
+ ALTER TABLE ai_model_call_usage DROP COLUMN model_kind;
15
+
16
+ ALTER TABLE ai_model_call_usage RENAME COLUMN model_kind_next TO model_kind;
@@ -0,0 +1,21 @@
1
+ -- URLs issued by blobs.createDownloadUrl(). Only the SHA-256 of each URL's token is stored.
2
+ -- Timestamps are ISO-8601 UTC text and compare lexicographically.
3
+ CREATE TABLE file_download_grants (
4
+ project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
5
+ id TEXT NOT NULL CHECK (length(trim(id)) > 0),
6
+ token_hash TEXT NOT NULL CHECK (
7
+ length(token_hash) = 64
8
+ AND token_hash = lower(token_hash)
9
+ AND token_hash NOT GLOB '*[^0-9a-f]*'
10
+ ),
11
+ file TEXT NOT NULL CHECK (json_type(file) = 'object'),
12
+ execution_id TEXT NOT NULL CHECK (length(trim(execution_id)) > 0),
13
+ created_at TEXT NOT NULL,
14
+ expires_at TEXT NOT NULL,
15
+ revoked_at TEXT,
16
+ PRIMARY KEY (project_id, id),
17
+ UNIQUE (project_id, token_hash),
18
+ CHECK (expires_at > created_at)
19
+ );
20
+
21
+ CREATE INDEX idx_file_download_grants_expiry ON file_download_grants (project_id, expires_at);
@@ -0,0 +1,11 @@
1
+ -- A superseded version's `root_count` counts the roots it still holds: cleanup lowers it as it
2
+ -- deletes them. A version that reaches zero is found through this index, instead of every
3
+ -- superseded version being walked and probed for roots on every maintenance pass.
4
+ UPDATE ontology_sources
5
+ SET root_count = (SELECT count(*) FROM ontology_source_roots AS roots
6
+ WHERE roots.version_id = ontology_sources.version_id)
7
+ WHERE status = 'superseded' AND root_count <> (SELECT count(*) FROM ontology_source_roots AS roots
8
+ WHERE roots.version_id = ontology_sources.version_id);
9
+ CREATE INDEX idx_ontology_sources_drained
10
+ ON ontology_sources (project_id, terminal_at, source_id, materialization_id)
11
+ WHERE status = 'superseded' AND root_count = 0;