@fadhilp/stateql 0.4.2 → 0.5.0
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/README.md +244 -127
- package/dist/src/adapters.js +3 -3
- package/dist/src/cli.js +25 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/migrations.d.ts +2 -0
- package/dist/src/migrations.js +305 -0
- package/dist/src/response-data.d.ts +5 -5
- package/dist/src/sqlite-process.js +2 -2
- package/dist/src/stateql.d.ts +39 -37
- package/dist/src/stateql.js +54 -10
- package/dist/src/store.d.ts +21 -3
- package/dist/src/store.js +221 -209
- package/dist/src/types.d.ts +239 -1
- package/dist/src/util.d.ts +5 -2
- package/dist/src/util.js +25 -3
- package/package.json +1 -1
package/dist/src/store.js
CHANGED
|
@@ -1,24 +1,52 @@
|
|
|
1
|
-
import { mkdirSync } from "node:fs";
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, statSync } from "node:fs";
|
|
2
2
|
import { DatabaseSync } from "node:sqlite";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { platform } from "node:process";
|
|
5
|
+
import { runMigrations } from "./migrations.js";
|
|
6
|
+
import { StateQLError } from "./errors.js";
|
|
7
|
+
import { isColumns, isRows, isSqlParameters, parseJson, toJsonSafe, } from "./util.js";
|
|
5
8
|
const HISTORY_LIMIT_PER_SESSION = 10_000;
|
|
9
|
+
const DEFAULT_MAX_STATE_BYTES = 256 * 1024 * 1024;
|
|
6
10
|
export class StateStore {
|
|
7
11
|
now;
|
|
12
|
+
maxStateBytes;
|
|
8
13
|
db;
|
|
9
|
-
|
|
14
|
+
closed = false;
|
|
15
|
+
constructor(home, now, maxStateBytes = DEFAULT_MAX_STATE_BYTES) {
|
|
10
16
|
this.now = now;
|
|
17
|
+
this.maxStateBytes = maxStateBytes;
|
|
11
18
|
const path = join(home, "state.sqlite");
|
|
12
|
-
mkdirSync(home, { recursive: true });
|
|
19
|
+
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
20
|
+
if (platform !== "win32")
|
|
21
|
+
restrictMode(home, 0o700);
|
|
13
22
|
this.db = new DatabaseSync(path);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
try {
|
|
24
|
+
if (platform !== "win32")
|
|
25
|
+
restrictMode(path, 0o600);
|
|
26
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
27
|
+
if (platform !== "win32") {
|
|
28
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
29
|
+
const sidecar = `${path}${suffix}`;
|
|
30
|
+
if (existsSync(sidecar))
|
|
31
|
+
restrictMode(sidecar, 0o600);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
35
|
+
this.db.exec("PRAGMA foreign_keys = ON");
|
|
36
|
+
runMigrations(this.db, this.now);
|
|
37
|
+
this.recoverStaleCommittingTransactions();
|
|
38
|
+
this.deleteExpiredData();
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
this.db.close();
|
|
42
|
+
this.closed = true;
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
20
45
|
}
|
|
21
46
|
close() {
|
|
47
|
+
if (this.closed)
|
|
48
|
+
return;
|
|
49
|
+
this.closed = true;
|
|
22
50
|
this.db.close();
|
|
23
51
|
}
|
|
24
52
|
nextId(prefix) {
|
|
@@ -302,14 +330,24 @@ export class StateStore {
|
|
|
302
330
|
}
|
|
303
331
|
saveResult(input) {
|
|
304
332
|
const id = this.nextId("q");
|
|
305
|
-
this.db
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
333
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
334
|
+
try {
|
|
335
|
+
this.db
|
|
336
|
+
.prepare(`INSERT INTO results
|
|
337
|
+
(id, session_id, connection_id, fingerprint, sql, parameters,
|
|
338
|
+
rows_json, columns_json, row_count, state_version, state_signature,
|
|
339
|
+
state_confidence, expires_at, created_at)
|
|
340
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
341
|
+
.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());
|
|
342
|
+
this.enforceResultQuota(id);
|
|
343
|
+
const result = this.getResult(id);
|
|
344
|
+
this.db.exec("COMMIT");
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
this.db.exec("ROLLBACK");
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
313
351
|
}
|
|
314
352
|
findResult(fingerprint) {
|
|
315
353
|
return this.db
|
|
@@ -332,10 +370,10 @@ export class StateStore {
|
|
|
332
370
|
.get(idOrAlias, idOrAlias, sessionId ?? null, sessionId ?? null);
|
|
333
371
|
}
|
|
334
372
|
resultRows(result) {
|
|
335
|
-
return parseJson(result.rows_json,
|
|
373
|
+
return parseJson(result.rows_json, `result "${result.id}" rows`, isRows);
|
|
336
374
|
}
|
|
337
375
|
resultColumns(result) {
|
|
338
|
-
return parseJson(result.columns_json,
|
|
376
|
+
return parseJson(result.columns_json, `result "${result.id}" columns`, isColumns);
|
|
339
377
|
}
|
|
340
378
|
setAlias(sessionId, name, resultId) {
|
|
341
379
|
this.db
|
|
@@ -509,6 +547,46 @@ export class StateStore {
|
|
|
509
547
|
.prepare("SELECT * FROM operations WHERE transaction_id = ? ORDER BY created_at")
|
|
510
548
|
.all(transactionId);
|
|
511
549
|
}
|
|
550
|
+
validatedTransactionOperations(transactionId) {
|
|
551
|
+
const operations = this.transactionOperations(transactionId);
|
|
552
|
+
for (const operation of operations) {
|
|
553
|
+
parseJson(operation.parameters, `operation "${operation.id}" parameters`, isSqlParameters);
|
|
554
|
+
}
|
|
555
|
+
return operations;
|
|
556
|
+
}
|
|
557
|
+
claimTransactionForCommit(transactionId, sessionId, actorId, expectedOperations) {
|
|
558
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
559
|
+
try {
|
|
560
|
+
const current = this.transactionOperations(transactionId);
|
|
561
|
+
const unchanged = current.length === expectedOperations.length &&
|
|
562
|
+
current.every((operation, index) => {
|
|
563
|
+
const expected = expectedOperations[index];
|
|
564
|
+
return expected &&
|
|
565
|
+
operation.id === expected.id &&
|
|
566
|
+
operation.connection_id === expected.connection_id &&
|
|
567
|
+
operation.sql === expected.sql &&
|
|
568
|
+
operation.parameters === expected.parameters &&
|
|
569
|
+
operation.status === expected.status;
|
|
570
|
+
});
|
|
571
|
+
if (!unchanged) {
|
|
572
|
+
this.db.exec("COMMIT");
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
const result = this.db.prepare(`UPDATE transactions SET state = 'committing', ended_at = ?
|
|
576
|
+
WHERE id = ? AND session_id = ? AND owner_actor_id = ?
|
|
577
|
+
AND state = 'active' AND EXISTS (
|
|
578
|
+
SELECT 1 FROM sessions
|
|
579
|
+
WHERE sessions.id = transactions.session_id
|
|
580
|
+
AND sessions.active_transaction_id = transactions.id
|
|
581
|
+
)`).run(this.now().toISOString(), transactionId, sessionId, actorId);
|
|
582
|
+
this.db.exec("COMMIT");
|
|
583
|
+
return Number(result.changes) === 1;
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
this.db.exec("ROLLBACK");
|
|
587
|
+
throw error;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
512
590
|
markTransactionCommitting(transactionId, sessionId, actorId) {
|
|
513
591
|
this.db.exec("BEGIN IMMEDIATE");
|
|
514
592
|
try {
|
|
@@ -758,6 +836,126 @@ export class StateStore {
|
|
|
758
836
|
LIMIT ?`)
|
|
759
837
|
.all(sessionId, limit);
|
|
760
838
|
}
|
|
839
|
+
diagnostics(sessionId) {
|
|
840
|
+
const issues = [];
|
|
841
|
+
const integrity = this.db.prepare("PRAGMA integrity_check").all();
|
|
842
|
+
if (integrity.some((row) => row.integrity_check !== "ok")) {
|
|
843
|
+
issues.push({ code: "SQLITE_INTEGRITY" });
|
|
844
|
+
}
|
|
845
|
+
if (this.db.prepare("PRAGMA foreign_key_check").all().length) {
|
|
846
|
+
issues.push({ code: "FOREIGN_KEY_INTEGRITY" });
|
|
847
|
+
}
|
|
848
|
+
const results = this.db.prepare("SELECT * FROM results WHERE session_id = ?").all(sessionId);
|
|
849
|
+
for (const result of results) {
|
|
850
|
+
try {
|
|
851
|
+
const rows = this.resultRows(result);
|
|
852
|
+
this.resultColumns(result);
|
|
853
|
+
parseJson(result.parameters, `result "${result.id}" parameters`, isSqlParameters);
|
|
854
|
+
if (rows.length !== result.row_count)
|
|
855
|
+
throw new Error("row count");
|
|
856
|
+
}
|
|
857
|
+
catch {
|
|
858
|
+
issues.push({ code: "CORRUPTED_RESULT", record: result.id });
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
for (const table of ["operations", "plans"]) {
|
|
862
|
+
const records = this.db.prepare(`SELECT id, parameters FROM ${table} WHERE session_id = ?`).all(sessionId);
|
|
863
|
+
for (const record of records) {
|
|
864
|
+
try {
|
|
865
|
+
parseJson(record.parameters, `${table.slice(0, -1)} "${record.id}" parameters`, isSqlParameters);
|
|
866
|
+
}
|
|
867
|
+
catch {
|
|
868
|
+
issues.push({
|
|
869
|
+
code: table === "plans" ? "CORRUPTED_PLAN" : "CORRUPTED_OPERATION",
|
|
870
|
+
record: record.id,
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
const storage = this.db.prepare(`SELECT
|
|
876
|
+
COUNT(*) AS results,
|
|
877
|
+
COALESCE(SUM(length(CAST(sql AS BLOB)) + length(CAST(parameters AS BLOB)) +
|
|
878
|
+
length(CAST(rows_json AS BLOB)) + length(CAST(columns_json AS BLOB))), 0)
|
|
879
|
+
AS result_bytes,
|
|
880
|
+
(SELECT COUNT(*) FROM history WHERE session_id = ?) AS history
|
|
881
|
+
FROM results WHERE session_id = ?`).get(sessionId, sessionId);
|
|
882
|
+
return {
|
|
883
|
+
integrity: issues.length ? "issues" : "ok",
|
|
884
|
+
issues,
|
|
885
|
+
migrations: this.db.prepare("SELECT name FROM schema_migrations ORDER BY rowid").all().map((row) => row.name),
|
|
886
|
+
storage,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
purge(sessionId, scope) {
|
|
890
|
+
const before = this.db.prepare(`SELECT
|
|
891
|
+
(SELECT COUNT(*) FROM results WHERE session_id = ?) +
|
|
892
|
+
(SELECT COUNT(*) FROM plans WHERE session_id = ?) +
|
|
893
|
+
(SELECT COUNT(*) FROM operations WHERE session_id = ?) +
|
|
894
|
+
(SELECT COUNT(*) FROM transactions WHERE session_id = ?) +
|
|
895
|
+
(SELECT COUNT(*) FROM history WHERE session_id = ?) AS count`).get(sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
896
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
897
|
+
try {
|
|
898
|
+
if (scope === "expired") {
|
|
899
|
+
this.db.prepare(`DELETE FROM aliases WHERE session_id = ? AND result_id IN (
|
|
900
|
+
SELECT id FROM results WHERE session_id = ? AND expires_at <= ?
|
|
901
|
+
)`).run(sessionId, sessionId, this.now().toISOString());
|
|
902
|
+
this.db.prepare("DELETE FROM results WHERE session_id = ? AND expires_at <= ?").run(sessionId, this.now().toISOString());
|
|
903
|
+
this.db.prepare(`DELETE FROM plans WHERE session_id = ? AND expires_at <= ?
|
|
904
|
+
AND claim_token IS NULL`).run(sessionId, this.now().toISOString());
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
if (scope === "results" || scope === "all") {
|
|
908
|
+
this.db.prepare("DELETE FROM aliases WHERE session_id = ?").run(sessionId);
|
|
909
|
+
this.db.prepare("DELETE FROM results WHERE session_id = ?").run(sessionId);
|
|
910
|
+
}
|
|
911
|
+
if (scope === "history" || scope === "all") {
|
|
912
|
+
this.db.prepare("DELETE FROM history WHERE session_id = ?").run(sessionId);
|
|
913
|
+
}
|
|
914
|
+
if (scope === "all") {
|
|
915
|
+
this.db.prepare("DELETE FROM plans WHERE session_id = ?").run(sessionId);
|
|
916
|
+
this.db.prepare("DELETE FROM operations WHERE session_id = ?").run(sessionId);
|
|
917
|
+
this.db.prepare("DELETE FROM transactions WHERE session_id = ?").run(sessionId);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
this.db.exec("COMMIT");
|
|
921
|
+
}
|
|
922
|
+
catch (error) {
|
|
923
|
+
this.db.exec("ROLLBACK");
|
|
924
|
+
throw error;
|
|
925
|
+
}
|
|
926
|
+
const after = this.db.prepare(`SELECT
|
|
927
|
+
(SELECT COUNT(*) FROM results WHERE session_id = ?) +
|
|
928
|
+
(SELECT COUNT(*) FROM plans WHERE session_id = ?) +
|
|
929
|
+
(SELECT COUNT(*) FROM operations WHERE session_id = ?) +
|
|
930
|
+
(SELECT COUNT(*) FROM transactions WHERE session_id = ?) +
|
|
931
|
+
(SELECT COUNT(*) FROM history WHERE session_id = ?) AS count`).get(sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
932
|
+
return before.count - after.count;
|
|
933
|
+
}
|
|
934
|
+
enforceResultQuota(protectedId) {
|
|
935
|
+
this.db.prepare(`DELETE FROM aliases WHERE result_id IN (
|
|
936
|
+
SELECT id FROM results WHERE expires_at <= ?
|
|
937
|
+
)`).run(this.now().toISOString());
|
|
938
|
+
this.db.prepare("DELETE FROM results WHERE expires_at <= ?")
|
|
939
|
+
.run(this.now().toISOString());
|
|
940
|
+
while (this.resultBytes() > this.maxStateBytes) {
|
|
941
|
+
const candidate = this.db.prepare(`SELECT id FROM results
|
|
942
|
+
WHERE id <> ? AND NOT EXISTS (
|
|
943
|
+
SELECT 1 FROM aliases WHERE aliases.result_id = results.id
|
|
944
|
+
)
|
|
945
|
+
ORDER BY created_at, rowid LIMIT 1`).get(protectedId);
|
|
946
|
+
if (!candidate) {
|
|
947
|
+
throw new StateQLError("STATE_QUOTA_EXCEEDED", `Stored results exceed the ${this.maxStateBytes}-byte state quota.`, { suggestedAction: "Purge results or increase maxStateBytes." });
|
|
948
|
+
}
|
|
949
|
+
this.db.prepare("DELETE FROM results WHERE id = ?").run(candidate.id);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
resultBytes() {
|
|
953
|
+
const row = this.db.prepare(`SELECT COALESCE(SUM(
|
|
954
|
+
length(CAST(sql AS BLOB)) + length(CAST(parameters AS BLOB)) +
|
|
955
|
+
length(CAST(rows_json AS BLOB)) + length(CAST(columns_json AS BLOB))
|
|
956
|
+
), 0) AS bytes FROM results`).get();
|
|
957
|
+
return row.bytes;
|
|
958
|
+
}
|
|
761
959
|
deleteExpiredData() {
|
|
762
960
|
const timestamp = this.now().toISOString();
|
|
763
961
|
this.db.exec("BEGIN IMMEDIATE");
|
|
@@ -811,193 +1009,7 @@ export class StateStore {
|
|
|
811
1009
|
throw error;
|
|
812
1010
|
}
|
|
813
1011
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
this.db.exec(`
|
|
818
|
-
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
819
|
-
name TEXT PRIMARY KEY,
|
|
820
|
-
applied_at TEXT NOT NULL
|
|
821
|
-
);
|
|
822
|
-
`);
|
|
823
|
-
const actorMigrationApplied = Boolean(this.db
|
|
824
|
-
.prepare("SELECT 1 FROM schema_migrations WHERE name = 'shared_session_actors_v1'")
|
|
825
|
-
.get());
|
|
826
|
-
this.db.exec(`
|
|
827
|
-
CREATE TABLE IF NOT EXISTS counters (
|
|
828
|
-
prefix TEXT PRIMARY KEY,
|
|
829
|
-
value INTEGER NOT NULL
|
|
830
|
-
);
|
|
831
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
832
|
-
id TEXT PRIMARY KEY,
|
|
833
|
-
name TEXT NOT NULL UNIQUE,
|
|
834
|
-
status TEXT NOT NULL,
|
|
835
|
-
active_connection_id TEXT,
|
|
836
|
-
active_transaction_id TEXT,
|
|
837
|
-
created_at TEXT NOT NULL,
|
|
838
|
-
updated_at TEXT NOT NULL
|
|
839
|
-
);
|
|
840
|
-
CREATE TABLE IF NOT EXISTS session_members (
|
|
841
|
-
session_id TEXT NOT NULL,
|
|
842
|
-
actor_id TEXT NOT NULL UNIQUE,
|
|
843
|
-
attached_at TEXT NOT NULL,
|
|
844
|
-
PRIMARY KEY(session_id, actor_id),
|
|
845
|
-
FOREIGN KEY(session_id) REFERENCES sessions(id)
|
|
846
|
-
);
|
|
847
|
-
CREATE TABLE IF NOT EXISTS profiles (
|
|
848
|
-
name TEXT PRIMARY KEY,
|
|
849
|
-
target TEXT,
|
|
850
|
-
secret_env TEXT,
|
|
851
|
-
read_only INTEGER NOT NULL,
|
|
852
|
-
created_at TEXT NOT NULL,
|
|
853
|
-
updated_at TEXT NOT NULL,
|
|
854
|
-
CHECK(target IS NOT NULL OR secret_env IS NOT NULL)
|
|
855
|
-
);
|
|
856
|
-
CREATE TABLE IF NOT EXISTS connections (
|
|
857
|
-
id TEXT PRIMARY KEY,
|
|
858
|
-
session_id TEXT NOT NULL,
|
|
859
|
-
name TEXT NOT NULL,
|
|
860
|
-
driver TEXT NOT NULL,
|
|
861
|
-
database_name TEXT NOT NULL,
|
|
862
|
-
source TEXT NOT NULL,
|
|
863
|
-
secret_env TEXT,
|
|
864
|
-
read_only INTEGER NOT NULL,
|
|
865
|
-
version INTEGER NOT NULL,
|
|
866
|
-
created_at TEXT NOT NULL,
|
|
867
|
-
FOREIGN KEY(session_id) REFERENCES sessions(id)
|
|
868
|
-
);
|
|
869
|
-
CREATE TABLE IF NOT EXISTS results (
|
|
870
|
-
id TEXT PRIMARY KEY,
|
|
871
|
-
session_id TEXT NOT NULL,
|
|
872
|
-
connection_id TEXT NOT NULL,
|
|
873
|
-
fingerprint TEXT NOT NULL,
|
|
874
|
-
sql TEXT NOT NULL,
|
|
875
|
-
parameters TEXT NOT NULL,
|
|
876
|
-
rows_json TEXT NOT NULL,
|
|
877
|
-
columns_json TEXT NOT NULL,
|
|
878
|
-
row_count INTEGER NOT NULL,
|
|
879
|
-
state_version TEXT NOT NULL,
|
|
880
|
-
state_signature TEXT NOT NULL,
|
|
881
|
-
state_confidence TEXT NOT NULL,
|
|
882
|
-
expires_at TEXT NOT NULL,
|
|
883
|
-
created_at TEXT NOT NULL
|
|
884
|
-
);
|
|
885
|
-
CREATE INDEX IF NOT EXISTS results_fingerprint
|
|
886
|
-
ON results(fingerprint, created_at);
|
|
887
|
-
CREATE TABLE IF NOT EXISTS aliases (
|
|
888
|
-
session_id TEXT NOT NULL,
|
|
889
|
-
name TEXT NOT NULL,
|
|
890
|
-
result_id TEXT NOT NULL,
|
|
891
|
-
PRIMARY KEY(session_id, name),
|
|
892
|
-
FOREIGN KEY(result_id) REFERENCES results(id)
|
|
893
|
-
);
|
|
894
|
-
CREATE TABLE IF NOT EXISTS operations (
|
|
895
|
-
id TEXT PRIMARY KEY,
|
|
896
|
-
session_id TEXT NOT NULL,
|
|
897
|
-
actor_id TEXT NOT NULL,
|
|
898
|
-
connection_id TEXT NOT NULL,
|
|
899
|
-
fingerprint TEXT NOT NULL,
|
|
900
|
-
sql TEXT NOT NULL,
|
|
901
|
-
parameters TEXT NOT NULL,
|
|
902
|
-
statement_type TEXT NOT NULL,
|
|
903
|
-
affected_rows INTEGER,
|
|
904
|
-
status TEXT NOT NULL,
|
|
905
|
-
transaction_id TEXT,
|
|
906
|
-
replay_of TEXT,
|
|
907
|
-
idempotency_key TEXT,
|
|
908
|
-
state_version_before TEXT NOT NULL,
|
|
909
|
-
state_version_after TEXT,
|
|
910
|
-
created_at TEXT NOT NULL
|
|
911
|
-
);
|
|
912
|
-
CREATE INDEX IF NOT EXISTS operations_fingerprint
|
|
913
|
-
ON operations(connection_id, fingerprint, status);
|
|
914
|
-
CREATE UNIQUE INDEX IF NOT EXISTS operations_idempotency
|
|
915
|
-
ON operations(connection_id, idempotency_key)
|
|
916
|
-
WHERE idempotency_key IS NOT NULL AND status IN ('committed', 'pending');
|
|
917
|
-
CREATE TABLE IF NOT EXISTS transactions (
|
|
918
|
-
id TEXT PRIMARY KEY,
|
|
919
|
-
session_id TEXT NOT NULL,
|
|
920
|
-
owner_actor_id TEXT NOT NULL,
|
|
921
|
-
connection_id TEXT NOT NULL,
|
|
922
|
-
state TEXT NOT NULL,
|
|
923
|
-
isolation_level TEXT NOT NULL,
|
|
924
|
-
start_version TEXT NOT NULL,
|
|
925
|
-
created_at TEXT NOT NULL,
|
|
926
|
-
ended_at TEXT
|
|
927
|
-
);
|
|
928
|
-
CREATE TABLE IF NOT EXISTS plans (
|
|
929
|
-
id TEXT PRIMARY KEY,
|
|
930
|
-
session_id TEXT NOT NULL,
|
|
931
|
-
owner_actor_id TEXT NOT NULL,
|
|
932
|
-
connection_id TEXT NOT NULL,
|
|
933
|
-
sql TEXT NOT NULL,
|
|
934
|
-
parameters TEXT NOT NULL,
|
|
935
|
-
statement_type TEXT NOT NULL,
|
|
936
|
-
state_version TEXT NOT NULL,
|
|
937
|
-
state_signature TEXT NOT NULL,
|
|
938
|
-
destructive INTEGER NOT NULL,
|
|
939
|
-
allow_unbounded INTEGER NOT NULL,
|
|
940
|
-
allow_destructive INTEGER NOT NULL,
|
|
941
|
-
expires_at TEXT NOT NULL,
|
|
942
|
-
applied_operation_id TEXT,
|
|
943
|
-
claim_token TEXT,
|
|
944
|
-
created_at TEXT NOT NULL
|
|
945
|
-
);
|
|
946
|
-
CREATE TABLE IF NOT EXISTS history (
|
|
947
|
-
id TEXT PRIMARY KEY,
|
|
948
|
-
timestamp TEXT NOT NULL,
|
|
949
|
-
session_id TEXT NOT NULL,
|
|
950
|
-
actor_id TEXT NOT NULL,
|
|
951
|
-
command TEXT NOT NULL,
|
|
952
|
-
handle TEXT,
|
|
953
|
-
executed INTEGER NOT NULL,
|
|
954
|
-
cached INTEGER NOT NULL,
|
|
955
|
-
success INTEGER NOT NULL,
|
|
956
|
-
error_code TEXT
|
|
957
|
-
);
|
|
958
|
-
CREATE INDEX IF NOT EXISTS history_session
|
|
959
|
-
ON history(session_id);
|
|
960
|
-
`);
|
|
961
|
-
this.addColumn("operations", "actor_id", "TEXT");
|
|
962
|
-
this.addColumn("transactions", "owner_actor_id", "TEXT");
|
|
963
|
-
this.addColumn("plans", "owner_actor_id", "TEXT");
|
|
964
|
-
this.addColumn("plans", "claim_token", "TEXT");
|
|
965
|
-
this.addColumn("history", "actor_id", "TEXT");
|
|
966
|
-
if (!actorMigrationApplied) {
|
|
967
|
-
this.db.exec(`
|
|
968
|
-
INSERT OR IGNORE INTO session_members(session_id, actor_id, attached_at)
|
|
969
|
-
SELECT id, name, created_at FROM sessions;
|
|
970
|
-
`);
|
|
971
|
-
}
|
|
972
|
-
this.db.exec(`
|
|
973
|
-
UPDATE operations SET actor_id = (
|
|
974
|
-
SELECT name FROM sessions WHERE sessions.id = operations.session_id
|
|
975
|
-
) WHERE actor_id IS NULL;
|
|
976
|
-
UPDATE transactions SET owner_actor_id = (
|
|
977
|
-
SELECT name FROM sessions WHERE sessions.id = transactions.session_id
|
|
978
|
-
) WHERE owner_actor_id IS NULL;
|
|
979
|
-
UPDATE plans SET owner_actor_id = (
|
|
980
|
-
SELECT name FROM sessions WHERE sessions.id = plans.session_id
|
|
981
|
-
) WHERE owner_actor_id IS NULL;
|
|
982
|
-
UPDATE history SET actor_id = (
|
|
983
|
-
SELECT name FROM sessions WHERE sessions.id = history.session_id
|
|
984
|
-
) WHERE actor_id IS NULL;
|
|
985
|
-
`);
|
|
986
|
-
this.db
|
|
987
|
-
.prepare(`INSERT OR IGNORE INTO schema_migrations(name, applied_at)
|
|
988
|
-
VALUES ('shared_session_actors_v1', ?)`)
|
|
989
|
-
.run(this.now().toISOString());
|
|
990
|
-
this.db.exec("COMMIT");
|
|
991
|
-
}
|
|
992
|
-
catch (error) {
|
|
993
|
-
this.db.exec("ROLLBACK");
|
|
994
|
-
throw error;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
addColumn(table, column, definition) {
|
|
998
|
-
const columns = this.db.prepare(`PRAGMA table_info(${table})`).all();
|
|
999
|
-
if (!columns.some((candidate) => candidate.name === column)) {
|
|
1000
|
-
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1012
|
+
}
|
|
1013
|
+
function restrictMode(path, allowed) {
|
|
1014
|
+
chmodSync(path, statSync(path).mode & allowed);
|
|
1003
1015
|
}
|