@sixb/sqlite 0.1.8 → 0.1.10

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 (49) hide show
  1. package/dist/action-run-storage.d.ts.map +1 -1
  2. package/dist/agents/rows.d.ts +0 -1
  3. package/dist/agents/rows.d.ts.map +1 -1
  4. package/dist/agents/runs.d.ts.map +1 -1
  5. package/dist/ai-cost-storage.d.ts.map +1 -1
  6. package/dist/ai-usage-storage.d.ts.map +1 -1
  7. package/dist/execution-storage.d.ts.map +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +495 -244
  10. package/dist/index.js.map +23 -21
  11. package/dist/migrations.d.ts.map +1 -1
  12. package/dist/ontology-storage/materialization-state.d.ts +2 -1
  13. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  14. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  15. package/dist/ontology-storage/shared.d.ts +2 -0
  16. package/dist/ontology-storage/shared.d.ts.map +1 -1
  17. package/dist/ontology-storage/source-cleanup.d.ts +4 -0
  18. package/dist/ontology-storage/source-cleanup.d.ts.map +1 -0
  19. package/dist/ontology-storage/source-roots.d.ts +27 -0
  20. package/dist/ontology-storage/source-roots.d.ts.map +1 -0
  21. package/dist/ontology-storage/sources.d.ts.map +1 -1
  22. package/dist/pipeline-run-storage.d.ts.map +1 -1
  23. package/dist/projection-run-storage.d.ts.map +1 -1
  24. package/dist/sync-run-storage.d.ts.map +1 -1
  25. package/dist/workflow-run-storage.d.ts.map +1 -1
  26. package/package.json +4 -4
  27. package/src/action-run-storage.ts +6 -7
  28. package/src/agents/rows.ts +0 -2
  29. package/src/agents/runs.ts +3 -7
  30. package/src/ai-cost-groups.ts +3 -0
  31. package/src/ai-cost-storage.ts +11 -2
  32. package/src/ai-usage-storage.ts +1 -2
  33. package/src/connector-connection-storage.ts +3 -3
  34. package/src/execution-storage.ts +7 -5
  35. package/src/index.ts +4 -2
  36. package/src/migrations/037-execution-requester-groups.sql +24 -0
  37. package/src/migrations/038-projection-source-roots.sql +44 -0
  38. package/src/migrations/039-connector-optional-pkce.sql +52 -0
  39. package/src/migrations.ts +14 -0
  40. package/src/ontology-storage/materialization-state.ts +56 -91
  41. package/src/ontology-storage/materializations.ts +7 -1
  42. package/src/ontology-storage/shared.ts +10 -0
  43. package/src/ontology-storage/source-cleanup.ts +59 -0
  44. package/src/ontology-storage/source-roots.ts +230 -0
  45. package/src/ontology-storage/sources.ts +25 -78
  46. package/src/pipeline-run-storage.ts +2 -5
  47. package/src/projection-run-storage.ts +11 -3
  48. package/src/sync-run-storage.ts +2 -5
  49. package/src/workflow-run-storage.ts +1 -6
