@boxxy/store-sqlite 0.1.0 → 0.1.3

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 (54) hide show
  1. package/README.md +24 -3
  2. package/dist/codecs/parse-json-text.d.ts +1 -0
  3. package/dist/codecs/parse-store-row.d.ts +2 -0
  4. package/dist/codecs/table-codecs.d.ts +10 -0
  5. package/dist/codecs/to-column-name.d.ts +1 -0
  6. package/dist/codecs/to-store-columns.d.ts +3 -0
  7. package/dist/constants.d.ts +1 -0
  8. package/dist/create-sqlite-claim-store.d.ts +5 -0
  9. package/dist/index.d.ts +1 -1
  10. package/dist/migrations/create-tables-migration.d.ts +2 -0
  11. package/dist/migrations/rename-relations-migration.d.ts +2 -0
  12. package/dist/migrations/run-store-migrations.d.ts +3 -0
  13. package/dist/migrations/v1-tables-migration.d.ts +2 -0
  14. package/dist/reads/build-sqlite-reader.d.ts +4 -0
  15. package/dist/reads/is-latest-of-session.d.ts +3 -0
  16. package/dist/reads/read-item-views.d.ts +5 -0
  17. package/dist/reads/read-limit-fills.d.ts +4 -0
  18. package/dist/reads/read-wakes.d.ts +4 -0
  19. package/dist/test-utils/create-claim-bench.d.ts +8 -0
  20. package/dist/test-utils/create-test-claim-store.d.ts +2 -0
  21. package/dist/types.d.ts +172 -0
  22. package/dist/with-busy-retry.d.ts +1 -0
  23. package/dist/with-transaction.d.ts +3 -0
  24. package/dist/writes/apply-sqlite-change.d.ts +4 -0
  25. package/dist/writes/create-sqlite-tx.d.ts +4 -0
  26. package/dist/writes/take-write-lock.d.ts +3 -0
  27. package/package.json +15 -1
  28. package/src/codecs/parse-json-text.ts +5 -0
  29. package/src/codecs/parse-store-row.ts +36 -0
  30. package/src/codecs/table-codecs.ts +148 -0
  31. package/src/codecs/to-column-name.ts +3 -0
  32. package/src/codecs/to-store-columns.ts +44 -0
  33. package/src/constants.ts +1 -0
  34. package/src/create-sqlite-claim-store.ts +114 -0
  35. package/src/index.ts +1 -1
  36. package/src/migrations/create-tables-migration.ts +146 -0
  37. package/src/migrations/rename-relations-migration.ts +17 -0
  38. package/src/migrations/run-store-migrations.ts +42 -0
  39. package/src/migrations/v1-tables-migration.ts +184 -0
  40. package/src/reads/build-sqlite-reader.ts +484 -0
  41. package/src/reads/is-latest-of-session.ts +21 -0
  42. package/src/reads/read-item-views.ts +92 -0
  43. package/src/reads/read-limit-fills.ts +32 -0
  44. package/src/reads/read-wakes.ts +34 -0
  45. package/src/test-utils/create-claim-bench.ts +66 -0
  46. package/src/test-utils/create-test-claim-store.ts +23 -0
  47. package/src/types.ts +173 -0
  48. package/src/with-busy-retry.ts +32 -0
  49. package/src/with-transaction.ts +29 -0
  50. package/src/writes/apply-sqlite-change.ts +223 -0
  51. package/src/writes/create-sqlite-tx.ts +229 -0
  52. package/src/writes/take-write-lock.ts +8 -0
  53. package/dist/package-name.d.ts +0 -1
  54. package/src/package-name.ts +0 -1
