@fadhilp/stateql 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.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/src/adapters.d.ts +23 -0
- package/dist/src/adapters.js +346 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +516 -0
- package/dist/src/errors.d.ts +12 -0
- package/dist/src/errors.js +45 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +2 -0
- package/dist/src/sql.d.ts +13 -0
- package/dist/src/sql.js +91 -0
- package/dist/src/stateql.d.ts +55 -0
- package/dist/src/stateql.js +1655 -0
- package/dist/src/store.d.ts +245 -0
- package/dist/src/store.js +622 -0
- package/dist/src/types.d.ts +112 -0
- package/dist/src/types.js +1 -0
- package/dist/src/util.d.ts +8 -0
- package/dist/src/util.js +75 -0
- package/package.json +48 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { parseJson, toJsonSafe } from "./util.js";
|
|
5
|
+
const HISTORY_LIMIT_PER_SESSION = 10_000;
|
|
6
|
+
export class StateStore {
|
|
7
|
+
now;
|
|
8
|
+
db;
|
|
9
|
+
constructor(home, now) {
|
|
10
|
+
this.now = now;
|
|
11
|
+
const path = join(home, "state.sqlite");
|
|
12
|
+
mkdirSync(home, { recursive: true });
|
|
13
|
+
this.db = new DatabaseSync(path);
|
|
14
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
15
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
16
|
+
this.db.exec("PRAGMA foreign_keys = ON");
|
|
17
|
+
this.migrate();
|
|
18
|
+
this.recoverStaleCommittingTransactions();
|
|
19
|
+
}
|
|
20
|
+
close() {
|
|
21
|
+
this.db.close();
|
|
22
|
+
}
|
|
23
|
+
nextId(prefix) {
|
|
24
|
+
this.db
|
|
25
|
+
.prepare("INSERT OR IGNORE INTO counters(prefix, value) VALUES (?, 0)")
|
|
26
|
+
.run(prefix);
|
|
27
|
+
const row = this.db
|
|
28
|
+
.prepare("UPDATE counters SET value = value + 1 WHERE prefix = ? RETURNING value")
|
|
29
|
+
.get(prefix);
|
|
30
|
+
return `${prefix}_${row.value}`;
|
|
31
|
+
}
|
|
32
|
+
ensureSession(name = "default") {
|
|
33
|
+
const existing = this.getSession(name);
|
|
34
|
+
if (existing)
|
|
35
|
+
return existing;
|
|
36
|
+
return this.createSession(name);
|
|
37
|
+
}
|
|
38
|
+
createSession(name) {
|
|
39
|
+
const timestamp = this.now().toISOString();
|
|
40
|
+
const id = this.nextId("s");
|
|
41
|
+
this.db
|
|
42
|
+
.prepare(`INSERT INTO sessions
|
|
43
|
+
(id, name, status, created_at, updated_at)
|
|
44
|
+
VALUES (?, ?, 'active', ?, ?)`)
|
|
45
|
+
.run(id, name, timestamp, timestamp);
|
|
46
|
+
return this.getSession(id);
|
|
47
|
+
}
|
|
48
|
+
getSession(idOrName) {
|
|
49
|
+
return this.db
|
|
50
|
+
.prepare(`SELECT * FROM sessions
|
|
51
|
+
WHERE (id = ? OR name = ?) AND status = 'active'
|
|
52
|
+
LIMIT 1`)
|
|
53
|
+
.get(idOrName, idOrName);
|
|
54
|
+
}
|
|
55
|
+
listSessions() {
|
|
56
|
+
return this.db
|
|
57
|
+
.prepare("SELECT * FROM sessions ORDER BY created_at")
|
|
58
|
+
.all();
|
|
59
|
+
}
|
|
60
|
+
closeSession(sessionId) {
|
|
61
|
+
const timestamp = this.now().toISOString();
|
|
62
|
+
this.db
|
|
63
|
+
.prepare(`UPDATE sessions
|
|
64
|
+
SET status = 'closed', active_transaction_id = NULL, updated_at = ?
|
|
65
|
+
WHERE id = ?`)
|
|
66
|
+
.run(timestamp, sessionId);
|
|
67
|
+
}
|
|
68
|
+
addProfile(input) {
|
|
69
|
+
const timestamp = this.now().toISOString();
|
|
70
|
+
this.db
|
|
71
|
+
.prepare(`INSERT INTO profiles
|
|
72
|
+
(name, target, secret_env, read_only, created_at, updated_at)
|
|
73
|
+
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
74
|
+
.run(input.name, input.target ?? null, input.secretEnv ?? null, input.readOnly ? 1 : 0, timestamp, timestamp);
|
|
75
|
+
return this.getProfile(input.name);
|
|
76
|
+
}
|
|
77
|
+
getProfile(name) {
|
|
78
|
+
return this.db
|
|
79
|
+
.prepare("SELECT * FROM profiles WHERE name = ?")
|
|
80
|
+
.get(name);
|
|
81
|
+
}
|
|
82
|
+
listProfiles() {
|
|
83
|
+
return this.db
|
|
84
|
+
.prepare("SELECT * FROM profiles ORDER BY name")
|
|
85
|
+
.all();
|
|
86
|
+
}
|
|
87
|
+
removeProfile(name) {
|
|
88
|
+
const result = this.db
|
|
89
|
+
.prepare("DELETE FROM profiles WHERE name = ?")
|
|
90
|
+
.run(name);
|
|
91
|
+
return Number(result.changes) > 0;
|
|
92
|
+
}
|
|
93
|
+
addConnection(input) {
|
|
94
|
+
const id = this.nextId("conn");
|
|
95
|
+
this.db
|
|
96
|
+
.prepare(`INSERT INTO connections
|
|
97
|
+
(id, session_id, name, driver, database_name, source, secret_env,
|
|
98
|
+
read_only, version, created_at)
|
|
99
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`)
|
|
100
|
+
.run(id, input.sessionId, input.name, input.driver, input.databaseName, input.source, input.secretEnv ?? null, input.readOnly ? 1 : 0, this.now().toISOString());
|
|
101
|
+
this.db
|
|
102
|
+
.prepare(`UPDATE sessions
|
|
103
|
+
SET active_connection_id = ?, updated_at = ?
|
|
104
|
+
WHERE id = ?`)
|
|
105
|
+
.run(id, this.now().toISOString(), input.sessionId);
|
|
106
|
+
return this.getConnection(id);
|
|
107
|
+
}
|
|
108
|
+
getConnection(id) {
|
|
109
|
+
return this.db
|
|
110
|
+
.prepare("SELECT * FROM connections WHERE id = ?")
|
|
111
|
+
.get(id);
|
|
112
|
+
}
|
|
113
|
+
activeConnection(session) {
|
|
114
|
+
if (!session.active_connection_id)
|
|
115
|
+
return undefined;
|
|
116
|
+
return this.getConnection(session.active_connection_id);
|
|
117
|
+
}
|
|
118
|
+
disconnect(sessionId) {
|
|
119
|
+
this.db
|
|
120
|
+
.prepare(`UPDATE sessions
|
|
121
|
+
SET active_connection_id = NULL, active_transaction_id = NULL,
|
|
122
|
+
updated_at = ?
|
|
123
|
+
WHERE id = ?`)
|
|
124
|
+
.run(this.now().toISOString(), sessionId);
|
|
125
|
+
}
|
|
126
|
+
bumpVersion(connectionId) {
|
|
127
|
+
const row = this.db
|
|
128
|
+
.prepare(`UPDATE connections
|
|
129
|
+
SET version = version + 1
|
|
130
|
+
WHERE id = ?
|
|
131
|
+
RETURNING version`)
|
|
132
|
+
.get(connectionId);
|
|
133
|
+
return `sv_${row.version}`;
|
|
134
|
+
}
|
|
135
|
+
saveResult(input) {
|
|
136
|
+
const id = this.nextId("q");
|
|
137
|
+
this.db
|
|
138
|
+
.prepare(`INSERT INTO results
|
|
139
|
+
(id, session_id, connection_id, fingerprint, sql, parameters,
|
|
140
|
+
rows_json, columns_json, row_count, state_version, state_signature,
|
|
141
|
+
state_confidence, expires_at, created_at)
|
|
142
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
143
|
+
.run(id, input.sessionId, input.connectionId, input.fingerprint, input.sql, JSON.stringify(toJsonSafe(input.parameters)), JSON.stringify(toJsonSafe(input.rows)), JSON.stringify(input.columns), input.rows.length, input.stateVersion, input.stateSignature, input.stateConfidence, input.expiresAt, this.now().toISOString());
|
|
144
|
+
return this.getResult(id);
|
|
145
|
+
}
|
|
146
|
+
findResult(fingerprint) {
|
|
147
|
+
return this.db
|
|
148
|
+
.prepare(`SELECT * FROM results
|
|
149
|
+
WHERE fingerprint = ?
|
|
150
|
+
ORDER BY created_at DESC
|
|
151
|
+
LIMIT 1`)
|
|
152
|
+
.get(fingerprint);
|
|
153
|
+
}
|
|
154
|
+
getResult(idOrAlias, sessionId) {
|
|
155
|
+
return this.db
|
|
156
|
+
.prepare(`SELECT results.*
|
|
157
|
+
FROM results
|
|
158
|
+
LEFT JOIN aliases
|
|
159
|
+
ON aliases.result_id = results.id
|
|
160
|
+
AND aliases.session_id = results.session_id
|
|
161
|
+
WHERE (results.id = ? OR aliases.name = ?)
|
|
162
|
+
AND (? IS NULL OR results.session_id = ?)
|
|
163
|
+
LIMIT 1`)
|
|
164
|
+
.get(idOrAlias, idOrAlias, sessionId ?? null, sessionId ?? null);
|
|
165
|
+
}
|
|
166
|
+
resultRows(result) {
|
|
167
|
+
return parseJson(result.rows_json, []);
|
|
168
|
+
}
|
|
169
|
+
resultColumns(result) {
|
|
170
|
+
return parseJson(result.columns_json, []);
|
|
171
|
+
}
|
|
172
|
+
setAlias(sessionId, name, resultId) {
|
|
173
|
+
this.db
|
|
174
|
+
.prepare(`INSERT INTO aliases(session_id, name, result_id)
|
|
175
|
+
VALUES (?, ?, ?)
|
|
176
|
+
ON CONFLICT(session_id, name) DO UPDATE SET result_id = excluded.result_id`)
|
|
177
|
+
.run(sessionId, name, resultId);
|
|
178
|
+
}
|
|
179
|
+
saveOperation(input) {
|
|
180
|
+
const id = this.nextId("op");
|
|
181
|
+
this.db
|
|
182
|
+
.prepare(`INSERT INTO operations
|
|
183
|
+
(id, session_id, connection_id, fingerprint, sql, parameters,
|
|
184
|
+
statement_type, affected_rows, status, transaction_id, replay_of,
|
|
185
|
+
idempotency_key, state_version_before, state_version_after, created_at)
|
|
186
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
187
|
+
.run(id, input.sessionId, input.connectionId, input.fingerprint, input.sql, JSON.stringify(toJsonSafe(input.parameters)), input.statementType, input.affectedRows ?? null, input.status, input.transactionId ?? null, input.replayOf ?? null, input.idempotencyKey ?? null, input.stateVersionBefore, input.stateVersionAfter ?? null, this.now().toISOString());
|
|
188
|
+
return this.getOperation(id);
|
|
189
|
+
}
|
|
190
|
+
getOperation(id) {
|
|
191
|
+
return this.db
|
|
192
|
+
.prepare("SELECT * FROM operations WHERE id = ?")
|
|
193
|
+
.get(id);
|
|
194
|
+
}
|
|
195
|
+
findDuplicateOperation(input) {
|
|
196
|
+
if (input.idempotencyKey) {
|
|
197
|
+
return this.db
|
|
198
|
+
.prepare(`SELECT operations.*
|
|
199
|
+
FROM operations
|
|
200
|
+
JOIN connections previous_connection
|
|
201
|
+
ON previous_connection.id = operations.connection_id
|
|
202
|
+
JOIN connections current_connection
|
|
203
|
+
ON current_connection.id = ?
|
|
204
|
+
WHERE operations.idempotency_key = ?
|
|
205
|
+
AND operations.status IN
|
|
206
|
+
('committed', 'pending', 'executing', 'outcome_unknown')
|
|
207
|
+
AND previous_connection.driver = current_connection.driver
|
|
208
|
+
AND previous_connection.source = current_connection.source
|
|
209
|
+
AND COALESCE(previous_connection.secret_env, '') =
|
|
210
|
+
COALESCE(current_connection.secret_env, '')
|
|
211
|
+
AND previous_connection.database_name = current_connection.database_name
|
|
212
|
+
ORDER BY operations.created_at DESC LIMIT 1`)
|
|
213
|
+
.get(input.connectionId, input.idempotencyKey);
|
|
214
|
+
}
|
|
215
|
+
return this.db
|
|
216
|
+
.prepare(`SELECT * FROM operations
|
|
217
|
+
WHERE fingerprint = ?
|
|
218
|
+
AND status IN
|
|
219
|
+
('committed', 'pending', 'executing', 'outcome_unknown')
|
|
220
|
+
ORDER BY created_at DESC LIMIT 1`)
|
|
221
|
+
.get(input.fingerprint);
|
|
222
|
+
}
|
|
223
|
+
reserveOperation(input) {
|
|
224
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
225
|
+
try {
|
|
226
|
+
const previous = this.findDuplicateOperation({
|
|
227
|
+
connectionId: input.connectionId,
|
|
228
|
+
fingerprint: input.fingerprint,
|
|
229
|
+
idempotencyKey: input.idempotencyKey,
|
|
230
|
+
});
|
|
231
|
+
if (previous && !input.replay) {
|
|
232
|
+
this.db.exec("COMMIT");
|
|
233
|
+
return { previous };
|
|
234
|
+
}
|
|
235
|
+
const operation = this.saveOperation({
|
|
236
|
+
sessionId: input.sessionId,
|
|
237
|
+
connectionId: input.connectionId,
|
|
238
|
+
fingerprint: input.fingerprint,
|
|
239
|
+
sql: input.sql,
|
|
240
|
+
parameters: input.parameters,
|
|
241
|
+
statementType: input.statementType,
|
|
242
|
+
status: input.status,
|
|
243
|
+
transactionId: input.transactionId,
|
|
244
|
+
replayOf: previous?.id,
|
|
245
|
+
idempotencyKey: input.replay ? undefined : input.idempotencyKey,
|
|
246
|
+
stateVersionBefore: input.stateVersionBefore,
|
|
247
|
+
});
|
|
248
|
+
this.db.exec("COMMIT");
|
|
249
|
+
return { operation, previous };
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
this.db.exec("ROLLBACK");
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
finishOperation(operationId, affectedRows, stateVersion) {
|
|
257
|
+
this.db
|
|
258
|
+
.prepare(`UPDATE operations
|
|
259
|
+
SET status = 'committed', affected_rows = ?, state_version_after = ?
|
|
260
|
+
WHERE id = ?`)
|
|
261
|
+
.run(affectedRows, stateVersion, operationId);
|
|
262
|
+
return this.getOperation(operationId);
|
|
263
|
+
}
|
|
264
|
+
failOperation(operationId) {
|
|
265
|
+
this.db
|
|
266
|
+
.prepare("UPDATE operations SET status = 'failed' WHERE id = ?")
|
|
267
|
+
.run(operationId);
|
|
268
|
+
}
|
|
269
|
+
markOperationOutcomeUnknown(operationId) {
|
|
270
|
+
this.db
|
|
271
|
+
.prepare("UPDATE operations SET status = 'outcome_unknown' WHERE id = ?")
|
|
272
|
+
.run(operationId);
|
|
273
|
+
}
|
|
274
|
+
createTransaction(input) {
|
|
275
|
+
const id = this.nextId("tx");
|
|
276
|
+
const timestamp = this.now().toISOString();
|
|
277
|
+
this.db
|
|
278
|
+
.prepare(`INSERT INTO transactions
|
|
279
|
+
(id, session_id, connection_id, state, isolation_level,
|
|
280
|
+
start_version, created_at)
|
|
281
|
+
VALUES (?, ?, ?, 'active', ?, ?, ?)`)
|
|
282
|
+
.run(id, input.sessionId, input.connectionId, input.isolation, input.startVersion, timestamp);
|
|
283
|
+
this.db
|
|
284
|
+
.prepare(`UPDATE sessions
|
|
285
|
+
SET active_transaction_id = ?, updated_at = ?
|
|
286
|
+
WHERE id = ?`)
|
|
287
|
+
.run(id, timestamp, input.sessionId);
|
|
288
|
+
return this.getTransaction(id);
|
|
289
|
+
}
|
|
290
|
+
getTransaction(id) {
|
|
291
|
+
return this.db
|
|
292
|
+
.prepare("SELECT * FROM transactions WHERE id = ?")
|
|
293
|
+
.get(id);
|
|
294
|
+
}
|
|
295
|
+
transactionOperations(transactionId) {
|
|
296
|
+
return this.db
|
|
297
|
+
.prepare("SELECT * FROM operations WHERE transaction_id = ? ORDER BY created_at")
|
|
298
|
+
.all(transactionId);
|
|
299
|
+
}
|
|
300
|
+
markTransactionCommitting(transactionId) {
|
|
301
|
+
const result = this.db
|
|
302
|
+
.prepare(`UPDATE transactions
|
|
303
|
+
SET state = 'committing', ended_at = ?
|
|
304
|
+
WHERE id = ? AND state = 'active'`)
|
|
305
|
+
.run(this.now().toISOString(), transactionId);
|
|
306
|
+
return Number(result.changes) === 1;
|
|
307
|
+
}
|
|
308
|
+
markTransactionOutcomeUnknown(transactionId, sessionId) {
|
|
309
|
+
const timestamp = this.now().toISOString();
|
|
310
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
311
|
+
try {
|
|
312
|
+
this.db
|
|
313
|
+
.prepare(`UPDATE transactions
|
|
314
|
+
SET state = 'outcome_unknown', ended_at = ?
|
|
315
|
+
WHERE id = ?`)
|
|
316
|
+
.run(timestamp, transactionId);
|
|
317
|
+
this.db
|
|
318
|
+
.prepare(`UPDATE operations SET status = 'outcome_unknown'
|
|
319
|
+
WHERE transaction_id = ? AND status = 'pending'`)
|
|
320
|
+
.run(transactionId);
|
|
321
|
+
this.db
|
|
322
|
+
.prepare(`UPDATE sessions
|
|
323
|
+
SET active_transaction_id = NULL, updated_at = ?
|
|
324
|
+
WHERE id = ?`)
|
|
325
|
+
.run(timestamp, sessionId);
|
|
326
|
+
this.db.exec("COMMIT");
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
this.db.exec("ROLLBACK");
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
finishTransaction(transactionId, sessionId, state) {
|
|
334
|
+
const timestamp = this.now().toISOString();
|
|
335
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
336
|
+
try {
|
|
337
|
+
this.db
|
|
338
|
+
.prepare("UPDATE transactions SET state = ?, ended_at = ? WHERE id = ?")
|
|
339
|
+
.run(state, timestamp, transactionId);
|
|
340
|
+
this.db
|
|
341
|
+
.prepare(`UPDATE sessions
|
|
342
|
+
SET active_transaction_id = NULL, updated_at = ?
|
|
343
|
+
WHERE id = ?`)
|
|
344
|
+
.run(timestamp, sessionId);
|
|
345
|
+
if (state === "rolled_back" || state === "failed") {
|
|
346
|
+
this.db
|
|
347
|
+
.prepare(`UPDATE operations SET status = ?
|
|
348
|
+
WHERE transaction_id = ? AND status = 'pending'`)
|
|
349
|
+
.run(state, transactionId);
|
|
350
|
+
}
|
|
351
|
+
this.db.exec("COMMIT");
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
this.db.exec("ROLLBACK");
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
commitTransactionMetadata(input) {
|
|
359
|
+
const timestamp = this.now().toISOString();
|
|
360
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
361
|
+
try {
|
|
362
|
+
let stateVersion = `sv_${(this.getConnection(input.connectionId)?.version ?? 0)}`;
|
|
363
|
+
for (const operation of input.operations) {
|
|
364
|
+
const row = this.db
|
|
365
|
+
.prepare(`UPDATE connections
|
|
366
|
+
SET version = version + 1
|
|
367
|
+
WHERE id = ?
|
|
368
|
+
RETURNING version`)
|
|
369
|
+
.get(input.connectionId);
|
|
370
|
+
stateVersion = `sv_${row.version}`;
|
|
371
|
+
this.db
|
|
372
|
+
.prepare(`UPDATE operations
|
|
373
|
+
SET status = 'committed', affected_rows = ?, state_version_after = ?
|
|
374
|
+
WHERE id = ?`)
|
|
375
|
+
.run(operation.affectedRows, stateVersion, operation.id);
|
|
376
|
+
}
|
|
377
|
+
this.db
|
|
378
|
+
.prepare(`UPDATE transactions
|
|
379
|
+
SET state = 'committed', ended_at = ?
|
|
380
|
+
WHERE id = ?`)
|
|
381
|
+
.run(timestamp, input.transactionId);
|
|
382
|
+
this.db
|
|
383
|
+
.prepare(`UPDATE sessions
|
|
384
|
+
SET active_transaction_id = NULL, updated_at = ?
|
|
385
|
+
WHERE id = ?`)
|
|
386
|
+
.run(timestamp, input.sessionId);
|
|
387
|
+
this.db.exec("COMMIT");
|
|
388
|
+
return stateVersion;
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
this.db.exec("ROLLBACK");
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
savePlan(input) {
|
|
396
|
+
const id = this.nextId("p");
|
|
397
|
+
this.db
|
|
398
|
+
.prepare(`INSERT INTO plans
|
|
399
|
+
(id, session_id, connection_id, sql, parameters, statement_type,
|
|
400
|
+
state_version, state_signature, destructive, allow_unbounded,
|
|
401
|
+
allow_destructive, expires_at, created_at)
|
|
402
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
403
|
+
.run(id, input.sessionId, input.connectionId, input.sql, JSON.stringify(toJsonSafe(input.parameters)), input.statementType, input.stateVersion, input.stateSignature, input.destructive ? 1 : 0, input.allowUnbounded ? 1 : 0, input.allowDestructive ? 1 : 0, input.expiresAt, this.now().toISOString());
|
|
404
|
+
return this.getPlan(id);
|
|
405
|
+
}
|
|
406
|
+
getPlan(id) {
|
|
407
|
+
return this.db
|
|
408
|
+
.prepare("SELECT * FROM plans WHERE id = ?")
|
|
409
|
+
.get(id);
|
|
410
|
+
}
|
|
411
|
+
markPlanApplied(planId, operationId) {
|
|
412
|
+
this.db
|
|
413
|
+
.prepare("UPDATE plans SET applied_operation_id = ? WHERE id = ?")
|
|
414
|
+
.run(operationId, planId);
|
|
415
|
+
}
|
|
416
|
+
addHistory(input) {
|
|
417
|
+
const id = input.id ?? this.nextId("cmd");
|
|
418
|
+
this.db
|
|
419
|
+
.prepare(`INSERT INTO history
|
|
420
|
+
(id, timestamp, session_id, command, handle, executed, cached,
|
|
421
|
+
success, error_code)
|
|
422
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
423
|
+
.run(id, this.now().toISOString(), input.sessionId, input.command, input.handle ?? null, input.executed ? 1 : 0, input.cached ? 1 : 0, input.success ? 1 : 0, input.errorCode ?? null);
|
|
424
|
+
this.db
|
|
425
|
+
.prepare(`DELETE FROM history
|
|
426
|
+
WHERE rowid IN (
|
|
427
|
+
SELECT rowid FROM history
|
|
428
|
+
WHERE session_id = ?
|
|
429
|
+
ORDER BY rowid DESC
|
|
430
|
+
LIMIT -1 OFFSET ?
|
|
431
|
+
)`)
|
|
432
|
+
.run(input.sessionId, HISTORY_LIMIT_PER_SESSION);
|
|
433
|
+
return this.db
|
|
434
|
+
.prepare("SELECT * FROM history WHERE id = ?")
|
|
435
|
+
.get(id);
|
|
436
|
+
}
|
|
437
|
+
history(sessionId, limit) {
|
|
438
|
+
return this.db
|
|
439
|
+
.prepare(`SELECT * FROM history
|
|
440
|
+
WHERE session_id = ?
|
|
441
|
+
ORDER BY rowid DESC
|
|
442
|
+
LIMIT ?`)
|
|
443
|
+
.all(sessionId, limit);
|
|
444
|
+
}
|
|
445
|
+
recentOperations(sessionId, limit) {
|
|
446
|
+
return this.db
|
|
447
|
+
.prepare(`SELECT * FROM operations
|
|
448
|
+
WHERE session_id = ?
|
|
449
|
+
ORDER BY created_at DESC
|
|
450
|
+
LIMIT ?`)
|
|
451
|
+
.all(sessionId, limit);
|
|
452
|
+
}
|
|
453
|
+
knownResults(sessionId, limit) {
|
|
454
|
+
return this.db
|
|
455
|
+
.prepare(`SELECT results.*, aliases.name AS alias
|
|
456
|
+
FROM results
|
|
457
|
+
LEFT JOIN aliases
|
|
458
|
+
ON aliases.result_id = results.id
|
|
459
|
+
AND aliases.session_id = results.session_id
|
|
460
|
+
WHERE results.session_id = ?
|
|
461
|
+
ORDER BY results.created_at DESC
|
|
462
|
+
LIMIT ?`)
|
|
463
|
+
.all(sessionId, limit);
|
|
464
|
+
}
|
|
465
|
+
recoverStaleCommittingTransactions() {
|
|
466
|
+
const timestamp = this.now().toISOString();
|
|
467
|
+
const cutoff = new Date(this.now().getTime() - 5 * 60_000).toISOString();
|
|
468
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
469
|
+
try {
|
|
470
|
+
this.db
|
|
471
|
+
.prepare(`UPDATE operations SET status = 'outcome_unknown'
|
|
472
|
+
WHERE status = 'pending' AND transaction_id IN (
|
|
473
|
+
SELECT id FROM transactions
|
|
474
|
+
WHERE state = 'committing' AND ended_at <= ?
|
|
475
|
+
)`)
|
|
476
|
+
.run(cutoff);
|
|
477
|
+
this.db
|
|
478
|
+
.prepare(`UPDATE sessions
|
|
479
|
+
SET active_transaction_id = NULL, updated_at = ?
|
|
480
|
+
WHERE active_transaction_id IN (
|
|
481
|
+
SELECT id FROM transactions
|
|
482
|
+
WHERE state = 'committing' AND ended_at <= ?
|
|
483
|
+
)`)
|
|
484
|
+
.run(timestamp, cutoff);
|
|
485
|
+
this.db
|
|
486
|
+
.prepare(`UPDATE transactions
|
|
487
|
+
SET state = 'outcome_unknown', ended_at = ?
|
|
488
|
+
WHERE state = 'committing' AND ended_at <= ?`)
|
|
489
|
+
.run(timestamp, cutoff);
|
|
490
|
+
this.db.exec("COMMIT");
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
this.db.exec("ROLLBACK");
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
migrate() {
|
|
498
|
+
this.db.exec(`
|
|
499
|
+
CREATE TABLE IF NOT EXISTS counters (
|
|
500
|
+
prefix TEXT PRIMARY KEY,
|
|
501
|
+
value INTEGER NOT NULL
|
|
502
|
+
);
|
|
503
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
504
|
+
id TEXT PRIMARY KEY,
|
|
505
|
+
name TEXT NOT NULL UNIQUE,
|
|
506
|
+
status TEXT NOT NULL,
|
|
507
|
+
active_connection_id TEXT,
|
|
508
|
+
active_transaction_id TEXT,
|
|
509
|
+
created_at TEXT NOT NULL,
|
|
510
|
+
updated_at TEXT NOT NULL
|
|
511
|
+
);
|
|
512
|
+
CREATE TABLE IF NOT EXISTS profiles (
|
|
513
|
+
name TEXT PRIMARY KEY,
|
|
514
|
+
target TEXT,
|
|
515
|
+
secret_env TEXT,
|
|
516
|
+
read_only INTEGER NOT NULL,
|
|
517
|
+
created_at TEXT NOT NULL,
|
|
518
|
+
updated_at TEXT NOT NULL,
|
|
519
|
+
CHECK(target IS NOT NULL OR secret_env IS NOT NULL)
|
|
520
|
+
);
|
|
521
|
+
CREATE TABLE IF NOT EXISTS connections (
|
|
522
|
+
id TEXT PRIMARY KEY,
|
|
523
|
+
session_id TEXT NOT NULL,
|
|
524
|
+
name TEXT NOT NULL,
|
|
525
|
+
driver TEXT NOT NULL,
|
|
526
|
+
database_name TEXT NOT NULL,
|
|
527
|
+
source TEXT NOT NULL,
|
|
528
|
+
secret_env TEXT,
|
|
529
|
+
read_only INTEGER NOT NULL,
|
|
530
|
+
version INTEGER NOT NULL,
|
|
531
|
+
created_at TEXT NOT NULL,
|
|
532
|
+
FOREIGN KEY(session_id) REFERENCES sessions(id)
|
|
533
|
+
);
|
|
534
|
+
CREATE TABLE IF NOT EXISTS results (
|
|
535
|
+
id TEXT PRIMARY KEY,
|
|
536
|
+
session_id TEXT NOT NULL,
|
|
537
|
+
connection_id TEXT NOT NULL,
|
|
538
|
+
fingerprint TEXT NOT NULL,
|
|
539
|
+
sql TEXT NOT NULL,
|
|
540
|
+
parameters TEXT NOT NULL,
|
|
541
|
+
rows_json TEXT NOT NULL,
|
|
542
|
+
columns_json TEXT NOT NULL,
|
|
543
|
+
row_count INTEGER NOT NULL,
|
|
544
|
+
state_version TEXT NOT NULL,
|
|
545
|
+
state_signature TEXT NOT NULL,
|
|
546
|
+
state_confidence TEXT NOT NULL,
|
|
547
|
+
expires_at TEXT NOT NULL,
|
|
548
|
+
created_at TEXT NOT NULL
|
|
549
|
+
);
|
|
550
|
+
CREATE INDEX IF NOT EXISTS results_fingerprint
|
|
551
|
+
ON results(fingerprint, created_at);
|
|
552
|
+
CREATE TABLE IF NOT EXISTS aliases (
|
|
553
|
+
session_id TEXT NOT NULL,
|
|
554
|
+
name TEXT NOT NULL,
|
|
555
|
+
result_id TEXT NOT NULL,
|
|
556
|
+
PRIMARY KEY(session_id, name),
|
|
557
|
+
FOREIGN KEY(result_id) REFERENCES results(id)
|
|
558
|
+
);
|
|
559
|
+
CREATE TABLE IF NOT EXISTS operations (
|
|
560
|
+
id TEXT PRIMARY KEY,
|
|
561
|
+
session_id TEXT NOT NULL,
|
|
562
|
+
connection_id TEXT NOT NULL,
|
|
563
|
+
fingerprint TEXT NOT NULL,
|
|
564
|
+
sql TEXT NOT NULL,
|
|
565
|
+
parameters TEXT NOT NULL,
|
|
566
|
+
statement_type TEXT NOT NULL,
|
|
567
|
+
affected_rows INTEGER,
|
|
568
|
+
status TEXT NOT NULL,
|
|
569
|
+
transaction_id TEXT,
|
|
570
|
+
replay_of TEXT,
|
|
571
|
+
idempotency_key TEXT,
|
|
572
|
+
state_version_before TEXT NOT NULL,
|
|
573
|
+
state_version_after TEXT,
|
|
574
|
+
created_at TEXT NOT NULL
|
|
575
|
+
);
|
|
576
|
+
CREATE INDEX IF NOT EXISTS operations_fingerprint
|
|
577
|
+
ON operations(connection_id, fingerprint, status);
|
|
578
|
+
CREATE UNIQUE INDEX IF NOT EXISTS operations_idempotency
|
|
579
|
+
ON operations(connection_id, idempotency_key)
|
|
580
|
+
WHERE idempotency_key IS NOT NULL AND status IN ('committed', 'pending');
|
|
581
|
+
CREATE TABLE IF NOT EXISTS transactions (
|
|
582
|
+
id TEXT PRIMARY KEY,
|
|
583
|
+
session_id TEXT NOT NULL,
|
|
584
|
+
connection_id TEXT NOT NULL,
|
|
585
|
+
state TEXT NOT NULL,
|
|
586
|
+
isolation_level TEXT NOT NULL,
|
|
587
|
+
start_version TEXT NOT NULL,
|
|
588
|
+
created_at TEXT NOT NULL,
|
|
589
|
+
ended_at TEXT
|
|
590
|
+
);
|
|
591
|
+
CREATE TABLE IF NOT EXISTS plans (
|
|
592
|
+
id TEXT PRIMARY KEY,
|
|
593
|
+
session_id TEXT NOT NULL,
|
|
594
|
+
connection_id TEXT NOT NULL,
|
|
595
|
+
sql TEXT NOT NULL,
|
|
596
|
+
parameters TEXT NOT NULL,
|
|
597
|
+
statement_type TEXT NOT NULL,
|
|
598
|
+
state_version TEXT NOT NULL,
|
|
599
|
+
state_signature TEXT NOT NULL,
|
|
600
|
+
destructive INTEGER NOT NULL,
|
|
601
|
+
allow_unbounded INTEGER NOT NULL,
|
|
602
|
+
allow_destructive INTEGER NOT NULL,
|
|
603
|
+
expires_at TEXT NOT NULL,
|
|
604
|
+
applied_operation_id TEXT,
|
|
605
|
+
created_at TEXT NOT NULL
|
|
606
|
+
);
|
|
607
|
+
CREATE TABLE IF NOT EXISTS history (
|
|
608
|
+
id TEXT PRIMARY KEY,
|
|
609
|
+
timestamp TEXT NOT NULL,
|
|
610
|
+
session_id TEXT NOT NULL,
|
|
611
|
+
command TEXT NOT NULL,
|
|
612
|
+
handle TEXT,
|
|
613
|
+
executed INTEGER NOT NULL,
|
|
614
|
+
cached INTEGER NOT NULL,
|
|
615
|
+
success INTEGER NOT NULL,
|
|
616
|
+
error_code TEXT
|
|
617
|
+
);
|
|
618
|
+
CREATE INDEX IF NOT EXISTS history_session
|
|
619
|
+
ON history(session_id);
|
|
620
|
+
`);
|
|
621
|
+
}
|
|
622
|
+
}
|