@@ -0,0 +1,230 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import type { ProjectionEntityRef } from "@sixb/core/internal/materialization"
3
+ import {
4
+ MaterializationConflictError,
5
+ MaterializationValidationError,
6
+ projectionEntityKey,
7
+ } from "@sixb/core/internal/materialization"
8
+ import { sourceStageRoots, utf8SortKey } from "@sixb/core/internal/ontology-storage-provider"
9
+ import type { SourceActivationWrite, StageSourceRowsInput } from "@sixb/core/storage"
10
+ import type { SqliteOntologySourceAssertionRow } from "./shared"
11
+ import { canonicalJson, type SqliteOntologySourceRow } from "./shared"
12
+
13
+ export function replacementSourceRows(input: {
14
+ readonly projectId: string
15
+ readonly sourceId: string
16
+ readonly materializationId: string
17
+ readonly incremental: boolean
18
+ }): { readonly sql: string; readonly values: readonly string[] } {
19
+ const roots = input.incremental
20
+ ? `ontology_source_roots AS changed
21
+ CROSS JOIN ontology_source_roots AS roots ON roots.project_id = changed.project_id
22
+ AND roots.source_id = changed.source_id AND roots.root_sort_key = changed.root_sort_key AND roots.active = 1`
23
+ : "ontology_source_roots AS roots"
24
+ const selected = input.incremental ? "changed" : "roots"
25
+ return {
26
+ sql: `SELECT * FROM ontology_source_rows WHERE project_id = ? AND source_id = ? AND materialization_id = ?
27
+ UNION ALL
28
+ SELECT rows.* FROM ${roots}
29
+ CROSS JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id
30
+ AND rows.source_id = roots.source_id AND rows.materialization_id = roots.materialization_id
31
+ AND rows.root_sort_key = roots.root_sort_key
32
+ WHERE ${selected}.project_id = ? AND ${selected}.source_id = ?
33
+ AND ${input.incremental ? "changed.materialization_id = ?" : "roots.active = 1"}`,
34
+ values: [
35
+ input.projectId,
36
+ input.sourceId,
37
+ input.materializationId,
38
+ input.projectId,
39
+ input.sourceId,
40
+ ...(input.incremental ? [input.materializationId] : []),
41
+ ],
42
+ }
43
+ }
44
+
45
+ export function stageSourceRoots(
46
+ db: Database,
47
+ manifest: SqliteOntologySourceRow,
48
+ input: StageSourceRowsInput
49
+ ): void {
50
+ const roots = sourceStageRoots(manifest.projection_kind, input)
51
+ if (!manifest.base_materialization_id && roots.some((root) => root.deleted)) {
52
+ throw new MaterializationValidationError("Root deletions require a source delta base.")
53
+ }
54
+ const existing = db.query(`SELECT staging_ordinal, deleted FROM ontology_source_roots
55
+ WHERE project_id = ? AND source_id = ? AND materialization_id = ? AND root_sort_key = ?`)
56
+ const insert = db.query(`INSERT INTO ontology_source_roots (
57
+ project_id, source_id, materialization_id, root_sort_key, root_kind, root_key, root, staging_ordinal, deleted
58
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING RETURNING root_sort_key`)
59
+ for (const root of roots) {
60
+ const inserted = insert.get(
61
+ input.projectId,
62
+ input.source.projectionId,
63
+ input.materializationId,
64
+ root.sortKey,
65
+ root.root.kind,
66
+ root.rootKey,
67
+ canonicalJson(root.root),
68
+ root.stagingOrdinal,
69
+ Number(root.deleted)
70
+ )
71
+ if (inserted) continue
72
+ const previous = existing.get(
73
+ input.projectId,
74
+ input.source.projectionId,
75
+ input.materializationId,
76
+ root.sortKey
77
+ ) as { staging_ordinal: number; deleted: number } | null
78
+ if (
79
+ !previous ||
80
+ previous.staging_ordinal !== root.stagingOrdinal ||
81
+ Boolean(previous.deleted) !== root.deleted
82
+ ) {
83
+ throw new MaterializationValidationError(
84
+ "Source materialization repeats root or stream ordinal with different content."
85
+ )
86
+ }
87
+ }
88
+ }
89
+
90
+ export function activateSourceRoots(
91
+ db: Database,
92
+ projectId: string,
93
+ candidate: SqliteOntologySourceRow,
94
+ activation: SourceActivationWrite
95
+ ): void {
96
+ if (
97
+ candidate.base_materialization_id !== null &&
98
+ (candidate.base_materialization_id !== activation.expected.activeMaterializationId ||
99
+ candidate.base_commit_id !== activation.expected.lastCommitId)
100
+ )
101
+ throw new MaterializationConflictError(
102
+ "projection-fence",
103
+ "Source delta base changed before activation."
104
+ )
105
+
106
+ if (candidate.base_materialization_id !== null && candidate.root_count === 0) return
107
+ const scope = [projectId, activation.source.projectionId, activation.materializationId] as const
108
+ db.query(`UPDATE ontology_source_roots AS roots SET active = 0, retired_at = ?
109
+ WHERE project_id = ? AND source_id = ? AND active = 1
110
+ ${
111
+ candidate.base_materialization_id === null
112
+ ? ""
113
+ : `AND root_sort_key IN (
114
+ SELECT root_sort_key FROM ontology_source_roots
115
+ WHERE project_id = ? AND source_id = ? AND materialization_id = ?)`
116
+ }`).run(
117
+ activation.updatedAt,
118
+ projectId,
119
+ activation.source.projectionId,
120
+ ...(candidate.base_materialization_id === null ? [] : [...scope])
121
+ )
122
+ db.query(`UPDATE ontology_source_roots SET active = CASE WHEN deleted = 1 THEN 0 ELSE 1 END,
123
+ retired_at = CASE WHEN deleted = 1 THEN ? ELSE NULL END
124
+ WHERE project_id = ? AND source_id = ? AND materialization_id = ?`).run(
125
+ activation.updatedAt,
126
+ ...scope
127
+ )
128
+ }
129
+
130
+ export function assertSourceRootCoverage(db: Database, manifest: SqliteOntologySourceRow): void {
131
+ const invalid = db
132
+ .query(`SELECT 1 FROM ontology_source_roots AS roots
133
+ WHERE project_id = ? AND source_id = ? AND materialization_id = ?
134
+ AND roots.deleted = EXISTS (
135
+ SELECT 1 FROM ontology_source_rows AS rows
136
+ WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
137
+ AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
138
+ ) LIMIT 1`)
139
+ .get(manifest.project_id, manifest.source_id, manifest.materialization_id)
140
+ if (invalid)
141
+ throw new MaterializationValidationError(
142
+ "Source roots must have complete assertions or an explicit deletion, never both."
143
+ )
144
+ }
145
+
146
+ export function activeSourceRows(
147
+ db: Database,
148
+ projectId: string,
149
+ refs: readonly ProjectionEntityRef[]
150
+ ): SqliteOntologySourceAssertionRow[] {
151
+ const keys = refs.flatMap((entity) => {
152
+ const entitySortKey = utf8SortKey(projectionEntityKey(entity))
153
+ const roots =
154
+ entity.kind === "object"
155
+ ? [entitySortKey]
156
+ : [
157
+ entitySortKey,
158
+ utf8SortKey(projectionEntityKey({ kind: "object", ref: entity.ref.source })),
159
+ ]
160
+ return roots.map((rootSortKey) => ({ rootSortKey, entitySortKey }))
161
+ })
162
+ return db
163
+ .query(`WITH requested AS (
164
+ SELECT DISTINCT json_extract(value, '$.rootSortKey') AS root_sort_key,
165
+ json_extract(value, '$.entitySortKey') AS entity_sort_key FROM json_each(?)
166
+ ) SELECT rows.* FROM requested
167
+ CROSS JOIN ontology_source_roots AS roots ON roots.project_id = ?
168
+ AND roots.active = 1 AND roots.root_sort_key = requested.root_sort_key
169
+ CROSS JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
170
+ AND rows.materialization_id = roots.materialization_id AND rows.entity_sort_key = requested.entity_sort_key
171
+ AND rows.root_sort_key = roots.root_sort_key`)
172
+ .all(canonicalJson(keys), projectId) as SqliteOntologySourceAssertionRow[]
173
+ }
174
+
175
+ export function replacementAssertionRows(
176
+ db: Database,
177
+ input: {
178
+ projectId: string
179
+ sourceId: string
180
+ materializationId: string
181
+ incremental: boolean
182
+ includePrevious: boolean
183
+ refs: readonly ProjectionEntityRef[]
184
+ }
185
+ ): SqliteOntologySourceAssertionRow[] {
186
+ const keys = input.refs.map((ref) => ({
187
+ entityKind: ref.kind,
188
+ entityKey: projectionEntityKey(ref),
189
+ rootKeys:
190
+ ref.kind === "object"
191
+ ? [utf8SortKey(projectionEntityKey(ref))]
192
+ : [
193
+ utf8SortKey(projectionEntityKey(ref)),
194
+ utf8SortKey(projectionEntityKey({ kind: "object", ref: ref.ref.source })),
195
+ ],
196
+ }))
197
+ const previous = !input.includePrevious
198
+ ? ""
199
+ : `UNION ALL
200
+ SELECT rows.* FROM requested_entities AS requested
201
+ CROSS JOIN ontology_source_roots AS roots ON roots.project_id = ? AND roots.source_id = ?
202
+ AND roots.active = 1 AND roots.root_sort_key IN (SELECT value FROM json_each(requested.root_keys))
203
+ CROSS JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
204
+ AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
205
+ AND rows.entity_kind = requested.entity_kind AND rows.entity_key = requested.entity_key
206
+ ${
207
+ !input.incremental
208
+ ? ""
209
+ : `WHERE EXISTS (SELECT 1 FROM ontology_source_roots AS changed
210
+ WHERE changed.project_id = roots.project_id AND changed.source_id = roots.source_id
211
+ AND changed.materialization_id = ? AND changed.root_sort_key = roots.root_sort_key)`
212
+ }`
213
+ return db
214
+ .query(`WITH requested_entities AS (
215
+ SELECT DISTINCT json_extract(value, '$.entityKind') AS entity_kind,
216
+ json_extract(value, '$.entityKey') AS entity_key, json_extract(value, '$.rootKeys') AS root_keys FROM json_each(?)
217
+ ) SELECT rows.* FROM requested_entities AS requested
218
+ CROSS JOIN ontology_source_rows AS rows ON rows.project_id = ? AND rows.source_id = ?
219
+ AND rows.materialization_id = ? AND rows.entity_kind = requested.entity_kind AND rows.entity_key = requested.entity_key
220
+ ${previous}`)
221
+ .all(
222
+ canonicalJson(keys),
223
+ input.projectId,
224
+ input.sourceId,
225
+ input.materializationId,
226
+ ...(input.includePrevious
227
+ ? [input.projectId, input.sourceId, ...(input.incremental ? [input.materializationId] : [])]
228
+ : [])
229
+ ) as SqliteOntologySourceAssertionRow[]
230
+ }
@@ -47,6 +47,8 @@ import {
47
47
  sourceAssertion,
48
48
  sourceRecord,
49
49
  } from "./shared"