@@ -0,0 +1,114 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import type { ClaimStore, StoreReader } from '@boxxy/model';
3
+ import { buildSQLiteDialect } from '@zgeoff/kysely-bun-sqlite';
4
+ import { Kysely, sql } from 'kysely';
5
+ import { lockWaitLimitMs } from './constants.ts';
6
+ import { runStoreMigrations } from './migrations/run-store-migrations.ts';
7
+ import { buildSQLiteReader } from './reads/build-sqlite-reader.ts';
8
+ import type { StoreDatabase } from './types.ts';
9
+ import { withBusyRetry } from './with-busy-retry.ts';
10
+ import { withTransaction } from './with-transaction.ts';
11
+ import { createSQLiteTx } from './writes/create-sqlite-tx.ts';
12
+ import { takeWriteLock } from './writes/take-write-lock.ts';
13
+
14
+ export interface SQLiteClaimStore extends ClaimStore {
15
+ close: () => Promise<void>;
16
+ }
17
+
18
+ export async function createSQLiteClaimStore(path: string): Promise<SQLiteClaimStore> {
19
+ await using opened = new AsyncDisposableStack();
20
+
21
+ const writer = new Database(path, { readwrite: true, create: true });
22
+
23
+ opened.defer(() => {
24
+ writer.close();
25
+ });
26
+
27
+ writer.run('PRAGMA busy_timeout = 0');
28
+
29
+ // The file keeps its journal mode, so a read handle never sets it. SQLite fails these at once,
30
+ // whatever the busy timeout, while another handle switches the file to WAL or recovers it.
31
+ await withBusyRetry(lockWaitLimitMs, () => {
32
+ writer.run('PRAGMA journal_mode = WAL');
33
+ writer.run('PRAGMA synchronous = NORMAL');
34
+ });
35
+
36
+ const writerDB = new Kysely<StoreDatabase>({ dialect: buildSQLiteDialect(writer) });
37
+
38
+ opened.defer(() => writerDB.destroy());
39
+
40
+ await runStoreMigrations(writerDB);
41
+
42
+ const handles = opened.move();
43
+
44
+ const reads = new Set<Promise<unknown>>();
45
+
46
+ let isClosed = false;
47
+
48
+ return {
49
+ // Kysely holds the one connection of a handle from acquire to release, so two calls on this
50
+ // store run one at a time, and the write lock makes another store's writer wait.
51
+ exclusive: (work) =>
52
+ writerDB.connection().execute((db) =>
53
+ withTransaction(
54
+ db,
55
+ (locked) => takeWriteLock(locked, lockWaitLimitMs),
56
+ () => work(createSQLiteTx(db)),
57
+ ),
58
+ ),
59
+
60
+ read: async (work) => {
61
+ if (isClosed) {
62
+ throw new Error('the store is closed');
63
+ }
64
+
65
+ const reading = withReadHandle(path, work);
66
+
67
+ reads.add(reading);
68
+
69
+ try {
70
+ return await reading;
71
+ } finally {
72
+ reads.delete(reading);
73
+ }
74
+ },
75
+
76
+ close: async () => {
77
+ isClosed = true;
78
+
79
+ await Promise.allSettled(reads);
80
+ await handles.disposeAsync();
81
+ },
82
+ };
83
+ }
84
+
85
+ // A handle of its own per call, so a read sees the last commit and never a running exclusive
86
+ // call's writes, and a read started inside another read never waits for the outer one's handle.
87
+ async function withReadHandle<T>(
88
+ path: string,
89
+ work: (reader: StoreReader) => Promise<T>,
90
+ ): Promise<T> {
91
+ await using opened = new AsyncDisposableStack();
92
+
93
+ // without create, a missing file fails the open, where the default makes an empty file
94
+ const handle = new Database(path, { readwrite: true, create: false });
95
+
96
+ opened.defer(() => {
97
+ handle.close();
98
+ });
99
+
100
+ handle.run(`PRAGMA busy_timeout = ${lockWaitLimitMs}`);
101
+ handle.run('PRAGMA synchronous = NORMAL');
102
+
103
+ const readerDB = new Kysely<StoreDatabase>({ dialect: buildSQLiteDialect(handle) });
104
+
105
+ opened.defer(() => readerDB.destroy());
106
+
107
+ return await readerDB.connection().execute((db) =>
108
+ withTransaction(
109
+ db,
110
+ (connection) => sql`BEGIN`.execute(connection),
111
+ () => work(buildSQLiteReader(db)),
112
+ ),
113
+ );
114
+ }
package/src/index.ts CHANGED
@@ -1 +1 @@
1
- export { packageName } from './package-name.ts';
1
+ export { createSQLiteClaimStore, type SQLiteClaimStore } from './create-sqlite-claim-store.ts';
@@ -0,0 +1,146 @@
1
+ import { sql } from 'kysely';
2
+ import type { Kysely } from 'kysely';
3
+ import type { Migration } from 'kysely/migration';
4
+
5
+ // a STRICT table refuses a value of the wrong type, so a bad write fails at the store
6
+ const statements = [
7
+ sql`CREATE TABLE items (
8
+ id TEXT PRIMARY KEY,
9
+ kind TEXT NOT NULL,
10
+ repo TEXT NOT NULL,
11
+ title TEXT NOT NULL,
12
+ outcome TEXT NOT NULL,
13
+ acceptance TEXT NOT NULL,
14
+ found_in_attempt TEXT,
15
+ filed_by TEXT NOT NULL,
16
+ filed_at INTEGER NOT NULL,
17
+ closed_at INTEGER,
18
+ close_reason TEXT
19
+ ) STRICT`,
20
+ sql`CREATE TABLE links (
21
+ source TEXT NOT NULL,
22
+ target TEXT NOT NULL,
23
+ type TEXT NOT NULL,
24
+ added_by TEXT NOT NULL,
25
+ added_at INTEGER NOT NULL,
26
+ removed_at INTEGER,
27
+ PRIMARY KEY (source, target, type, added_at)
28
+ ) STRICT`,
29
+ sql`CREATE UNIQUE INDEX links_live ON links (source, target, type) WHERE removed_at IS NULL`,
30
+ sql`CREATE TABLE levers (
31
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ kind TEXT NOT NULL,
33
+ item TEXT NOT NULL,
34
+ until INTEGER,
35
+ expires_at INTEGER,
36
+ reason TEXT,
37
+ actor TEXT NOT NULL,
38
+ at INTEGER NOT NULL
39
+ ) STRICT`,
40
+ sql`CREATE INDEX levers_item_kind ON levers (item, kind)`,
41
+ sql`CREATE TABLE attempts (
42
+ id TEXT PRIMARY KEY,
43
+ item TEXT NOT NULL,
44
+ phase TEXT NOT NULL,
45
+ number INTEGER NOT NULL,
46
+ runner TEXT NOT NULL,
47
+ recipe_hash TEXT NOT NULL,
48
+ rules_version TEXT NOT NULL,
49
+ pool TEXT NOT NULL,
50
+ slot INTEGER NOT NULL,
51
+ slot_gen INTEGER NOT NULL,
52
+ lease_gen INTEGER NOT NULL,
53
+ spawn_request TEXT NOT NULL,
54
+ spawn_request_hash TEXT NOT NULL,
55
+ state TEXT NOT NULL,
56
+ failure TEXT,
57
+ atc_session TEXT,
58
+ close_out TEXT,
59
+ claimed_at INTEGER NOT NULL,
60
+ started_at INTEGER,
61
+ ended_at INTEGER,
62
+ UNIQUE (item, phase, number)
63
+ ) STRICT`,
64
+ sql`CREATE INDEX attempts_atc_session ON attempts (atc_session)`,
65
+ sql`CREATE TABLE attempt_sessions (
66
+ attempt TEXT NOT NULL,
67
+ agent_session TEXT NOT NULL UNIQUE,
68
+ atc_session TEXT NOT NULL,
69
+ seen_at INTEGER NOT NULL,
70
+ PRIMARY KEY (attempt, agent_session)
71
+ ) STRICT`,
72
+ sql`CREATE TABLE leases (
73
+ item TEXT NOT NULL,
74
+ phase TEXT NOT NULL,
75
+ attempt TEXT NOT NULL,
76
+ gen INTEGER NOT NULL,
77
+ taken_at INTEGER NOT NULL,
78
+ released_at INTEGER,
79
+ PRIMARY KEY (item, phase, gen)
80
+ ) STRICT`,
81
+ sql`CREATE UNIQUE INDEX leases_live ON leases (item, phase) WHERE released_at IS NULL`,
82
+ sql`CREATE TABLE pool_slots (
83
+ pool TEXT NOT NULL,
84
+ slot INTEGER NOT NULL,
85
+ holder TEXT,
86
+ gen INTEGER NOT NULL,
87
+ retired INTEGER NOT NULL,
88
+ PRIMARY KEY (pool, slot)
89
+ ) STRICT`,
90
+ sql`CREATE TABLE decisions (
91
+ id TEXT PRIMARY KEY,
92
+ attempt TEXT NOT NULL,
93
+ item TEXT NOT NULL,
94
+ pool TEXT NOT NULL,
95
+ rules_version TEXT NOT NULL,
96
+ snapshot_hash TEXT NOT NULL,
97
+ store_version INTEGER NOT NULL,
98
+ pepys_offsets TEXT NOT NULL,
99
+ faults TEXT NOT NULL,
100
+ at INTEGER NOT NULL
101
+ ) STRICT`,
102
+ sql`CREATE TABLE evidence (
103
+ fact_id TEXT PRIMARY KEY,
104
+ attempt TEXT NOT NULL,
105
+ kind TEXT NOT NULL,
106
+ ref TEXT NOT NULL,
107
+ observed TEXT,
108
+ source TEXT NOT NULL,
109
+ at INTEGER NOT NULL,
110
+ UNIQUE (attempt, kind, ref)
111
+ ) STRICT`,
112
+ sql`CREATE TABLE pull_requests (
113
+ repo TEXT NOT NULL,
114
+ number INTEGER NOT NULL,
115
+ attempt TEXT NOT NULL,
116
+ state TEXT NOT NULL,
117
+ merged_at INTEGER,
118
+ merge_commit TEXT,
119
+ checked_at INTEGER NOT NULL,
120
+ PRIMARY KEY (repo, number)
121
+ ) STRICT`,
122
+ sql`CREATE TABLE runners (
123
+ id TEXT PRIMARY KEY,
124
+ heartbeat_at INTEGER NOT NULL,
125
+ paused INTEGER NOT NULL
126
+ ) STRICT`,
127
+ sql`CREATE TABLE outbox (
128
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
129
+ fact_id TEXT NOT NULL UNIQUE,
130
+ type TEXT NOT NULL,
131
+ entity TEXT NOT NULL,
132
+ actor TEXT NOT NULL,
133
+ at INTEGER NOT NULL,
134
+ payload TEXT NOT NULL,
135
+ sent_at INTEGER
136
+ ) STRICT`,
137
+ sql`CREATE INDEX outbox_unsent ON outbox (seq) WHERE sent_at IS NULL`,
138
+ ];
139
+
140
+ export const createTablesMigration: Migration = {
141
+ up: async (db: Kysely<unknown>) => {
142
+ for (const statement of statements) {
143
+ await statement.execute(db);
144
+ }
145
+ },
146
+ };
@@ -0,0 +1,17 @@
1
+ import { sql } from 'kysely';
2
+ import type { Kysely } from 'kysely';
3
+ import type { Migration } from 'kysely/migration';
4
+
5
+ const statements = [
6
+ sql`ALTER TABLE links RENAME TO relations`,
7
+ sql`DROP INDEX links_live`,
8
+ sql`CREATE UNIQUE INDEX relations_live ON relations (source, target, type) WHERE removed_at IS NULL`,
9
+ ];
10
+
11
+ export const renameRelationsMigration: Migration = {
12
+ up: async (db: Kysely<unknown>) => {
13
+ for (const statement of statements) {
14
+ await statement.execute(db);
15
+ }
16
+ },
17
+ };
@@ -0,0 +1,42 @@
1
+ import type { Kysely } from 'kysely';
2
+ import { Migrator } from 'kysely/migration';
3
+ import { lockWaitLimitMs } from '../constants.ts';
4
+ import type { StoreDatabase } from '../types.ts';
5
+ import { withTransaction } from '../with-transaction.ts';
6
+ import { takeWriteLock } from '../writes/take-write-lock.ts';
7
+ import { createTablesMigration } from './create-tables-migration.ts';
8
+ import { renameRelationsMigration } from './rename-relations-migration.ts';
9
+ import { v1TablesMigration } from './v1-tables-migration.ts';
10
+
11
+ // Kysely's SQLite migration lock does nothing, so two openers of a new file would both find nothing
12
+ // run; the write lock makes the second one wait and then find the file migrated.
13
+ export async function runStoreMigrations(db: Kysely<StoreDatabase>): Promise<void> {
14
+ await db.connection().execute((connection) =>
15
+ withTransaction(
16
+ connection,
17
+ (locked) => takeWriteLock(locked, lockWaitLimitMs),
18
+ () => runMigrator(connection),
19
+ ),
20
+ );
21
+ }
22
+
23
+ // Kysely runs the migrations in the order of their names, and skips each one the file has run.
24
+ const migrations = {
25
+ '0001-create-tables': createTablesMigration,
26
+ '0002-rename-relations': renameRelationsMigration,
27
+ '0003-v1-tables': v1TablesMigration,
28
+ };
29
+
30
+ async function runMigrator(db: Kysely<StoreDatabase>): Promise<void> {
31
+ const migrator = new Migrator({
32
+ db,
33
+ provider: { getMigrations: () => Promise.resolve(migrations) },
34
+ disableTransactions: true,
35
+ });
36
+
37
+ const outcome = await migrator.migrateToLatest();
38
+
39
+ if (outcome.error !== undefined) {
40
+ throw new Error('the store migrations failed', { cause: outcome.error });
41
+ }
42
+ }
@@ -0,0 +1,184 @@
1
+ import { sql } from 'kysely';
2
+ import type { Kysely } from 'kysely';
3
+ import type { Migration } from 'kysely/migration';
4
+
5
+ // Slice 1 never ran outside tests, so this rebuilds each changed table empty, in place of ALTER
6
+ // statements that would need an invented default for every new NOT NULL column. A file that
7
+ // already holds an item or a fact is refused, so no row is ever dropped.
8
+ const statements = [
9
+ sql`DROP TABLE items`,
10
+ sql`CREATE TABLE items (
11
+ id TEXT PRIMARY KEY,
12
+ kind TEXT NOT NULL,
13
+ repo TEXT NOT NULL,
14
+ title TEXT NOT NULL,
15
+ outcome TEXT NOT NULL,
16
+ acceptance TEXT NOT NULL,
17
+ fields TEXT NOT NULL,
18
+ labels TEXT NOT NULL,
19
+ found_in_attempt TEXT,
20
+ filed_by TEXT NOT NULL,
21
+ filed_at INTEGER NOT NULL,
22
+ closed_at INTEGER,
23
+ close_reason TEXT,
24
+ kept_session TEXT
25
+ ) STRICT`,
26
+ sql`DROP TABLE levers`,
27
+ sql`CREATE TABLE levers (
28
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ kind TEXT NOT NULL,
30
+ item TEXT NOT NULL,
31
+ until INTEGER,
32
+ expires_at INTEGER,
33
+ reason TEXT,
34
+ phase TEXT,
35
+ actor TEXT NOT NULL,
36
+ at INTEGER NOT NULL
37
+ ) STRICT`,
38
+ sql`CREATE INDEX levers_item_kind ON levers (item, kind)`,
39
+ sql`DROP TABLE attempts`,
40
+ sql`CREATE TABLE attempts (
41
+ id TEXT PRIMARY KEY,
42
+ item TEXT NOT NULL,
43
+ phase TEXT NOT NULL,
44
+ number INTEGER NOT NULL,
45
+ runner TEXT NOT NULL,
46
+ recipe_hash TEXT NOT NULL,
47
+ rules_version TEXT NOT NULL,
48
+ pool TEXT NOT NULL,
49
+ lease_gen INTEGER NOT NULL,
50
+ spawn_request TEXT NOT NULL,
51
+ spawn_request_hash TEXT NOT NULL,
52
+ state TEXT NOT NULL,
53
+ failure TEXT,
54
+ atc_session TEXT,
55
+ caller TEXT,
56
+ claimed_at INTEGER NOT NULL,
57
+ started_at INTEGER,
58
+ ended_at INTEGER,
59
+ fields TEXT NOT NULL,
60
+ mode TEXT NOT NULL,
61
+ round INTEGER NOT NULL,
62
+ input TEXT,
63
+ hold_gen INTEGER NOT NULL,
64
+ close_out TEXT,
65
+ close_out_hash TEXT,
66
+ close_out_fact TEXT,
67
+ closing TEXT,
68
+ closing_at INTEGER,
69
+ suspend_requested_at INTEGER,
70
+ slept_at INTEGER,
71
+ wake_requested_at INTEGER,
72
+ woken_at INTEGER,
73
+ outcome TEXT,
74
+ returned_outcome TEXT,
75
+ strayed_at INTEGER,
76
+ session_stopped_at INTEGER,
77
+ session_forgotten_at INTEGER,
78
+ UNIQUE (item, phase, number)
79
+ ) STRICT`,
80
+ sql`CREATE INDEX attempts_atc_session ON attempts (atc_session)`,
81
+ sql`DROP TABLE attempt_sessions`,
82
+
83
+ // a kept session binds one agent session to each later attempt, so the agent session is no key
84
+ sql`CREATE TABLE attempt_sessions (
85
+ attempt TEXT NOT NULL,
86
+ agent_session TEXT NOT NULL,
87
+ atc_session TEXT NOT NULL,
88
+ product TEXT NOT NULL,
89
+ source TEXT NOT NULL,
90
+ seen_at INTEGER NOT NULL,
91
+ PRIMARY KEY (attempt, agent_session)
92
+ ) STRICT`,
93
+ sql`CREATE INDEX attempt_sessions_agent_session ON attempt_sessions (agent_session)`,
94
+ sql`DROP TABLE pool_slots`,
95
+ sql`CREATE TABLE limits (
96
+ name TEXT PRIMARY KEY,
97
+ max INTEGER NOT NULL,
98
+ counts TEXT NOT NULL
99
+ ) STRICT`,
100
+ sql`CREATE TABLE limit_seats (
101
+ "limit" TEXT NOT NULL,
102
+ attempt TEXT NOT NULL,
103
+ hold_gen INTEGER NOT NULL,
104
+ over_cap INTEGER NOT NULL,
105
+ taken_at INTEGER NOT NULL,
106
+ released_at INTEGER
107
+ ) STRICT`,
108
+ sql`CREATE UNIQUE INDEX limit_seats_held ON limit_seats ("limit", attempt, hold_gen)
109
+ WHERE released_at IS NULL`,
110
+ sql`DROP TABLE decisions`,
111
+ sql`CREATE TABLE decisions (
112
+ id TEXT PRIMARY KEY,
113
+ attempt TEXT NOT NULL,
114
+ item TEXT NOT NULL,
115
+ kind TEXT NOT NULL,
116
+ pool TEXT NOT NULL,
117
+ rules_version TEXT NOT NULL,
118
+ snapshot_hash TEXT NOT NULL,
119
+ store_version INTEGER NOT NULL,
120
+ pepys_offsets TEXT NOT NULL,
121
+ faults TEXT NOT NULL,
122
+ limits TEXT NOT NULL,
123
+ at INTEGER NOT NULL
124
+ ) STRICT`,
125
+ sql`DROP TABLE pull_requests`,
126
+ sql`CREATE TABLE pull_requests (
127
+ repo TEXT NOT NULL,
128
+ number INTEGER NOT NULL,
129
+ attempt TEXT NOT NULL,
130
+ github_id INTEGER NOT NULL,
131
+ url TEXT NOT NULL,
132
+ state TEXT NOT NULL,
133
+ merged_at INTEGER,
134
+ merge_commit TEXT,
135
+ checked_at INTEGER NOT NULL,
136
+ PRIMARY KEY (repo, number)
137
+ ) STRICT`,
138
+ sql`CREATE TABLE notes (
139
+ item TEXT NOT NULL,
140
+ seq INTEGER NOT NULL,
141
+ text TEXT NOT NULL,
142
+ "by" TEXT NOT NULL,
143
+ at INTEGER NOT NULL,
144
+ PRIMARY KEY (item, seq)
145
+ ) STRICT`,
146
+ sql`CREATE TABLE notices (
147
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
148
+ item TEXT NOT NULL,
149
+ reason TEXT NOT NULL,
150
+ at INTEGER NOT NULL,
151
+ tries INTEGER NOT NULL,
152
+ sent_at INTEGER
153
+ ) STRICT`,
154
+ sql`CREATE INDEX notices_unsent ON notices (id) WHERE sent_at IS NULL`,
155
+ sql`DROP TABLE outbox`,
156
+ sql`CREATE TABLE outbox (
157
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
158
+ fact_id TEXT NOT NULL UNIQUE,
159
+ type TEXT NOT NULL,
160
+ entity TEXT NOT NULL,
161
+ actor TEXT NOT NULL,
162
+ at INTEGER NOT NULL,
163
+ payload TEXT NOT NULL,
164
+ sent_at INTEGER,
165
+ parked_at INTEGER,
166
+ parked_reason TEXT
167
+ ) STRICT`,
168
+ sql`CREATE INDEX outbox_unsent ON outbox (seq) WHERE sent_at IS NULL AND parked_at IS NULL`,
169
+ sql`CREATE INDEX outbox_behind ON outbox (entity) WHERE sent_at IS NULL`,
170
+ ];
171
+
172
+ export const v1TablesMigration: Migration = {
173
+ up: async (db: Kysely<unknown>) => {
174
+ const found = await sql`SELECT 1 FROM items UNION ALL SELECT 1 FROM outbox LIMIT 1`.execute(db);
175
+
176
+ if (found.rows.length > 0) {
177
+ throw new Error('the store holds slice 1 rows, which the v1 tables cannot take');
178
+ }
179
+
180
+ for (const statement of statements) {
181
+ await statement.execute(db);
182
+ }
183
+ },
184
+ };