@sixb/sqlite 0.1.0 → 0.1.2

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 (110) hide show
  1. package/README.md +18 -13
  2. package/dist/action-run-storage.d.ts +5 -2
  3. package/dist/action-run-storage.d.ts.map +1 -1
  4. package/dist/agents/agent-storage.d.ts +4 -2
  5. package/dist/agents/agent-storage.d.ts.map +1 -1
  6. package/dist/agents/rows.d.ts +3 -10
  7. package/dist/agents/rows.d.ts.map +1 -1
  8. package/dist/agents/runs.d.ts +3 -2
  9. package/dist/agents/runs.d.ts.map +1 -1
  10. package/dist/ai-usage-storage.d.ts +22 -0
  11. package/dist/ai-usage-storage.d.ts.map +1 -0
  12. package/dist/auth-storage/service-accounts.d.ts.map +1 -1
  13. package/dist/auth-storage/shared.d.ts +1 -10
  14. package/dist/auth-storage/shared.d.ts.map +1 -1
  15. package/dist/connector-connection-storage.d.ts +9 -0
  16. package/dist/connector-connection-storage.d.ts.map +1 -0
  17. package/dist/execution-storage.d.ts +14 -0
  18. package/dist/execution-storage.d.ts.map +1 -0
  19. package/dist/index.d.ts +13 -9
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +4114 -1149
  22. package/dist/index.js.map +30 -27
  23. package/dist/latest-run-query.d.ts.map +1 -1
  24. package/dist/migrations.d.ts +0 -1
  25. package/dist/migrations.d.ts.map +1 -1
  26. package/dist/ontology-storage/materialization-session.d.ts +50 -8
  27. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  28. package/dist/ontology-storage/materialization-state.d.ts +4 -5
  29. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  30. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  31. package/dist/ontology-storage/materializations.d.ts +0 -2
  32. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  33. package/dist/ontology-storage/outbox.d.ts.map +1 -1
  34. package/dist/ontology-storage/shared.d.ts +3 -3
  35. package/dist/ontology-storage/shared.d.ts.map +1 -1
  36. package/dist/ontology-storage/sources.d.ts.map +1 -1
  37. package/dist/pipeline-run-storage.d.ts +7 -2
  38. package/dist/pipeline-run-storage.d.ts.map +1 -1
  39. package/dist/projection-run-storage.d.ts +9 -2
  40. package/dist/projection-run-storage.d.ts.map +1 -1
  41. package/dist/run-list-query.d.ts +1 -1
  42. package/dist/run-list-query.d.ts.map +1 -1
  43. package/dist/sync-run-storage.d.ts +7 -2
  44. package/dist/sync-run-storage.d.ts.map +1 -1
  45. package/dist/testing.d.ts +6 -0
  46. package/dist/testing.d.ts.map +1 -0
  47. package/dist/transactions.d.ts +3 -0
  48. package/dist/transactions.d.ts.map +1 -1
  49. package/dist/webhook-run-storage.d.ts +9 -2
  50. package/dist/webhook-run-storage.d.ts.map +1 -1
  51. package/dist/workflow-run-storage.d.ts +7 -3
  52. package/dist/workflow-run-storage.d.ts.map +1 -1
  53. package/package.json +4 -3
  54. package/src/action-run-storage.ts +51 -74
  55. package/src/agents/agent-storage.ts +5 -3
  56. package/src/agents/rows.ts +8 -40
  57. package/src/agents/runs.ts +27 -21
  58. package/src/ai-usage-storage.ts +324 -0
  59. package/src/auth-storage/service-accounts.ts +14 -21
  60. package/src/auth-storage/shared.ts +0 -22
  61. package/src/connector-connection-storage.ts +784 -0
  62. package/src/execution-storage.ts +158 -0
  63. package/src/index.ts +63 -22
  64. package/src/latest-run-query.ts +7 -1
  65. package/src/migrations/002-workflow-run-output.sql +27 -0
  66. package/src/migrations/003-merge-sync-runs.sql +71 -0
  67. package/src/migrations/004-executions.sql +142 -0
  68. package/src/migrations/005-workflow-executions.sql +7 -0
  69. package/src/migrations/006-narrow-ontology-source-root-index.sql +4 -0
  70. package/src/migrations/007-split-overrides.sql +154 -0
  71. package/src/migrations/008-action-executions.sql +4 -0
  72. package/src/migrations/009-agent-executions.sql +115 -0
  73. package/src/migrations/010-ai-usage-accounting-foundation.sql +74 -0
  74. package/src/migrations/011-sync-failure-record.sql +22 -0
  75. package/src/migrations/012-pipeline-failure-record.sql +45 -0
  76. package/src/migrations/013-workflow-failure-record.sql +45 -0
  77. package/src/migrations/014-agent-failure-record.sql +59 -0
  78. package/src/migrations/015-projection-failure-record.sql +20 -0
  79. package/src/migrations/016-webhook-run-failure-record.sql +19 -0
  80. package/src/migrations/017-action-failure-record.sql +65 -0
  81. package/src/migrations/018-ontology-outbox-failure-record.sql +80 -0
  82. package/src/migrations/019-webhook-delivery-failure-record.sql +62 -0
  83. package/src/migrations/020-drop-run-usage-projections.sql +7 -0
  84. package/src/migrations/021-sync-pipeline-executions.sql +101 -0
  85. package/src/migrations/022-projection-executions.sql +244 -0
  86. package/src/migrations/023-webhook-executions.sql +51 -0
  87. package/src/migrations/024-ontology-commit-executions.sql +58 -0
  88. package/src/migrations/025-connector-connections.sql +257 -0
  89. package/src/migrations.ts +126 -5
  90. package/src/ontology-storage/materialization-session.ts +317 -143
  91. package/src/ontology-storage/materialization-state.ts +398 -150
  92. package/src/ontology-storage/materialization-writer.ts +142 -67
  93. package/src/ontology-storage/materializations.ts +67 -141
  94. package/src/ontology-storage/outbox.ts +10 -3
  95. package/src/ontology-storage/shared.ts +10 -4
  96. package/src/ontology-storage/sources.ts +4 -1
  97. package/src/pipeline-run-storage.ts +128 -43
  98. package/src/projection-run-storage.ts +209 -97
  99. package/src/run-list-query.ts +9 -4
  100. package/src/sync-run-storage.ts +120 -34
  101. package/src/testing.ts +20 -0
  102. package/src/transactions.ts +10 -3
  103. package/src/webhook-run-storage.ts +118 -74
  104. package/src/workflow-run-storage.ts +124 -133
  105. package/dist/validation.d.ts +0 -5
  106. package/dist/validation.d.ts.map +0 -1
  107. package/dist/webhook-delivery-storage.d.ts +0 -27
  108. package/dist/webhook-delivery-storage.d.ts.map +0 -1
  109. package/src/validation.ts +0 -30
  110. package/src/webhook-delivery-storage.ts +0 -272