50
+ import { cleanupSourceVersions } from "./source-cleanup"
51
+ import { assertSourceRootCoverage, stageSourceRoots } from "./source-roots"
50
52
 
51
53
  export class SqliteOntologySourceStorage implements OntologySourceStorage {
52
54
  constructor(
@@ -98,9 +100,9 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
98
100
  dataset_id, dataset_version_id, dataset_version_created_at,
99
101
  projection_revision, ownership_hash, ontology_revision,
100
102
  root_count, assertion_count, created_at, ready_at, activated_at,
101
- terminal_at, last_commit_id, updated_at
103
+ terminal_at, last_commit_id, updated_at, base_materialization_id, base_commit_id
102
104
  ) VALUES (?, ?, ?, ?, ?, 'replacement', 'staging', ?, ?, ?, ?, ?, ?, ?,
103
- NULL, NULL, ?, NULL, NULL, NULL, NULL, ?)
105
+ NULL, NULL, ?, NULL, NULL, NULL, NULL, ?, ?, ?)
104
106
  `
105
107
  )
106
108
  .run(
@@ -117,7 +119,9 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
117
119
  input.ownershipHash,
118
120
  input.ontologyRevision,
119
121
  input.createdAt,
120
- input.createdAt
122
+ input.createdAt,
123
+ input.base?.materializationId ?? null,
124
+ input.base?.lastCommitId ?? null
121
125
  )
122
126
  return sourceRecord(
123
127
  this.requireManifest(input.projectId, input.source.projectionId, input.materializationId)
@@ -144,6 +148,7 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
144
148
  const rows = sourceStageRows(manifest.projection_kind, input.rows)
145
149
  const existing = this.findStageRows(input, rows)
146
150
  const { pending, unchanged } = reconcileSourceStageRows(rows, existing)
151
+ stageSourceRoots(this.db, manifest, input)
147
152
  this.insertStageRows(input, pending)
148
153
  return { inserted: pending.length, unchanged }
149
154
  })
@@ -249,78 +254,7 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
249
254
  assertNonblank(input.projectId, "Terminal source cleanup project id")
250
255
  assertTimestamp(input.terminalBefore, "Terminal source cleanup cutoff", true)
251
256
  assertPositiveInteger(input.limit, "Terminal source cleanup limit")
252
- const manifests = this.db
253
- .query(
254
- `
255
- SELECT * FROM ontology_sources
256
- WHERE project_id = ? AND status IN ('superseded', 'abandoned')
257
- AND terminal_at < ?
258
- ORDER BY terminal_at, source_id, materialization_id
259
- LIMIT ?
260
- `
261
- )
262
- .all(input.projectId, input.terminalBefore, input.limit) as SqliteOntologySourceRow[]
263
- let remaining = input.limit
264
- let rowsDeleted = 0
265
- let materializationsDeleted = 0
266
- for (const manifest of manifests) {
267
- if (remaining === 0) break
268
- const deleted = this.db
269
- .query(
270
- `
271
- DELETE FROM ontology_source_rows
272
- WHERE rowid IN (
273
- SELECT rows.rowid
274
- FROM ontology_source_rows AS rows
275
- JOIN ontology_sources AS sources
276
- ON sources.project_id = rows.project_id
277
- AND sources.source_id = rows.source_id
278
- AND sources.materialization_id = rows.materialization_id
279
- WHERE rows.project_id = ? AND rows.source_id = ? AND rows.materialization_id = ?
280
- AND sources.status IN ('superseded', 'abandoned')
281
- AND sources.terminal_at = ? AND sources.terminal_at < ?
282
- ORDER BY rows.entity_sort_key
283
- LIMIT ?
284
- )
285
- `
286
- )
287
- .run(
288
- manifest.project_id,
289
- manifest.source_id,
290
- manifest.materialization_id,
291
- manifest.terminal_at,
292
- input.terminalBefore,
293
- remaining
294
- ).changes
295
- rowsDeleted += deleted
296
- remaining -= deleted
297
- if (remaining === 0) break
298
- const removed = this.db
299
- .query(
300
- `
301
- DELETE FROM ontology_sources
302
- WHERE project_id = ? AND source_id = ? AND materialization_id = ?
303
- AND status IN ('superseded', 'abandoned')
304
- AND terminal_at = ? AND terminal_at < ?
305
- AND NOT EXISTS (
306
- SELECT 1 FROM ontology_source_rows AS rows
307
- WHERE rows.project_id = ontology_sources.project_id
308
- AND rows.source_id = ontology_sources.source_id
309
- AND rows.materialization_id = ontology_sources.materialization_id
310
- )
311
- `
312
- )
313
- .run(
314
- manifest.project_id,
315
- manifest.source_id,
316
- manifest.materialization_id,
317
- manifest.terminal_at,
318
- input.terminalBefore
319
- ).changes
320
- materializationsDeleted += removed
321
- remaining -= removed
322
- }
323
- return { rowsDeleted, materializationsDeleted }
257
+ return cleanupSourceVersions(this.db, input)
324
258
  })
325
259
  }
326
260
 
@@ -433,6 +367,10 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
433
367
  `Source materialization '${manifest.materialization_id}' cannot transition to 'abandoned'.`
434
368
  )
435
369
  }
370
+ this.db
371
+ .query(`UPDATE ontology_source_roots SET retired_at = ?
372
+ WHERE project_id = ? AND source_id = ? AND materialization_id = ? AND active = 0`)
373
+ .run(abandonedAt, manifest.project_id, manifest.source_id, manifest.materialization_id)
436
374
  return sourceRecord(
437
375
  this.requireManifest(manifest.project_id, manifest.source_id, manifest.materialization_id)
438
376
  )
@@ -446,14 +384,22 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
446
384
  const counts = this.db
447
385
  .query(
448
386
  `
