@boxxy/store-sqlite 0.0.0 → 0.1.1

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,321 @@
1
+ import { brandSchemas, outboxFactSchema } from '@boxxy/model';
2
+ import type { ItemView, ItemWithLevers, PoolName, StoreReader } from '@boxxy/model';
3
+ import type { Kysely } from 'kysely';
4
+ import { parseStoreRow } from './parse-store-row.ts';
5
+ import { readItemViews } from './read-item-views.ts';
6
+ import type { StoreDatabase } from './types.ts';
7
+
8
+ export function buildSQLiteReader(db: Kysely<StoreDatabase>): StoreReader {
9
+ return {
10
+ readSnapshot: async (now, maxAttemptsByKind) => {
11
+ const views = await readItemViews(
12
+ db,
13
+ (builder) => builder('closed_at', 'is', null),
14
+ now,
15
+ maxAttemptsByKind,
16
+ );
17
+
18
+ const open = views.filter((item) => isOpen(item));
19
+
20
+ const links = await db
21
+ .selectFrom('links')
22
+ .selectAll()
23
+ .where('removed_at', 'is', null)
24
+ .orderBy('added_at')
25
+ .orderBy('source')
26
+ .orderBy('target')
27
+ .execute();
28
+
29
+ const version = await db
30
+ .selectFrom('outbox')
31
+ .select((builder) => builder.fn.max('seq').as('seq'))
32
+ .executeTakeFirst();
33
+
34
+ return {
35
+ storeVersion: version?.seq ?? 0,
36
+ items: open.map((item) => toItemView(item)),
37
+ links: links.map((row) => parseStoreRow('links', row)),
38
+ levers: open
39
+ .flatMap((item) => item.levers)
40
+ .toSorted((first, second) => first.seq - second.seq),
41
+ freeSlots: await readFreeSlots(db),
42
+ };
43
+ },
44
+ readItem: async (id, now, maxAttemptsByKind) => {
45
+ const views = await readItemViews(
46
+ db,
47
+ (builder) => builder('id', '=', id),
48
+ now,
49
+ maxAttemptsByKind,
50
+ );
51
+
52
+ const item = views.at(0);
53
+
54
+ return item === undefined ? null : toItemView(item);
55
+ },
56
+ readItemDetail: async (id, now, maxAttemptsByKind) => {
57
+ const views = await readItemViews(
58
+ db,
59
+ (builder) => builder('id', '=', id),
60
+ now,
61
+ maxAttemptsByKind,
62
+ );
63
+
64
+ const item = views.at(0);
65
+
66
+ if (item === undefined) {
67
+ return null;
68
+ }
69
+
70
+ const attemptsOfItem = db.selectFrom('attempts').select('id').where('item', '=', id);
71
+
72
+ const attempts = await db
73
+ .selectFrom('attempts')
74
+ .selectAll()
75
+ .where('item', '=', id)
76
+ .orderBy('claimed_at')
77
+ .orderBy('id')
78
+ .execute();
79
+
80
+ const links = await db
81
+ .selectFrom('links')
82
+ .selectAll()
83
+ .where('removed_at', 'is', null)
84
+ .where((builder) => builder.or([builder('source', '=', id), builder('target', '=', id)]))
85
+ .orderBy('added_at')
86
+ .orderBy('source')
87
+ .orderBy('target')
88
+ .execute();
89
+
90
+ const evidence = await db
91
+ .selectFrom('evidence')
92
+ .selectAll()
93
+ .where('attempt', 'in', attemptsOfItem)
94
+ .orderBy('at')
95
+ .orderBy('fact_id')
96
+ .execute();
97
+
98
+ const pullRequests = await db
99
+ .selectFrom('pull_requests')
100
+ .selectAll()
101
+ .where('attempt', 'in', attemptsOfItem)
102
+ .orderBy('repo')
103
+ .orderBy('number')
104
+ .execute();
105
+
106
+ return {
107
+ item,
108
+ attempts: attempts.map((row) => parseStoreRow('attempts', row)),
109
+ links: links.map((row) => parseStoreRow('links', row)),
110
+ evidence: evidence.map((row) => parseStoreRow('evidence', row)),
111
+ pullRequests: pullRequests.map((row) => parseStoreRow('pull_requests', row)),
112
+ };
113
+ },
114
+ listItems: async (filter, now, maxAttemptsByKind) => {
115
+ const repo = filter.repo;
116
+
117
+ const views = await readItemViews(
118
+ db,
119
+ (builder) => (repo === undefined ? builder.lit(true) : builder('repo', '=', repo)),
120
+ now,
121
+ maxAttemptsByKind,
122
+ );
123
+
124
+ return views.filter(
125
+ (item) => filter.state === undefined || item.derived.state === filter.state,
126
+ );
127
+ },
128
+ listOpenIssues: async (repo, now, maxAttemptsByKind) => {
129
+ const views = await readItemViews(
130
+ db,
131
+ (builder) =>
132
+ builder.and([
133
+ builder('kind', '=', 'core.issue'),
134
+ builder('repo', '=', repo),
135
+ builder('closed_at', 'is', null),
136
+ ]),
137
+ now,
138
+ maxAttemptsByKind,
139
+ );
140
+
141
+ return views.filter((item) => isOpen(item)).map((item) => toItemView(item));
142
+ },
143
+ readLastLeaseGen: async (item, phase) => {
144
+ const row = await db
145
+ .selectFrom('leases')
146
+ .select((builder) => builder.fn.max('gen').as('gen'))
147
+ .where('item', '=', item)
148
+ .where('phase', '=', phase)
149
+ .executeTakeFirst();
150
+
151
+ return row?.gen ?? 0;
152
+ },
153
+ readAttempt: async (id) => {
154
+ const row = await db
155
+ .selectFrom('attempts')
156
+ .selectAll()
157
+ .where('id', '=', id)
158
+ .executeTakeFirst();
159
+
160
+ return row === undefined ? null : parseStoreRow('attempts', row);
161
+ },
162
+ listEvidence: async (attempt) => {
163
+ const rows = await db
164
+ .selectFrom('evidence')
165
+ .selectAll()
166
+ .where('attempt', '=', attempt)
167
+ .orderBy('at')
168
+ .orderBy('fact_id')
169
+ .execute();
170
+
171
+ return rows.map((row) => parseStoreRow('evidence', row));
172
+ },
173
+ listLiveAttempts: async () => {
174
+ const rows = await db
175
+ .selectFrom('attempts')
176
+ .selectAll()
177
+ .where('state', 'in', ['starting', 'running', 'reconciling'])
178
+ .orderBy('claimed_at')
179
+ .orderBy('id')
180
+ .execute();
181
+
182
+ return rows.map((row) => parseStoreRow('attempts', row));
183
+ },
184
+ listSucceededAttemptsWithoutPr: async () => {
185
+ const linked = db.selectFrom('pull_requests').select('attempt');
186
+
187
+ const rows = await db
188
+ .selectFrom('attempts')
189
+ .selectAll()
190
+ .where('state', '=', 'succeeded')
191
+ .where('id', 'not in', linked)
192
+ .orderBy('claimed_at')
193
+ .orderBy('id')
194
+ .execute();
195
+
196
+ return rows.map((row) => parseStoreRow('attempts', row));
197
+ },
198
+ // oxlint-disable-next-line zgeoff/verb-contract -- the StoreReader port in @boxxy/model fixes this name
199
+ findAttemptBySession: async (session) => {
200
+ const row = await db
201
+ .selectFrom('attempts')
202
+ .selectAll()
203
+ .where('atc_session', '=', session)
204
+ .orderBy('claimed_at')
205
+ .orderBy('id')
206
+ .executeTakeFirst();
207
+
208
+ return row === undefined ? null : parseStoreRow('attempts', row);
209
+ },
210
+ // oxlint-disable-next-line zgeoff/verb-contract -- the StoreReader port in @boxxy/model fixes this name
211
+ findAttemptByAgentSession: async (session) => {
212
+ const row = await db
213
+ .selectFrom('attempt_sessions')
214
+ .select('attempt')
215
+ .where('agent_session', '=', session)
216
+ .executeTakeFirst();
217
+
218
+ return row === undefined ? null : brandSchemas.attemptId.parse(row.attempt);
219
+ },
220
+ listWatchedPullRequests: async () => {
221
+ const rows = await db
222
+ .selectFrom('pull_requests')
223
+ .selectAll()
224
+ .where('state', '=', 'open')
225
+ .orderBy('repo')
226
+ .orderBy('number')
227
+ .execute();
228
+
229
+ return rows.map((row) => parseStoreRow('pull_requests', row));
230
+ },
231
+ listPoolSlots: async () => {
232
+ const rows = await db
233
+ .selectFrom('pool_slots')
234
+ .selectAll()
235
+ .orderBy('pool')
236
+ .orderBy('slot')
237
+ .execute();
238
+
239
+ return rows.map((row) => parseStoreRow('pool_slots', row));
240
+ },
241
+ readUnsentFacts: async (limit) => {
242
+ const rows = await db
243
+ .selectFrom('outbox')
244
+ .selectAll()
245
+ .where('sent_at', 'is', null)
246
+ .orderBy('seq')
247
+ .limit(limit)
248
+ .execute();
249
+
250
+ return rows.map((row) =>
251
+ outboxFactSchema.parse({
252
+ seq: row.seq,
253
+ factId: row.fact_id,
254
+ type: row.type,
255
+ entity: row.entity,
256
+ actor: row.actor,
257
+ at: row.at,
258
+ payload: parseJSONText(row.payload),
259
+ sentAt: row.sent_at,
260
+ }),
261
+ );
262
+ },
263
+ readOutboxLag: async () => {
264
+ const row = await db
265
+ .selectFrom('outbox')
266
+ .select((builder) => [
267
+ builder.fn.countAll<number>().as('unsent'),
268
+ builder.fn.min('at').as('oldest'),
269
+ ])
270
+ .where('sent_at', 'is', null)
271
+ .executeTakeFirstOrThrow();
272
+
273
+ const oldest = row.oldest === null ? null : brandSchemas.instant.parse(row.oldest);
274
+
275
+ return { unsent: row.unsent, oldestUnsentAt: oldest };
276
+ },
277
+ readRunner: async (id) => {
278
+ const row = await db
279
+ .selectFrom('runners')
280
+ .selectAll()
281
+ .where('id', '=', id)
282
+ .executeTakeFirst();
283
+
284
+ return row === undefined ? null : parseStoreRow('runners', row);
285
+ },
286
+ };
287
+ }
288
+
289
+ function isOpen(item: ItemView): boolean {
290
+ return item.derived.state !== 'done' && item.derived.state !== 'archived';
291
+ }
292
+
293
+ function toItemView(item: ItemWithLevers): ItemView {
294
+ const { levers: _levers, ...view } = item;
295
+
296
+ return view;
297
+ }
298
+
299
+ async function readFreeSlots(db: Kysely<StoreDatabase>): Promise<Record<PoolName, number>> {
300
+ const rows = await db
301
+ .selectFrom('pool_slots')
302
+ .select(['pool', 'holder'])
303
+ .where('retired', '=', 0)
304
+ .execute();
305
+
306
+ const free: Record<PoolName, number> = {};
307
+
308
+ for (const row of rows) {
309
+ const pool = brandSchemas.poolName.parse(row.pool);
310
+
311
+ free[pool] = (free[pool] ?? 0) + (row.holder === null ? 1 : 0);
312
+ }
313
+
314
+ return free;
315
+ }
316
+
317
+ function parseJSONText(text: string): unknown {
318
+ const parsed: unknown = JSON.parse(text);
319
+
320
+ return parsed;
321
+ }
@@ -0,0 +1,92 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import type { ClaimStore } from '@boxxy/model';
3
+ import { buildSQLiteDialect } from '@zgeoff/kysely-bun-sqlite';
4
+ import { Kysely, sql } from 'kysely';
5
+ import type { RawBuilder } from 'kysely';
6
+ import { buildSQLiteReader } from './build-sqlite-reader.ts';
7
+ import { createSQLiteTx } from './create-sqlite-tx.ts';
8
+ import { runStoreMigrations } from './run-store-migrations.ts';
9
+ import type { StoreDatabase } from './types.ts';
10
+
11
+ export interface SQLiteClaimStore extends ClaimStore {
12
+ close: () => Promise<void>;
13
+ }
14
+
15
+ export async function createSQLiteClaimStore(path: string): Promise<SQLiteClaimStore> {
16
+ await using opened = new AsyncDisposableStack();
17
+
18
+ const writer = openStoreHandle(path);
19
+
20
+ opened.defer(() => {
21
+ writer.close();
22
+ });
23
+
24
+ const reader = openStoreHandle(path);
25
+
26
+ opened.defer(() => {
27
+ reader.close();
28
+ });
29
+
30
+ const writerDB = new Kysely<StoreDatabase>({ dialect: buildSQLiteDialect(writer) });
31
+ const readerDB = new Kysely<StoreDatabase>({ dialect: buildSQLiteDialect(reader) });
32
+
33
+ opened.defer(() => readerDB.destroy());
34
+ opened.defer(() => writerDB.destroy());
35
+
36
+ await runStoreMigrations(writerDB);
37
+
38
+ const handles = opened.move();
39
+
40
+ return {
41
+ // Kysely holds the one connection of a handle from acquire to release, so two calls on this
42
+ // store run one at a time, and BEGIN IMMEDIATE makes another process's writer wait.
43
+ exclusive: (work) =>
44
+ writerDB
45
+ .connection()
46
+ .execute((db) => withTransaction(db, sql`BEGIN IMMEDIATE`, () => work(createSQLiteTx(db)))),
47
+
48
+ // a second handle, so a read sees the last commit and never a running exclusive call's writes
49
+ read: (work) =>
50
+ readerDB
51
+ .connection()
52
+ .execute((db) => withTransaction(db, sql`BEGIN`, () => work(buildSQLiteReader(db)))),
53
+
54
+ close: () => handles.disposeAsync(),
55
+ };
56
+ }
57
+
58
+ function openStoreHandle(path: string): Database {
59
+ const handle = new Database(path, { create: true });
60
+
61
+ handle.run('PRAGMA journal_mode = WAL');
62
+ handle.run('PRAGMA synchronous = NORMAL');
63
+ handle.run('PRAGMA busy_timeout = 5000');
64
+
65
+ return handle;
66
+ }
67
+
68
+ async function withTransaction<T>(
69
+ db: Kysely<StoreDatabase>,
70
+ begin: RawBuilder<unknown>,
71
+ work: () => Promise<T>,
72
+ ): Promise<T> {
73
+ await begin.execute(db);
74
+
75
+ try {
76
+ const result = await work();
77
+
78
+ await sql`COMMIT`.execute(db);
79
+
80
+ return result;
81
+ } catch (error) {
82
+ // SQLite ends the transaction by itself after some errors, such as a full disk, and ROLLBACK
83
+ // then fails; its error joins the first one, never replaces it
84
+ await sql`ROLLBACK`.execute(db).catch((rollbackError: unknown) => {
85
+ throw new AggregateError([error, rollbackError], 'the work failed, and so did its rollback', {
86
+ cause: error,
87
+ });
88
+ });
89
+
90
+ throw error;
91
+ }
92
+ }
@@ -0,0 +1,134 @@
1
+ import type { FactId, Instant, PullRequestRef, RunnerId, StoreChange, StoreTx } from '@boxxy/model';
2
+ import type { Kysely } from 'kysely';
3
+ import { applySQLiteChange } from './apply-sqlite-change.ts';
4
+ import { buildSQLiteReader } from './build-sqlite-reader.ts';
5
+ import type { StoreDatabase } from './types.ts';
6
+
7
+ type PoolCounts = Parameters<StoreTx['syncPoolSlots']>[0];
8
+
9
+ // A call started beside a change would run its statements inside the change's savepoint, and a
10
+ // refusal would roll them back after the call resolved, so every call waits for the one before it.
11
+ export function createSQLiteTx(db: Kysely<StoreDatabase>): StoreTx {
12
+ const inTurn = makeTurnRunner();
13
+ const reader = buildSQLiteReader(db);
14
+
15
+ return {
16
+ readSnapshot: inTurn(reader.readSnapshot),
17
+ readItem: inTurn(reader.readItem),
18
+ readItemDetail: inTurn(reader.readItemDetail),
19
+ listItems: inTurn(reader.listItems),
20
+ listOpenIssues: inTurn(reader.listOpenIssues),
21
+ readLastLeaseGen: inTurn(reader.readLastLeaseGen),
22
+ readAttempt: inTurn(reader.readAttempt),
23
+ listEvidence: inTurn(reader.listEvidence),
24
+ listLiveAttempts: inTurn(reader.listLiveAttempts),
25
+ listSucceededAttemptsWithoutPr: inTurn(reader.listSucceededAttemptsWithoutPr),
26
+ findAttemptBySession: inTurn(reader.findAttemptBySession),
27
+ findAttemptByAgentSession: inTurn(reader.findAttemptByAgentSession),
28
+ listWatchedPullRequests: inTurn(reader.listWatchedPullRequests),
29
+ listPoolSlots: inTurn(reader.listPoolSlots),
30
+ readUnsentFacts: inTurn(reader.readUnsentFacts),
31
+ readOutboxLag: inTurn(reader.readOutboxLag),
32
+ readRunner: inTurn(reader.readRunner),
33
+ applyChange: inTurn((change: StoreChange) => applySQLiteChange(db, change)),
34
+ markFactsSent: inTurn((ids: FactId[], at: Instant) => writeFactsSent(db, ids, at)),
35
+ writeHeartbeat: inTurn((runner: RunnerId, at: Instant) => writeRunnerBeat(db, runner, at)),
36
+ syncPoolSlots: inTurn((pools: PoolCounts) => writePoolSlots(db, pools)),
37
+ markPullRequestChecked: inTurn((ref: PullRequestRef, at: Instant) =>
38
+ writePullRequestChecked(db, ref, at),
39
+ ),
40
+ };
41
+ }
42
+
43
+ function makeTurnRunner() {
44
+ let turn: Promise<unknown> = Promise.resolve();
45
+
46
+ return <Args extends unknown[], Result>(run: (...args: Args) => Promise<Result>) =>
47
+ async (...args: Args): Promise<Result> => {
48
+ const previous = turn;
49
+ const next = Promise.withResolvers<void>();
50
+
51
+ turn = next.promise;
52
+
53
+ await previous;
54
+
55
+ try {
56
+ return await run(...args);
57
+ } finally {
58
+ next.resolve();
59
+ }
60
+ };
61
+ }
62
+
63
+ async function writeFactsSent(
64
+ db: Kysely<StoreDatabase>,
65
+ ids: FactId[],
66
+ at: Instant,
67
+ ): Promise<void> {
68
+ if (ids.length === 0) {
69
+ return;
70
+ }
71
+
72
+ await db
73
+ .updateTable('outbox')
74
+ .set({ sent_at: at })
75
+ .where('fact_id', 'in', ids)
76
+ .where('sent_at', 'is', null)
77
+ .execute();
78
+ }
79
+
80
+ async function writeRunnerBeat(
81
+ db: Kysely<StoreDatabase>,
82
+ runner: RunnerId,
83
+ at: Instant,
84
+ ): Promise<void> {
85
+ await db
86
+ .insertInto('runners')
87
+ .values({ id: runner, heartbeat_at: at, paused: 0 })
88
+ .onConflict((conflict) => conflict.column('id').doUpdateSet({ heartbeat_at: at }))
89
+ .execute();
90
+ }
91
+
92
+ async function writePoolSlots(db: Kysely<StoreDatabase>, pools: PoolCounts): Promise<void> {
93
+ for (const pool of pools) {
94
+ const rows = Array.from({ length: pool.slots }, (_, index) => ({
95
+ pool: pool.name,
96
+ slot: index + 1,
97
+ holder: null,
98
+ gen: 0,
99
+ retired: 0,
100
+ }));
101
+
102
+ if (rows.length > 0) {
103
+ await db
104
+ .insertInto('pool_slots')
105
+ .values(rows)
106
+ .onConflict((conflict) => conflict.columns(['pool', 'slot']).doNothing())
107
+ .execute();
108
+ }
109
+ }
110
+
111
+ await db.updateTable('pool_slots').set({ retired: 1 }).execute();
112
+
113
+ for (const pool of pools) {
114
+ await db
115
+ .updateTable('pool_slots')
116
+ .set({ retired: 0 })
117
+ .where('pool', '=', pool.name)
118
+ .where('slot', '<=', pool.slots)
119
+ .execute();
120
+ }
121
+ }
122
+
123
+ async function writePullRequestChecked(
124
+ db: Kysely<StoreDatabase>,
125
+ ref: PullRequestRef,
126
+ at: Instant,
127
+ ): Promise<void> {
128
+ await db
129
+ .updateTable('pull_requests')
130
+ .set({ checked_at: at })
131
+ .where('repo', '=', ref.repo)
132
+ .where('number', '=', ref.number)
133
+ .execute();
134
+ }
@@ -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
+ };
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';