@@ -18,6 +18,7 @@ import {
18
18
  import {
19
19
  appendScopeSnapshot,
20
20
  finishScopeAccumulator,
21
+ linkSlotOverrideValue,
21
22
  startScopeAccumulator,
22
23
  } from "@sixb/core/internal/ontology-storage-provider"
23
24
  import type {
@@ -27,6 +28,7 @@ import type {
27
28
  SourceReplacementLinkState,
28
29
  SourceReplacementObjectState,
29
30
  StoredLinkOverride,
31
+ StoredLinkSlotOverride,
30
32
  StoredObjectOverride,
31
33
  StoredSourceAssertion,
32
34
  StoredSourceLinkAssertion,
@@ -38,8 +40,8 @@ import {
38
40
  linkRefFromColumns,
39
41
  objectRefFromColumns,
40
42
  parseJson,
41
- type SqliteOntologyOverrideRow,
42
43
  type SqliteOntologySourceAssertionRow,
44
+ type SqliteStoredOverrideRow,
43
45
  } from "./shared"
44
46
 
45
47
  export const SQLITE_MATERIALIZATION_WORK_TABLE = "ontology_materialization_work"
@@ -81,12 +83,12 @@ interface LinkScopeRow extends EffectiveLinkRow {
81
83
  readonly scope_sort_key: string
82
84
  }
83
85
 
84
- interface ObjectOverrideRow extends SqliteOntologyOverrideRow {
86
+ interface ObjectOverrideRow extends SqliteStoredOverrideRow {
85
87
  readonly object_type_id: string
86
88
  readonly primary_id: string
87
89
  }
88
90
 
89
- interface LinkOverrideRow extends SqliteOntologyOverrideRow {
91
+ interface LinkOverrideRow extends SqliteStoredOverrideRow {
90
92
  readonly source_type_id: string
91
93
  readonly source_primary_id: string
92
94
  readonly link_id: string
@@ -94,6 +96,17 @@ interface LinkOverrideRow extends SqliteOntologyOverrideRow {
94
96
  readonly target_primary_id: string
95
97
  }
96
98
 
99
+ interface LinkSlotOverrideRow {
100
+ readonly source_type_id: string
101
+ readonly source_primary_id: string
102
+ readonly link_id: string
103
+ readonly target_type_id: string
104
+ readonly target_primary_id: string
105
+ readonly value: string
106
+ readonly last_commit_id: string
107
+ readonly updated_at: string
108
+ }
109
+
97
110
  interface ReplacementSources {
98
111
  readonly owned: ReadonlySet<string>
99
112
  readonly byMaterialization: ReadonlyMap<string, ReadonlyMap<string, StoredSourceAssertion>>
@@ -122,6 +135,18 @@ interface ReplacementIdentityInput {
122
135
  readonly pageRows: number
123
136
  }
124
137
 
138
+ interface ReplacementWorkRow {
139
+ readonly entity_kind: "object" | "link"
140
+ readonly identity_key: string
141
+ readonly sort_key: string
142
+ readonly diff_required: number
143
+ }
144
+
145
+ interface ReplacementCursor {
146
+ readonly sortKey: string
147
+ readonly identityKey: string
148
+ }
149
+
125
150
  export class SqliteMaterializationStateReader {
126
151
  constructor(
127
152
  private readonly db: Database,
@@ -142,33 +167,43 @@ export class SqliteMaterializationStateReader {
142
167
  json_extract(value, '$.primaryId') AS primary_id
143
168
  FROM json_each(?)
144
169
  `
170
+ // Keep the JSON request set on the outer side of each lookup. SQLite cannot estimate the
171
+ // cardinality of json_each() and otherwise scans every project row once per requested key.
172
+ // CROSS JOIN deliberately fixes the loop order so the existing composite indexes are used.
145
173
  const effectiveRows = this.db
146
174
  .query(
147
175
  `WITH requested AS (${requestedObjects})
148
- SELECT objects.* FROM objects
149
- JOIN requested USING (object_type_id, primary_id)
150
- WHERE objects.project_id = ?`
176
+ SELECT objects.* FROM requested
177
+ CROSS JOIN objects
178
+ ON objects.project_id = ?
179
+ AND objects.object_type_id = requested.object_type_id
180
+ AND objects.primary_id = requested.primary_id`
151
181
  )
152
182
  .all(requested, this.projectId) as EffectiveObjectRow[]
153
183
  const sourceRows = this.db
154
184
  .query(
155
185
  `WITH requested AS (${requestedObjects})
156
- SELECT rows.* FROM ontology_source_rows AS rows
157
- JOIN requested USING (object_type_id, primary_id)
186
+ SELECT rows.* FROM requested
187
+ CROSS JOIN ontology_source_rows AS rows
188
+ ON rows.project_id = ?
189
+ AND rows.entity_kind = 'object'
190
+ AND rows.object_type_id = requested.object_type_id
191
+ AND rows.primary_id = requested.primary_id
158
192
  JOIN ontology_sources AS sources
159
193
  ON sources.project_id = rows.project_id
160
194
  AND sources.source_id = rows.source_id
161
195
  AND sources.materialization_id = rows.materialization_id
162
- WHERE rows.project_id = ? AND rows.entity_kind = 'object'
163
- AND sources.status = 'active'`
196
+ WHERE sources.status = 'active'`
164
197
  )
165
198
  .all(requested, this.projectId) as SqliteOntologySourceAssertionRow[]
166
199
  const overrideRows = this.db
167
200
  .query(
168
201
  `WITH requested AS (${requestedObjects})
169
- SELECT overrides.* FROM ontology_overrides AS overrides
170
- JOIN requested USING (object_type_id, primary_id)
171
- WHERE overrides.project_id = ? AND overrides.entity_kind = 'object'`
202
+ SELECT overrides.* FROM requested
203
+ CROSS JOIN ontology_object_overrides AS overrides
204
+ ON overrides.project_id = ?
205
+ AND overrides.object_type_id = requested.object_type_id
206
+ AND overrides.primary_id = requested.primary_id`
172
207
  )
173
208
  .all(requested, this.projectId) as ObjectOverrideRow[]
174
209
  const telemetryRows = this.db
@@ -180,11 +215,11 @@ export class SqliteMaterializationStateReader {
180
215
  timeseries.property_id
181
216
  ORDER BY timeseries.at DESC
182
217
  ) AS rank
183
- FROM timeseries
184
- JOIN requested
185
- ON requested.object_type_id = timeseries.object_type_id
186
- AND requested.primary_id = timeseries.object_id
187
- WHERE timeseries.project_id = ?
218
+ FROM requested
219
+ CROSS JOIN timeseries
220
+ ON timeseries.project_id = ?
221
+ AND timeseries.object_type_id = requested.object_type_id
222
+ AND timeseries.object_id = requested.primary_id
188
223
  )
189
224
  SELECT * FROM ranked WHERE rank = 1`
190
225
  )
@@ -261,36 +296,61 @@ export class SqliteMaterializationStateReader {
261
296
  const effectiveRows = this.db
262
297
  .query(
263
298
  `WITH requested AS (${effectiveRequest})
264
- SELECT links.* FROM links
265
- JOIN requested USING (source_type_id, source_id, link_id, target_type_id, target_id)
266
- WHERE links.project_id = ?`
299
+ SELECT links.* FROM requested
300
+ CROSS JOIN links
301
+ ON links.project_id = ?
302
+ AND links.source_type_id = requested.source_type_id
303
+ AND links.source_id = requested.source_id
304
+ AND links.link_id = requested.link_id
305
+ AND links.target_type_id = requested.target_type_id
306
+ AND links.target_id = requested.target_id`
267
307
  )
268
308
  .all(requested, this.projectId) as EffectiveLinkRow[]
269
309
  const sourceRows = this.db
270
310
  .query(
271
311
  `WITH requested AS (${sourceRequest})
272
- SELECT rows.* FROM ontology_source_rows AS rows
273
- JOIN requested USING (
274
- source_type_id, source_primary_id, link_id, target_type_id, target_primary_id
275
- )
312
+ SELECT rows.* FROM requested
313
+ CROSS JOIN ontology_source_rows AS rows
314
+ ON rows.project_id = ?
315
+ AND rows.entity_kind = 'link'
316
+ AND rows.source_type_id = requested.source_type_id
317
+ AND rows.source_primary_id = requested.source_primary_id
318
+ AND rows.link_id = requested.link_id
319
+ AND rows.target_type_id = requested.target_type_id
320
+ AND rows.target_primary_id = requested.target_primary_id
276
321
  JOIN ontology_sources AS sources
277
322
  ON sources.project_id = rows.project_id
278
323
  AND sources.source_id = rows.source_id
279
324
  AND sources.materialization_id = rows.materialization_id
280
- WHERE rows.project_id = ? AND rows.entity_kind = 'link'
281
- AND sources.status = 'active'`
325
+ WHERE sources.status = 'active'`
282
326
  )
283
327
  .all(requested, this.projectId) as SqliteOntologySourceAssertionRow[]
284
328
  const overrideRows = this.db
285
329
  .query(
286
330
  `WITH requested AS (${sourceRequest})
287
- SELECT overrides.* FROM ontology_overrides AS overrides
288
- JOIN requested USING (
289
- source_type_id, source_primary_id, link_id, target_type_id, target_primary_id
290
- )
291
- WHERE overrides.project_id = ? AND overrides.entity_kind = 'link'`
331
+ SELECT overrides.* FROM requested
332
+ CROSS JOIN ontology_link_overrides AS overrides
333
+ ON overrides.project_id = ?
334
+ AND overrides.identity_kind = 'edge'
335
+ AND overrides.source_type_id = requested.source_type_id
336
+ AND overrides.source_primary_id = requested.source_primary_id
337
+ AND overrides.link_id = requested.link_id
338
+ AND overrides.target_type_id = requested.target_type_id
339
+ AND overrides.target_primary_id = requested.target_primary_id`
292
340
  )
293
341
  .all(requested, this.projectId) as LinkOverrideRow[]
342
+ const slotOverrideRows = this.db
343
+ .query(
344
+ `WITH requested AS (${sourceRequest})
345
+ SELECT overrides.* FROM requested
346
+ CROSS JOIN ontology_link_overrides AS overrides
347
+ ON overrides.project_id = ?
348
+ AND overrides.identity_kind = 'slot'
349
+ AND overrides.source_type_id = requested.source_type_id
350
+ AND overrides.source_primary_id = requested.source_primary_id
351
+ AND overrides.link_id = requested.link_id`
352
+ )
353
+ .all(requested, this.projectId) as LinkSlotOverrideRow[]
294
354
  const effective = new Map(
295
355
  effectiveRows.map((row) => [linkRefKey(linkRefFromColumns(row)), row] as const)
296
356
  )
@@ -300,6 +360,12 @@ export class SqliteMaterializationStateReader {
300
360
  (row) => [linkRefKey(linkRefFromOverrideColumns(row)), storedLinkOverride(row)] as const
301
361
  )
302
362
  )
363
+ const slotOverrides = new Map(
364
+ slotOverrideRows.map((row) => {
365
+ const stored = storedLinkSlotOverride(row)
366
+ return [linkScopeSortKey(stored.ref.source, stored.ref.linkId), stored] as const
367
+ })
368
+ )
303
369
  return refs.map((ref) => {
304
370
  const key = linkRefKey(ref)
305
371
  const effectiveRow = effective.get(key)
@@ -307,6 +373,7 @@ export class SqliteMaterializationStateReader {
307
373
  ref: structuredClone(ref),
308
374
  source: sourceLink(sources.get(projectionEntityKey({ kind: "link", ref }))),
309
375
  override: overrides.get(key) ?? null,
376
+ slotOverride: slotOverrides.get(linkScopeSortKey(ref.source, ref.linkId)) ?? null,
310
377
  effective: effectiveRow ? effectiveLinkSnapshot(effectiveRow) : null,
311
378
  }
312
379
  })
@@ -337,9 +404,13 @@ export class SqliteMaterializationStateReader {
337
404
  json_extract(value, '$.at') AS at
338
405
  FROM json_each(?)
339
406
  )
340
- SELECT timeseries.* FROM timeseries
341
- JOIN requested USING (object_type_id, object_id, property_id, at)
342
- WHERE timeseries.project_id = ?`
407
+ SELECT timeseries.* FROM requested
408
+ CROSS JOIN timeseries
409
+ ON timeseries.project_id = ?
410
+ AND timeseries.object_type_id = requested.object_type_id
411
+ AND timeseries.object_id = requested.object_id
412
+ AND timeseries.property_id = requested.property_id
413
+ AND timeseries.at = requested.at`
343
414
  )
344
415
  .all(requested, this.projectId) as TelemetryRow[]
345
416
  const found = new Map(
@@ -367,9 +438,59 @@ export class SqliteMaterializationStateReader {
367
438
  const accumulators = new Map(
368
439
  scopes.map(({ source, linkId }) => {
369
440
  const key = linkScopeSortKey(source, linkId)
370
- return [key, startScopeAccumulator(source, linkId, key)] as const
441
+ return [key, startScopeAccumulator(source, linkId)] as const
371
442
  })
372
443
  )
444
+ const requestedJson = canonicalJson(requested)
445
+ const sourceRows = this.db
446
+ .query(
447
+ `WITH requested AS (
448
+ SELECT DISTINCT
449
+ json_extract(value, '$.sourceTypeId') AS source_type_id,
450
+ json_extract(value, '$.sourceId') AS source_primary_id,
451
+ json_extract(value, '$.linkId') AS link_id
452
+ FROM json_each(?)
453
+ )
454
+ SELECT rows.* FROM requested
455
+ CROSS JOIN ontology_source_rows AS rows
456
+ ON rows.project_id = ?
457
+ AND rows.entity_kind = 'link'
458
+ AND rows.source_type_id = requested.source_type_id
459
+ AND rows.source_primary_id = requested.source_primary_id
460
+ AND rows.link_id = requested.link_id
461
+ JOIN ontology_sources AS sources
462
+ ON sources.project_id = rows.project_id
463
+ AND sources.source_id = rows.source_id
464
+ AND sources.materialization_id = rows.materialization_id
465
+ WHERE sources.status = 'active'`
466
+ )
467
+ .all(requestedJson, this.projectId) as SqliteOntologySourceAssertionRow[]
468
+ const slotOverrideRows = this.db
469
+ .query(
470
+ `WITH requested AS (
471
+ SELECT DISTINCT
472
+ json_extract(value, '$.sourceTypeId') AS source_type_id,
473
+ json_extract(value, '$.sourceId') AS source_primary_id,
474
+ json_extract(value, '$.linkId') AS link_id
475
+ FROM json_each(?)
476
+ )
477
+ SELECT overrides.* FROM requested
478
+ CROSS JOIN ontology_link_overrides AS overrides
479
+ ON overrides.project_id = ?
480
+ AND overrides.identity_kind = 'slot'
481
+ AND overrides.source_type_id = requested.source_type_id
482
+ AND overrides.source_primary_id = requested.source_primary_id
483
+ AND overrides.link_id = requested.link_id`
484
+ )
485
+ .all(requestedJson, this.projectId) as LinkSlotOverrideRow[]
486
+ const sources = activeScopeSourceMap(sourceRows)
487
+ const overrides = new Map(
488
+ slotOverrideRows.map((row) => {
489
+ const stored = storedLinkSlotOverride(row)
490
+ return [linkScopeSortKey(stored.ref.source, stored.ref.linkId), stored] as const
491
+ })
492
+ )
493
+ const effective = new Map<string, EffectiveLinkSnapshot | null>()
373
494
  const rows = this.db
374
495
  .query(
375
496
  `
@@ -382,13 +503,16 @@ export class SqliteMaterializationStateReader {
382
503
  FROM json_each(?)
383
504
  )
384
505
  SELECT requested.scope_sort_key, links.*
385
- FROM links
386
- JOIN requested USING (source_type_id, source_id, link_id)
387
- WHERE links.project_id = ?
506
+ FROM requested
507
+ CROSS JOIN links
508
+ ON links.project_id = ?
509
+ AND links.source_type_id = requested.source_type_id
510
+ AND links.source_id = requested.source_id
511
+ AND links.link_id = requested.link_id
388
512
  ORDER BY requested.scope_sort_key, ${linkSortExpression("links")}
389
513
  `
390
514
  )
391
- .iterate(canonicalJson(requested), this.projectId) as Iterable<LinkScopeRow>
515
+ .iterate(requestedJson, this.projectId) as Iterable<LinkScopeRow>
392
516
  for (const row of rows) {
393
517
  const accumulator = accumulators.get(row.scope_sort_key)
394
518
  if (!accumulator) {
@@ -397,11 +521,19 @@ export class SqliteMaterializationStateReader {
397
521
  `Unexpected link scope '${row.scope_sort_key}' returned by storage.`
398
522
  )
399
523
  }
400
- appendScopeSnapshot(accumulator, effectiveLinkSnapshot(row))
524
+ const snapshot = effectiveLinkSnapshot(row)
525
+ appendScopeSnapshot(accumulator, snapshot)
526
+ effective.set(row.scope_sort_key, accumulator.effectiveCount === 1 ? snapshot : null)
401
527
  }
402
- return scopes.map(({ source, linkId }) =>
403
- finishScopeAccumulator(accumulators.get(linkScopeSortKey(source, linkId))!)
404
- )
528
+ return scopes.map(({ source, linkId }) => {
529
+ const key = linkScopeSortKey(source, linkId)
530
+ return {
531
+ ...finishScopeAccumulator(accumulators.get(key)!),
532
+ sourceAssertion: sources.get(key) ?? null,
533
+ override: overrides.get(key) ?? null,
534
+ effective: effective.get(key) ?? null,
535
+ }
536
+ })
405
537
  }
406
538
 
407
539
  linkScope(source: OntologyObjectRef, linkId: string): MaterializationLinkScopeState {
@@ -433,8 +565,8 @@ export class SqliteMaterializationStateReader {
433
565
  FROM links WHERE project_id = ?
434
566
  UNION
435
567
  SELECT source_type_id, source_primary_id, link_id, target_type_id, target_primary_id
436
- FROM ontology_overrides
437
- WHERE project_id = ? AND entity_kind = 'link'
568
+ FROM ontology_link_overrides
569
+ WHERE project_id = ?
438
570
  UNION
439
571
  SELECT rows.source_type_id, rows.source_primary_id, rows.link_id,
440
572
  rows.target_type_id, rows.target_primary_id
@@ -478,29 +610,15 @@ export class SqliteMaterializationStateReader {
478
610
  }
479
611
 
480
612
  *replacementIdentities(input: ReplacementIdentityInput): Iterable<ReplacementIdentity[]> {
481
- let cursor: string | null = null
613
+ this.prepareReplacementIdentities(input)
614
+ let cursor: ReplacementCursor | null = null
482
615
  while (true) {
483
- if (input.kind === "object") {
484
- const rows = this.replacementObjectRows(input, cursor)
485
- if (rows.length === 0) break
486
- yield rows.map((row) => ({
487
- kind: "object",
488
- ref: { objectTypeId: row.object_type_id, primaryId: row.primary_id },
489
- sortKey: row.sort_key,
490
- diffRequired: true,
491
- }))
492
- cursor = rows[rows.length - 1]?.sort_key ?? null
493
- continue
494
- }
495
- const rows = this.replacementLinkRows(input, cursor)
616
+ const rows = this.replacementRows(input, cursor)
496
617
  if (rows.length === 0) break
497
- yield rows.map((row) => ({
498
- kind: "link",
499
- ref: linkRefFromColumns(row),
500
- sortKey: row.sort_key,
501
- diffRequired: row.diff_required === 1,
502
- }))
503
- cursor = rows[rows.length - 1]?.sort_key ?? null
618
+ yield rows.map(replacementIdentity)
619
+ if (rows.length < input.pageRows) break
620
+ const last = rows[rows.length - 1]!
621
+ cursor = { sortKey: last.sort_key, identityKey: last.identity_key }
504
622
  }
505
623
  }
506
624
 
@@ -550,6 +668,7 @@ export class SqliteMaterializationStateReader {
550
668
  ? sourceLink(candidate?.get(key))
551
669
  : state.source,
552
670
  override: state.override,
671
+ slotOverride: state.slotOverride,
553
672
  effective: state.effective,
554
673
  diffRequired: identities[index]!.diffRequired,
555
674
  }
@@ -592,16 +711,6 @@ export class SqliteMaterializationStateReader {
592
711
  return row ? row.last_commit_id : undefined
593
712
  }
594
713
 
595
- overrideLastCommit(kind: "object" | "link", entityKey: string): string | null {
596
- const row = this.db
597
- .query(
598
- `SELECT last_commit_id FROM ontology_overrides
599
- WHERE project_id = ? AND entity_kind = ? AND entity_key = ?`
600
- )
601
- .get(this.projectId, kind, entityKey) as { readonly last_commit_id: string } | null
602
- return row?.last_commit_id ?? null
603
- }
604
-
605
714
  private replacementSources(
606
715
  sourceId: string,
607
716
  materializationIds: readonly string[],
@@ -610,22 +719,30 @@ export class SqliteMaterializationStateReader {
610
719
  if (materializationIds.length === 0 || refs.length === 0) {
611
720
  return { owned: new Set(), byMaterialization: new Map() }
612
721
  }
613
- const requestedKeys = canonicalJson(refs.map((ref) => projectionEntityKey(ref)))
722
+ const requestedEntities = canonicalJson(
723
+ refs.map((ref) => ({ entityKind: ref.kind, entityKey: projectionEntityKey(ref) }))
724
+ )
614
725
  const requestedMaterializations = canonicalJson([...new Set(materializationIds)])
615
726
  const rows = this.db
616
727
  .query(
617
728
  `WITH requested_entities AS (
618
- SELECT value AS entity_key FROM json_each(?)
729
+ SELECT DISTINCT json_extract(value, '$.entityKind') AS entity_kind,
730
+ json_extract(value, '$.entityKey') AS entity_key
731
+ FROM json_each(?)
619
732
  ), requested_materializations AS (
620
733
  SELECT value AS materialization_id FROM json_each(?)
621
734
  )
622
- SELECT rows.* FROM ontology_source_rows AS rows
623
- JOIN requested_entities USING (entity_key)
624
- JOIN requested_materializations USING (materialization_id)
625
- WHERE rows.project_id = ? AND rows.source_id = ?`
735
+ SELECT rows.* FROM requested_materializations
736
+ CROSS JOIN requested_entities
737
+ CROSS JOIN ontology_source_rows AS rows
738
+ ON rows.project_id = ?
739
+ AND rows.source_id = ?
740
+ AND rows.materialization_id = requested_materializations.materialization_id
741
+ AND rows.entity_kind = requested_entities.entity_kind
742
+ AND rows.entity_key = requested_entities.entity_key`
626
743
  )
627
744
  .all(
628
- requestedKeys,
745
+ requestedEntities,
629
746
  requestedMaterializations,
630
747
  this.projectId,
631
748
  sourceId
@@ -643,62 +760,48 @@ export class SqliteMaterializationStateReader {
643
760
  return { owned, byMaterialization }
644
761
  }
645
762
 
646
- private replacementObjectRows(
647
- input: ReplacementIdentityInput,
648
- cursor: string | null
649
- ): (EffectiveObjectRow & { readonly sort_key: string })[] {
650
- return this.db
763
+ private prepareReplacementIdentities(input: ReplacementIdentityInput): void {
764
+ if (input.kind === "object") {
765
+ this.prepareReplacementObjects(input)
766
+ return
767
+ }
768
+ this.prepareReplacementLinks(input)
769
+ }
770
+
771
+ private prepareReplacementObjects(input: ReplacementIdentityInput): void {
772
+ this.db
651
773
  .query(
652
774
  `
653
- SELECT DISTINCT object_type_id, primary_id, entity_sort_key AS sort_key
775
+ INSERT INTO ${SQLITE_REPLACEMENT_WORK_TABLE} (
776
+ session_id, entity_kind, identity_key, sort_key, diff_required
777
+ )
778
+ SELECT ?, 'object', json_array(object_type_id, primary_id),
779
+ MIN(entity_sort_key), 1
654
780
  FROM ontology_source_rows
655
781
  WHERE project_id = ? AND source_id = ? AND entity_kind = 'object'
656
782
  AND materialization_id IN (?, COALESCE(?, ''))
657
- AND (? IS NULL OR entity_sort_key > ?)
658
- ORDER BY sort_key
659
- LIMIT ?
783
+ GROUP BY object_type_id, primary_id
660
784
  `
661
785
  )
662
- .all(
786
+ .run(
787
+ input.sessionId,
663
788
  this.projectId,
664
789
  input.sourceId,
665
790
  input.candidateMaterializationId,
666
- input.previousMaterializationId,
667
- cursor,
668
- cursor,
669
- input.pageRows
670
- ) as (EffectiveObjectRow & { readonly sort_key: string })[]
791
+ input.previousMaterializationId
792
+ )
671
793
  }
672
794
 
673
- private replacementLinkRows(
674
- input: ReplacementIdentityInput,
675
- cursor: string | null
676
- ): (EffectiveLinkRow & { readonly sort_key: string; readonly diff_required: number })[] {
677
- return this.db
795
+ private prepareReplacementLinks(input: ReplacementIdentityInput): void {
796
+ this.db
678
797
  .query(
679
798
  `
680
799
  WITH incident_objects AS (
681
- SELECT
800
+ SELECT DISTINCT
682
801
  json_extract(payload, '$.ref.objectTypeId') AS object_type_id,
683
802
  json_extract(payload, '$.ref.primaryId') AS primary_id
684
803
  FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
685
804
  WHERE session_id = ? AND kind = 'incident-object'
686
- ), authority_links AS (
687
- SELECT source_type_id, source_id, link_id, target_type_id, target_id
688
- FROM links WHERE project_id = ?
689
- UNION
690
- SELECT source_type_id, source_primary_id, link_id, target_type_id, target_primary_id
691
- FROM ontology_overrides
692
- WHERE project_id = ? AND entity_kind = 'link'
693
- UNION
694
- SELECT rows.source_type_id, rows.source_primary_id, rows.link_id,
695
- rows.target_type_id, rows.target_primary_id
696
- FROM ontology_source_rows AS rows
697
- JOIN ontology_sources AS sources
698
- ON sources.project_id = rows.project_id
699
- AND sources.source_id = rows.source_id
700
- AND sources.materialization_id = rows.materialization_id
701
- WHERE rows.project_id = ? AND rows.entity_kind = 'link' AND sources.status = 'active'
702
805
  ), replacement_links AS (
703
806
  SELECT source_type_id, source_primary_id AS source_id, link_id,
704
807
  target_type_id, target_primary_id AS target_id
@@ -706,14 +809,67 @@ export class SqliteMaterializationStateReader {
706
809
  WHERE project_id = ? AND source_id = ? AND entity_kind = 'link'
707
810
  AND materialization_id IN (?, COALESCE(?, ''))
708
811
  ), incident_links AS (
709
- SELECT links.* FROM authority_links AS links
710
- WHERE EXISTS (
711
- SELECT 1 FROM incident_objects
712
- WHERE (incident_objects.object_type_id = links.source_type_id
713
- AND incident_objects.primary_id = links.source_id)
714
- OR (incident_objects.object_type_id = links.target_type_id
715
- AND incident_objects.primary_id = links.target_id)
716
- )
812
+ SELECT links.source_type_id, links.source_id, links.link_id,
813
+ links.target_type_id, links.target_id
814
+ FROM links
815
+ JOIN incident_objects
816
+ ON incident_objects.object_type_id = links.source_type_id
817
+ AND incident_objects.primary_id = links.source_id
818
+ WHERE links.project_id = ?
819
+ UNION
820
+ SELECT links.source_type_id, links.source_id, links.link_id,
821
+ links.target_type_id, links.target_id
822
+ FROM links
823
+ JOIN incident_objects
824
+ ON incident_objects.object_type_id = links.target_type_id
825
+ AND incident_objects.primary_id = links.target_id
826
+ WHERE links.project_id = ?
827
+ UNION
828
+ SELECT overrides.source_type_id,
829
+ overrides.source_primary_id AS source_id,
830
+ overrides.link_id, overrides.target_type_id,
831
+ overrides.target_primary_id AS target_id
832
+ FROM ontology_link_overrides AS overrides
833
+ JOIN incident_objects
834
+ ON incident_objects.object_type_id = overrides.source_type_id
835
+ AND incident_objects.primary_id = overrides.source_primary_id
836
+ WHERE overrides.project_id = ?
837
+ UNION
838
+ SELECT overrides.source_type_id,
839
+ overrides.source_primary_id AS source_id,
840
+ overrides.link_id, overrides.target_type_id,
841
+ overrides.target_primary_id AS target_id
842
+ FROM ontology_link_overrides AS overrides
843
+ JOIN incident_objects
844
+ ON incident_objects.object_type_id = overrides.target_type_id
845
+ AND incident_objects.primary_id = overrides.target_primary_id
846
+ WHERE overrides.project_id = ?
847
+ UNION
848
+ SELECT rows.source_type_id, rows.source_primary_id AS source_id,
849
+ rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
850
+ FROM ontology_source_rows AS rows
851
+ JOIN ontology_sources AS sources
852
+ ON sources.project_id = rows.project_id
853
+ AND sources.source_id = rows.source_id
854
+ AND sources.materialization_id = rows.materialization_id
855
+ JOIN incident_objects
856
+ ON incident_objects.object_type_id = rows.source_type_id
857
+ AND incident_objects.primary_id = rows.source_primary_id
858
+ WHERE rows.project_id = ? AND rows.entity_kind = 'link'
859
+ AND sources.status = 'active'
860
+ UNION
861
+ SELECT rows.source_type_id, rows.source_primary_id AS source_id,
862
+ rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
863
+ FROM ontology_source_rows AS rows
864
+ JOIN ontology_sources AS sources
865
+ ON sources.project_id = rows.project_id
866
+ AND sources.source_id = rows.source_id
867
+ AND sources.materialization_id = rows.materialization_id
868
+ JOIN incident_objects
869
+ ON incident_objects.object_type_id = rows.target_type_id
870
+ AND incident_objects.primary_id = rows.target_primary_id
871
+ WHERE rows.project_id = ? AND rows.entity_kind = 'link'
872
+ AND sources.status = 'active'
717
873
  ), diff_links AS (
718
874
  SELECT * FROM replacement_links
719
875
  UNION SELECT * FROM incident_links
@@ -727,6 +883,16 @@ export class SqliteMaterializationStateReader {
727
883
  FROM links
728
884
  JOIN affected_scopes USING (source_type_id, source_id, link_id)
729
885
  WHERE links.project_id = ?
886
+ UNION ALL
887
+ SELECT overrides.source_type_id, overrides.source_primary_id AS source_id,
888
+ overrides.link_id, overrides.target_type_id,
889
+ overrides.target_primary_id AS target_id, 0 AS diff_required
890
+ FROM ontology_link_overrides AS overrides
891
+ JOIN affected_scopes
892
+ ON affected_scopes.source_type_id = overrides.source_type_id
893
+ AND affected_scopes.source_id = overrides.source_primary_id
894
+ AND affected_scopes.link_id = overrides.link_id
895
+ WHERE overrides.project_id = ? AND overrides.identity_kind = 'slot'
730
896
  ), selected AS (
731
897
  SELECT source_type_id, source_id, link_id, target_type_id, target_id,
732
898
  ${linkSortExpression("all_links")} AS sort_key,
@@ -734,26 +900,78 @@ export class SqliteMaterializationStateReader {
734
900
  FROM all_links
735
901
  GROUP BY source_type_id, source_id, link_id, target_type_id, target_id
736
902
  )
737
- SELECT * FROM selected
738
- WHERE (? IS NULL OR sort_key > ?)
739
- ORDER BY sort_key
740
- LIMIT ?
903
+ INSERT INTO ${SQLITE_REPLACEMENT_WORK_TABLE} (
904
+ session_id, entity_kind, identity_key, sort_key, diff_required
905
+ )
906
+ SELECT ?, 'link',
907
+ json_array(source_type_id, source_id, link_id, target_type_id, target_id),
908
+ sort_key, diff_required
909
+ FROM selected
741
910
  `
742
911
  )
743
- .all(
912
+ .run(
744
913
  input.sessionId,
745
914
  this.projectId,
746
- this.projectId,
747
- this.projectId,
748
- this.projectId,
749
915
  input.sourceId,
750
916
  input.candidateMaterializationId,
751
917
  input.previousMaterializationId,
752
918
  this.projectId,
753
- cursor,
754
- cursor,
919
+ this.projectId,
920
+ this.projectId,
921
+ this.projectId,
922
+ this.projectId,
923
+ this.projectId,
924
+ this.projectId,
925
+ this.projectId,
926
+ input.sessionId
927
+ )
928
+ }
929
+
930
+ private replacementRows(
931
+ input: ReplacementIdentityInput,
932
+ cursor: ReplacementCursor | null
933
+ ): ReplacementWorkRow[] {
934
+ return this.db
935
+ .query(
936
+ `
937
+ SELECT entity_kind, identity_key, sort_key, diff_required
938
+ FROM ${SQLITE_REPLACEMENT_WORK_TABLE}
939
+ WHERE session_id = ? AND entity_kind = ?
940
+ AND (? IS NULL OR (sort_key, identity_key) > (?, ?))
941
+ ORDER BY sort_key, identity_key
942
+ LIMIT ?
943
+ `
944
+ )
945
+ .all(
946
+ input.sessionId,
947
+ input.kind,
948
+ cursor?.sortKey ?? null,
949
+ cursor?.sortKey ?? null,
950
+ cursor?.identityKey ?? null,
755
951
  input.pageRows
756
- ) as (EffectiveLinkRow & { readonly sort_key: string; readonly diff_required: number })[]
952
+ ) as ReplacementWorkRow[]
953
+ }
954
+ }
955
+
956
+ function replacementIdentity(row: ReplacementWorkRow): ReplacementIdentity {
957
+ const parts = parseJson<string[]>(row.identity_key)
958
+ if (row.entity_kind === "object") {
959
+ return {
960
+ kind: "object",
961
+ ref: { objectTypeId: parts[0]!, primaryId: parts[1]! },
962
+ sortKey: row.sort_key,
963
+ diffRequired: true,
964
+ }
965
+ }
966
+ return {
967
+ kind: "link",
968
+ ref: {
969
+ source: { objectTypeId: parts[0]!, primaryId: parts[1]! },
970
+ linkId: parts[2]!,
971
+ target: { objectTypeId: parts[3]!, primaryId: parts[4]! },
972
+ },
973
+ sortKey: row.sort_key,
974
+ diffRequired: row.diff_required === 1,
757
975
  }
758
976
  }
759
977
 
@@ -775,6 +993,26 @@ function activeSourceMap(
775
993
  return result
776
994
  }
777
995
 
996
+ function activeScopeSourceMap(
997
+ rows: readonly SqliteOntologySourceAssertionRow[]
998
+ ): ReadonlyMap<string, StoredSourceLinkAssertion> {
999
+ const result = new Map<string, StoredSourceLinkAssertion>()
1000
+ for (const row of rows) {
1001
+ const source = sourceLink(storedSource(row))
1002
+ if (!source) continue
1003
+ const ref = source.assertion.ref
1004
+ const key = linkScopeSortKey(ref.source, ref.linkId)
1005
+ if (result.has(key)) {
1006
+ throw new MaterializationConflictError(
1007
+ "source-materialization",
1008
+ `Multiple active sources assert cardinality-one link slot ${key}.`
1009
+ )
1010
+ }
1011
+ result.set(key, source)
1012
+ }
1013
+ return result
1014
+ }
1015
+
778
1016
  function sourceObject(
779
1017
  source: StoredSourceAssertion | undefined
780
1018
  ): StoredSourceObjectAssertion | null {
@@ -805,6 +1043,19 @@ function storedLinkOverride(row: LinkOverrideRow): StoredLinkOverride {
805
1043
  }
806
1044
  }
807
1045
 
1046
+ function storedLinkSlotOverride(row: LinkSlotOverrideRow): StoredLinkSlotOverride {
1047
+ const ref = {
1048
+ source: { objectTypeId: row.source_type_id, primaryId: row.source_primary_id },
1049
+ linkId: row.link_id,
1050
+ }
1051
+ return {
1052
+ ref,
1053
+ value: linkSlotOverrideValue(parseJson<unknown>(row.value), linkScopeLabel(ref)),
1054
+ lastCommitId: row.last_commit_id,
1055
+ updatedAt: row.updated_at,
1056
+ }
1057
+ }
1058
+
808
1059
  function linkRefFromOverrideColumns(row: LinkOverrideRow): OntologyLinkRef {
809
1060
  return {
810
1061
  source: { objectTypeId: row.source_type_id, primaryId: row.source_primary_id },
@@ -813,6 +1064,13 @@ function linkRefFromOverrideColumns(row: LinkOverrideRow): OntologyLinkRef {
813
1064
  }
814
1065
  }
815
1066
 
1067
+ function linkScopeLabel(scope: {
1068
+ readonly source: OntologyObjectRef
1069
+ readonly linkId: string
1070
+ }): string {
1071
+ return JSON.stringify([scope.source.objectTypeId, scope.source.primaryId, scope.linkId])
1072
+ }
1073
+
816
1074
  function storedSource(row: SqliteOntologySourceAssertionRow): StoredSourceAssertion {
817
1075
  return {
818
1076
  source: { projectionId: row.source_id },
@@ -885,17 +1143,7 @@ function storedPoint(row: TelemetryRow): StoredTelemetryPoint {
885
1143
  }
886
1144
  }
887
1145
 
888
- export function objectSortExpression(alias?: string): string {
889
- const prefix = alias ? `${alias}.` : ""
890
- return `LOWER(HEX(CAST(json_array(${prefix}object_type_id, ${prefix}primary_id) AS BLOB)))`
891
- }
892
-
893
1146
  export function linkSortExpression(alias?: string): string {
894
1147
  const prefix = alias ? `${alias}.` : ""
895
1148
  return `LOWER(HEX(CAST(json_array(${prefix}source_type_id, ${prefix}source_id, ${prefix}link_id, ${prefix}target_type_id, ${prefix}target_id) AS BLOB)))`
896
1149
  }
897
-
898
- export function pointSortExpression(alias?: string): string {
899
- const prefix = alias ? `${alias}.` : ""
900
- return `LOWER(HEX(CAST(json_array(${prefix}object_type_id, ${prefix}object_id, ${prefix}property_id, ${prefix}at) AS BLOB)))`
901
- }