449
- SELECT COUNT(*) AS assertions, COUNT(DISTINCT root_key) AS roots,
387
+ SELECT (SELECT COUNT(*) FROM ontology_source_rows
388
+ WHERE project_id = ? AND source_id = ? AND materialization_id = ?) AS assertions, COUNT(*) AS roots,
450
389
  COUNT(DISTINCT staging_ordinal) AS ordinals,
451
390
  MIN(staging_ordinal) AS min_ordinal, MAX(staging_ordinal) AS max_ordinal
452
- FROM ontology_source_rows
391
+ FROM ontology_source_roots
453
392
  WHERE project_id = ? AND source_id = ? AND materialization_id = ?
454
393
  `
455
394
  )
456
- .get(manifest.project_id, manifest.source_id, manifest.materialization_id) as {
395
+ .get(
396
+ manifest.project_id,
397
+ manifest.source_id,
398
+ manifest.materialization_id,
399
+ manifest.project_id,
400
+ manifest.source_id,
401
+ manifest.materialization_id
402
+ ) as {
457
403
  assertions: number
458
404
  roots: number
459
405
  ordinals: number
@@ -470,6 +416,7 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
470
416
  "Source ready counts do not match the staged roots and assertions."
471
417
  )
472
418
  }
419
+ assertSourceRootCoverage(this.db, manifest)
473
420
  const invalid = this.db
474
421
  .query(
475
422
  manifest.projection_kind === "link"
@@ -1,6 +1,7 @@
1
1
  import type { Database } from "bun:sqlite"
2
2
  import { parseSixbFailure, serializeSixbFailure } from "@sixb/core/internal/errors"
3
3
  import { assertPipelineRunExecution } from "@sixb/core/internal/pipeline-run-storage-provider"
4
+ import { canRequeuePipelineRunAfterEnqueueFailure } from "@sixb/core/internal/storage"
4
5
  import type { DatasetVersionRef } from "@sixb/core/lake-storage"
5
6
  import type {
6
7
  ExecutionStorage,
@@ -19,11 +20,7 @@ import type {
19
20
  StartPipelineRunInput,
20
21
  StartPipelineStepRunInput,
21
22
  } from "@sixb/core/storage"
22
- import {
23
- canRequeuePipelineRunAfterEnqueueFailure,
24
- PIPELINE_RUN_FAILURE_CODES,
25
- PipelineRunError,
26
- } from "@sixb/core/storage"
23
+ import { PIPELINE_RUN_FAILURE_CODES, PipelineRunError } from "@sixb/core/storage"
27
24
  import { queryLatestRunsByOwnerId } from "./latest-run-query"
28
25
  import { installFreshSqliteSchema } from "./migrations"
29
26
  import {
@@ -114,7 +114,7 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
114
114
  input_exhausted = ?, missing_target_object_type_id = NULL,
115
115
  missing_target_object_id = NULL, missing_target_batch_ordinal = NULL,
116
116
  missing_target_first_seen_at = NULL, source_rows_read = 0,
117
- source_rows_skipped = 0, error = NULL
117
+ source_rows_skipped = 0, source_changes_read = NULL, error = NULL
118
118
  WHERE project_id = ? AND id = ? AND status = 'failed'
119
119
  AND json_extract(error, '$.code') = 'queue.enqueue_failed'
120
120
  `
