@sixb/sqlite 0.1.18 → 0.1.19

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.
@@ -0,0 +1,149 @@
1
+ -- Actions run in the request that asks for them and record their run once, when it ends. No run
2
+ -- is queued, running, or cancelled any more, and every run has started and finished. SQLite cannot
3
+ -- alter a CHECK constraint, so the table is rebuilt.
4
+ CREATE TABLE action_runs_v2 (
5
+ project_id TEXT NOT NULL,
6
+ id TEXT NOT NULL,
7
+ execution_id TEXT NOT NULL,
8
+ action_id TEXT NOT NULL,
9
+ subject_kind TEXT NOT NULL CHECK (subject_kind IN ('none', 'object')),
10
+ object_type_id TEXT,
11
+ primary_id TEXT,
12
+ status TEXT NOT NULL CHECK (status IN ('succeeded', 'failed')),
13
+ phase TEXT NOT NULL CHECK (phase IN ('validation', 'writeback', 'edits', 'commit', 'effects')),
14
+ started_at TEXT NOT NULL,
15
+ finished_at TEXT NOT NULL,
16
+ params TEXT NOT NULL,
17
+ idempotency_key TEXT NOT NULL,
18
+ writeback_status TEXT CHECK (writeback_status IS NULL OR writeback_status IN ('succeeded', 'failed')),
19
+ writeback_completed_at TEXT,
20
+ writeback_result TEXT,
21
+ writeback_error TEXT CHECK (writeback_error IS NULL OR json_valid(writeback_error)),
22
+ effects_status TEXT CHECK (effects_status IS NULL OR effects_status IN ('succeeded', 'failed')),
23
+ effects_completed_at TEXT,
24
+ effects_error TEXT CHECK (effects_error IS NULL OR json_valid(effects_error)),
25
+ error TEXT CHECK (error IS NULL OR json_valid(error)),
26
+ CHECK (
27
+ (subject_kind = 'none' AND object_type_id IS NULL AND primary_id IS NULL)
28
+ OR (subject_kind = 'object' AND object_type_id IS NOT NULL AND primary_id IS NOT NULL)
29
+ ),
30
+ CHECK ((status = 'failed') = (error IS NOT NULL)),
31
+ PRIMARY KEY (project_id, id),
32
+ UNIQUE (project_id, execution_id)
33
+ );
34
+
35
+ INSERT INTO action_runs_v2 (
36
+ project_id, id, execution_id, action_id, subject_kind, object_type_id, primary_id, status,
37
+ phase, started_at, finished_at, params, idempotency_key, writeback_status,
38
+ writeback_completed_at, writeback_result, writeback_error, effects_status,
39
+ effects_completed_at, effects_error, error
40
+ )
41
+ SELECT
42
+ project_id, id, execution_id, action_id, subject_kind, object_type_id, primary_id,
43
+ migrated_status, migrated_phase,
44
+ -- A run starts when it was queued at the latest.
45
+ COALESCE(started_at, queued_at),
46
+ migrated_finished_at,
47
+ params, idempotency_key, writeback_status,
48
+ CASE
49
+ WHEN writeback_status IS NULL THEN NULL
50
+ ELSE COALESCE(writeback_completed_at, migrated_finished_at)
51
+ END,
52
+ writeback_result, writeback_error,
53
+ CASE WHEN effects_abandoned THEN 'failed' ELSE effects_status END,
54
+ CASE
55
+ WHEN effects_abandoned THEN migrated_at
56
+ WHEN effects_status IS NULL THEN NULL
57
+ ELSE COALESCE(effects_completed_at, migrated_finished_at)
58
+ END,
59
+ CASE
60
+ WHEN effects_abandoned THEN json_object(
61
+ 'code', 'internal.unexpected',
62
+ 'message', 'Abandoned: Actions now run synchronously; these effects never finished.',
63
+ 'retryable', json('false'),
64
+ 'at', migrated_at,
65
+ 'details', json_object('actionId', action_id, 'runId', id, 'phase', 'effects')
66
+ )
67
+ ELSE effects_error
68
+ END,
69
+ -- A failure names the phase its run ended in.
70
+ CASE
71
+ WHEN migrated_error IS NULL THEN NULL
72
+ WHEN json_extract(migrated_error, '$.details.phase') IS migrated_phase THEN migrated_error
73
+ ELSE json_set(migrated_error, '$.details.phase', migrated_phase)
74
+ END
75
+ FROM (
76
+ SELECT
77
+ *,
78
+ CASE
79
+ WHEN status = 'succeeded' OR edits_committed THEN 'succeeded'
80
+ ELSE 'failed'
81
+ END AS migrated_status,
82
+ CASE
83
+ WHEN edits_committed THEN CASE WHEN phase = 'effects' THEN 'effects' ELSE 'commit' END
84
+ -- The phases `request`, `enqueue`, and `cancelled` are gone: a run that stopped in one of
85
+ -- them never got past validation.
86
+ WHEN phase IN ('validation', 'writeback', 'edits', 'commit', 'effects') THEN phase
87
+ ELSE 'validation'
88
+ END AS migrated_phase,
89
+ CASE
90
+ WHEN edits_committed THEN committed_at
91
+ WHEN status IN ('queued', 'running') THEN migrated_at
92
+ ELSE COALESCE(finished_at, migrated_at)
93
+ END AS migrated_finished_at,
94
+ edits_committed AND phase = 'effects' AND effects_status IS NULL AS effects_abandoned,
95
+ CASE
96
+ WHEN edits_committed THEN NULL
97
+ WHEN status IN ('queued', 'running') THEN json_object(
98
+ 'code', 'internal.unexpected',
99
+ 'message', 'Abandoned: Actions now run synchronously; this run never finished.',
100
+ 'retryable', json('false'),
101
+ 'at', migrated_at,
102
+ 'details', json_object('actionId', action_id, 'runId', id)
103
+ )
104
+ -- Every failed run carries a failure. One recorded before failures were kept explains
105
+ -- itself.
106
+ WHEN status IN ('failed', 'cancelled') AND error IS NULL THEN json_object(
107
+ 'code', 'internal.unexpected',
108
+ 'message', 'This run failed before Sixb recorded why.',
109
+ 'retryable', json('false'),
110
+ 'at', COALESCE(finished_at, migrated_at),
111
+ 'details', json_object('actionId', action_id, 'runId', id)
112
+ )
113
+ -- An enqueue failure, which no run can meet any more, reads as the unexpected error it
114
+ -- now is.
115
+ WHEN json_extract(error, '$.code') = 'queue.enqueue_failed'
116
+ THEN json_set(error, '$.code', 'internal.unexpected', '$.retryable', json('false'))
117
+ ELSE error
118
+ END AS migrated_error
119
+ FROM (
120
+ SELECT
121
+ run.*,
122
+ committed.committed_at,
123
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now') AS migrated_at,
124
+ -- A run left running after its edits committed, as a crash between the commit and the end
125
+ -- of the run left it, succeeded: its edits and their outbox facts are in the commit. A run
126
+ -- that stopped during its effects keeps that phase, and records effects that never ended.
127
+ -- Any other run left queued or running never finished, and nothing will resume it. A
128
+ -- cancelled run failed, and keeps the failure it recorded.
129
+ run.status IN ('queued', 'running') AND committed.committed_at IS NOT NULL
130
+ AS edits_committed
131
+ FROM action_runs AS run
132
+ LEFT JOIN ontology_commits AS committed
133
+ ON committed.origin_kind = 'action'
134
+ AND committed.project_id = run.project_id
135
+ AND committed.origin_run_id = run.id
136
+ )
137
+ );
138
+
139
+ DROP TABLE action_runs;
140
+ ALTER TABLE action_runs_v2 RENAME TO action_runs;
141
+
142
+ CREATE INDEX idx_action_runs_project_started
143
+ ON action_runs(project_id, started_at DESC);
144
+ CREATE INDEX idx_action_runs_project_action_started
145
+ ON action_runs(project_id, action_id, started_at DESC);
146
+ CREATE INDEX idx_action_runs_project_object_started
147
+ ON action_runs(project_id, object_type_id, primary_id, started_at DESC);
148
+ CREATE INDEX idx_action_runs_project_status_started
149
+ ON action_runs(project_id, status, started_at DESC);
package/src/migrations.ts CHANGED
@@ -271,6 +271,7 @@ export const sqliteStorageMigrations = defineMigrations({
271
271
  sqliteSql("057-reranking-model-kind", rerankingModelKindSql),
272
272
  sqliteSql("058-file-download-grants", fileDownloadGrantsSql),
273
273
  sqliteSql("059-drained-source-versions", drainedSourceVersionsSql),
274
+ sqliteSql("060-terminal-action-runs", terminalActionRunsSql),
274
275
  ],
275
276
  })
276
277
 
@@ -597,3 +598,4 @@ import fileDownloadGrantsSql from "./migrations/058-file-download-grants.sql" wi
597
598
  import drainedSourceVersionsSql from "./migrations/059-drained-source-versions.sql" with {
598
599
  type: "text",
599
600
  }
601
+ import terminalActionRunsSql from "./migrations/060-terminal-action-runs.sql" with { type: "text" }