@@ -257,6 +257,7 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
257
257
  execution_token = NULL,
258
258
  source_rows_read = ?,
259
259
  source_rows_skipped = ?,
260
+ source_changes_read = ?,
260
261
  input_exhausted = ?,
261
262
  error = ?
262
263
  WHERE project_id = ? AND id = ? AND status = 'running' AND execution_token = ?
@@ -267,6 +268,7 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
267
268
  plan.finishedAt.toISOString(),
268
269
  plan.progress.sourceRowsRead,
269
270
  plan.progress.sourceRowsSkipped,
271
+ plan.progress.sourceChangesRead ?? null,
270
272
  plan.inputExhausted ? 1 : existingRow.input_exhausted,
271
273
  plan.error === undefined
272
274
  ? null
@@ -531,9 +533,10 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
531
533
  progress: ProjectionRunProgress,
532
534
  executionToken: string
533
535
  ): number {
534
- const args: (string | number)[] = [
536
+ const args: (string | number | null)[] = [
535
537
  progress.sourceRowsRead,
536
538
  progress.sourceRowsSkipped,
539
+ progress.sourceChangesRead ?? null,
537
540
  existing.project_id,
538
541
  existing.id,
539
542
  executionToken,
@@ -544,7 +547,8 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
544
547
  UPDATE projection_runs
545
548
  SET
546
549
  source_rows_read = ?,
547
- source_rows_skipped = ?
550
+ source_rows_skipped = ?,
551
+ source_changes_read = ?
548
552
  WHERE project_id = ? AND id = ? AND status = 'running'
549
553
  AND execution_token = ?
550
554
  `
@@ -631,6 +635,9 @@ function restoreProjectionRunRow(row: DatabaseRow): StoredProjectionRunRecord {
631
635
  progress: {
632
636
  sourceRowsRead: databaseSafeInteger(row.source_rows_read, "sourceRowsRead"),
633
637
  sourceRowsSkipped: databaseSafeInteger(row.source_rows_skipped, "sourceRowsSkipped"),
638
+ ...(row.source_changes_read == null
639
+ ? {}
640
+ : { sourceChangesRead: databaseSafeInteger(row.source_changes_read, "sourceChangesRead") }),
634
641
  },
635
642
  error: row.error ?? undefined,
636
643
  }
@@ -694,6 +701,7 @@ interface DatabaseRow {
694
701
  missing_target_first_seen_at: string | null
695
702
  source_rows_read: number
696
703
  source_rows_skipped: number
704
+ source_changes_read: number | null
697
705
  error: string | null
698
706
  }
699
707
 
@@ -1,6 +1,7 @@
1
1
  import type { Database } from "bun:sqlite"
2
2
  import type { JsonValue } from "@sixb/core"
3
3
  import { parseSixbFailure, serializeSixbFailure } from "@sixb/core/internal/errors"
4
+ import { canRequeueSyncRunAfterEnqueueFailure } from "@sixb/core/internal/storage"
4
5
  import { assertSyncRunExecution } from "@sixb/core/internal/sync-run-storage-provider"
5
6
  import type {
6
7
  ExecutionStorage,
@@ -14,11 +15,7 @@ import type {
14
15
  SyncRunRecord,
15
16
  SyncRunStorage,
16
17
  } from "@sixb/core/storage"
17
- import {
18
- canRequeueSyncRunAfterEnqueueFailure,
19
- SYNC_RUN_FAILURE_CODES,
20
- SyncRunError,
21
- } from "@sixb/core/storage"
18
+ import { SYNC_RUN_FAILURE_CODES, SyncRunError } from "@sixb/core/storage"
22
19
  import { queryLatestRunsByOwnerId } from "./latest-run-query"
23
20
  import { installFreshSqliteSchema } from "./migrations"
24
21
  import {
@@ -1,5 +1,4 @@
1
1
  import type { Database } from "bun:sqlite"
2
- import { normalizeRequesterGroupIds } from "@sixb/core/internal/auth"
3
2
  import { parseSixbFailure, serializeSixbFailure } from "@sixb/core/internal/errors"
4
3
  import {
5
4
  assertWorkflowAgentNodeRunExecution,
@@ -112,9 +111,8 @@ export class SqliteWorkflowRunStorage implements WorkflowRunStorage {
112
111
  input,
113
112
  queued_at,
114
113
  started_at,
115
- requester_group_ids,
116
114
  attempt
117
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
115
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
118
116
  `
119
117
  )
120
118
  .run(
@@ -126,7 +124,6 @@ export class SqliteWorkflowRunStorage implements WorkflowRunStorage {
126
124
  serializeRecord(input.input),
127
125
  queuedAt.toISOString(),
128
126
  queuedAt.toISOString(),
129
- JSON.stringify(normalizeRequesterGroupIds(input.requesterGroupIds)),
130
127
  0
131
128
  )
132
129
  } catch (error) {
@@ -983,7 +980,6 @@ function rowToWorkflowRunRecord(row: WorkflowRunDatabaseRow): WorkflowRunRecord
983
980
  startedAt: new Date(row.started_at),
984
981
  finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
985
982
  error: row.error === null ? undefined : parseSixbFailure(row.error, WORKFLOW_RUN_FAILURE_CODES),
986
- requesterGroupIds: JSON.parse(row.requester_group_ids) as string[],
987
983
  attempt: row.attempt,
988
984
  ...(row.execution_token && row.execution_queue_lease_expires_at
989
985
  ? {
@@ -1046,7 +1042,6 @@ interface WorkflowRunDatabaseRow {
1046
1042
  started_at: string
1047
1043
  finished_at: string | null
1048
1044
  error: string | null
1049
- requester_group_ids: string
1050
1045
  attempt: number
1051
1046
  execution_token: string | null
1052
1047
  execution_queue_lease_expires_at: string | null