@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
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
const CORE_TABLES = [
|
|
2
|
+
"aliases",
|
|
3
|
+
"connections",
|
|
4
|
+
"counters",
|
|
5
|
+
"history",
|
|
6
|
+
"operations",
|
|
7
|
+
"plans",
|
|
8
|
+
"profiles",
|
|
9
|
+
"results",
|
|
10
|
+
"sessions",
|
|
11
|
+
"transactions",
|
|
12
|
+
];
|
|
13
|
+
const CORE_COLUMNS = {
|
|
14
|
+
counters: ["prefix", "value"],
|
|
15
|
+
sessions: ["id", "name", "status", "active_connection_id", "active_transaction_id", "created_at", "updated_at"],
|
|
16
|
+
profiles: ["name", "target", "secret_env", "read_only", "created_at", "updated_at"],
|
|
17
|
+
connections: ["id", "session_id", "name", "driver", "database_name", "source", "secret_env", "read_only", "version", "created_at"],
|
|
18
|
+
results: ["id", "session_id", "connection_id", "fingerprint", "sql", "parameters", "rows_json", "columns_json", "row_count", "state_version", "state_signature", "state_confidence", "expires_at", "created_at"],
|
|
19
|
+
aliases: ["session_id", "name", "result_id"],
|
|
20
|
+
operations: ["id", "session_id", "connection_id", "fingerprint", "sql", "parameters", "statement_type", "affected_rows", "status", "transaction_id", "replay_of", "idempotency_key", "state_version_before", "state_version_after", "created_at"],
|
|
21
|
+
transactions: ["id", "session_id", "connection_id", "state", "isolation_level", "start_version", "created_at", "ended_at"],
|
|
22
|
+
plans: ["id", "session_id", "connection_id", "sql", "parameters", "statement_type", "state_version", "state_signature", "destructive", "allow_unbounded", "allow_destructive", "expires_at", "applied_operation_id", "created_at"],
|
|
23
|
+
history: ["id", "timestamp", "session_id", "command", "handle", "executed", "cached", "success", "error_code"],
|
|
24
|
+
};
|
|
25
|
+
const MIGRATIONS = [
|
|
26
|
+
{
|
|
27
|
+
name: "initial_schema_v1",
|
|
28
|
+
apply: createInitialSchema,
|
|
29
|
+
validate(db) {
|
|
30
|
+
requireTables(db, CORE_TABLES);
|
|
31
|
+
for (const [table, columns] of Object.entries(CORE_COLUMNS)) {
|
|
32
|
+
requireColumns(db, table, columns);
|
|
33
|
+
}
|
|
34
|
+
requireIndexes(db, [
|
|
35
|
+
"history_session",
|
|
36
|
+
"operations_fingerprint",
|
|
37
|
+
"operations_idempotency",
|
|
38
|
+
"results_fingerprint",
|
|
39
|
+
]);
|
|
40
|
+
requireForeignKey(db, "connections", "session_id", "sessions", "id");
|
|
41
|
+
requireForeignKey(db, "aliases", "result_id", "results", "id");
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "shared_session_actors_v1",
|
|
46
|
+
apply: migrateSharedSessionActors,
|
|
47
|
+
validate: validateSharedSessionActors,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
export function runMigrations(db, now) {
|
|
51
|
+
db.exec(`
|
|
52
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
53
|
+
name TEXT PRIMARY KEY,
|
|
54
|
+
applied_at TEXT NOT NULL
|
|
55
|
+
)
|
|
56
|
+
`);
|
|
57
|
+
for (const migration of MIGRATIONS) {
|
|
58
|
+
db.exec("BEGIN IMMEDIATE");
|
|
59
|
+
try {
|
|
60
|
+
// Always reapply idempotently: a migration row is not proof that its
|
|
61
|
+
// schema changes survived a manually modified or partially copied store.
|
|
62
|
+
migration.apply(db);
|
|
63
|
+
migration.validate(db);
|
|
64
|
+
db.prepare(`INSERT OR IGNORE INTO schema_migrations(name, applied_at)
|
|
65
|
+
VALUES (?, ?)`).run(migration.name, now().toISOString());
|
|
66
|
+
db.exec("COMMIT");
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
db.exec("ROLLBACK");
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const violations = db.prepare("PRAGMA foreign_key_check").all();
|
|
74
|
+
if (violations.length) {
|
|
75
|
+
throw new Error("State store failed its foreign-key integrity check.");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function createInitialSchema(db) {
|
|
79
|
+
db.exec(`
|
|
80
|
+
CREATE TABLE IF NOT EXISTS counters (
|
|
81
|
+
prefix TEXT PRIMARY KEY,
|
|
82
|
+
value INTEGER NOT NULL
|
|
83
|
+
);
|
|
84
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
85
|
+
id TEXT PRIMARY KEY,
|
|
86
|
+
name TEXT NOT NULL UNIQUE,
|
|
87
|
+
status TEXT NOT NULL,
|
|
88
|
+
active_connection_id TEXT,
|
|
89
|
+
active_transaction_id TEXT,
|
|
90
|
+
created_at TEXT NOT NULL,
|
|
91
|
+
updated_at TEXT NOT NULL
|
|
92
|
+
);
|
|
93
|
+
CREATE TABLE IF NOT EXISTS session_members (
|
|
94
|
+
session_id TEXT NOT NULL,
|
|
95
|
+
actor_id TEXT NOT NULL UNIQUE,
|
|
96
|
+
attached_at TEXT NOT NULL,
|
|
97
|
+
PRIMARY KEY(session_id, actor_id),
|
|
98
|
+
FOREIGN KEY(session_id) REFERENCES sessions(id)
|
|
99
|
+
);
|
|
100
|
+
CREATE TABLE IF NOT EXISTS profiles (
|
|
101
|
+
name TEXT PRIMARY KEY,
|
|
102
|
+
target TEXT,
|
|
103
|
+
secret_env TEXT,
|
|
104
|
+
read_only INTEGER NOT NULL,
|
|
105
|
+
created_at TEXT NOT NULL,
|
|
106
|
+
updated_at TEXT NOT NULL,
|
|
107
|
+
CHECK(target IS NOT NULL OR secret_env IS NOT NULL)
|
|
108
|
+
);
|
|
109
|
+
CREATE TABLE IF NOT EXISTS connections (
|
|
110
|
+
id TEXT PRIMARY KEY,
|
|
111
|
+
session_id TEXT NOT NULL,
|
|
112
|
+
name TEXT NOT NULL,
|
|
113
|
+
driver TEXT NOT NULL,
|
|
114
|
+
database_name TEXT NOT NULL,
|
|
115
|
+
source TEXT NOT NULL,
|
|
116
|
+
secret_env TEXT,
|
|
117
|
+
read_only INTEGER NOT NULL,
|
|
118
|
+
version INTEGER NOT NULL,
|
|
119
|
+
created_at TEXT NOT NULL,
|
|
120
|
+
FOREIGN KEY(session_id) REFERENCES sessions(id)
|
|
121
|
+
);
|
|
122
|
+
CREATE TABLE IF NOT EXISTS results (
|
|
123
|
+
id TEXT PRIMARY KEY,
|
|
124
|
+
session_id TEXT NOT NULL,
|
|
125
|
+
connection_id TEXT NOT NULL,
|
|
126
|
+
fingerprint TEXT NOT NULL,
|
|
127
|
+
sql TEXT NOT NULL,
|
|
128
|
+
parameters TEXT NOT NULL,
|
|
129
|
+
rows_json TEXT NOT NULL,
|
|
130
|
+
columns_json TEXT NOT NULL,
|
|
131
|
+
row_count INTEGER NOT NULL,
|
|
132
|
+
state_version TEXT NOT NULL,
|
|
133
|
+
state_signature TEXT NOT NULL,
|
|
134
|
+
state_confidence TEXT NOT NULL,
|
|
135
|
+
expires_at TEXT NOT NULL,
|
|
136
|
+
created_at TEXT NOT NULL
|
|
137
|
+
);
|
|
138
|
+
CREATE INDEX IF NOT EXISTS results_fingerprint
|
|
139
|
+
ON results(fingerprint, created_at);
|
|
140
|
+
CREATE TABLE IF NOT EXISTS aliases (
|
|
141
|
+
session_id TEXT NOT NULL,
|
|
142
|
+
name TEXT NOT NULL,
|
|
143
|
+
result_id TEXT NOT NULL,
|
|
144
|
+
PRIMARY KEY(session_id, name),
|
|
145
|
+
FOREIGN KEY(result_id) REFERENCES results(id)
|
|
146
|
+
);
|
|
147
|
+
CREATE TABLE IF NOT EXISTS operations (
|
|
148
|
+
id TEXT PRIMARY KEY,
|
|
149
|
+
session_id TEXT NOT NULL,
|
|
150
|
+
actor_id TEXT NOT NULL,
|
|
151
|
+
connection_id TEXT NOT NULL,
|
|
152
|
+
fingerprint TEXT NOT NULL,
|
|
153
|
+
sql TEXT NOT NULL,
|
|
154
|
+
parameters TEXT NOT NULL,
|
|
155
|
+
statement_type TEXT NOT NULL,
|
|
156
|
+
affected_rows INTEGER,
|
|
157
|
+
status TEXT NOT NULL,
|
|
158
|
+
transaction_id TEXT,
|
|
159
|
+
replay_of TEXT,
|
|
160
|
+
idempotency_key TEXT,
|
|
161
|
+
state_version_before TEXT NOT NULL,
|
|
162
|
+
state_version_after TEXT,
|
|
163
|
+
created_at TEXT NOT NULL
|
|
164
|
+
);
|
|
165
|
+
CREATE INDEX IF NOT EXISTS operations_fingerprint
|
|
166
|
+
ON operations(connection_id, fingerprint, status);
|
|
167
|
+
CREATE UNIQUE INDEX IF NOT EXISTS operations_idempotency
|
|
168
|
+
ON operations(connection_id, idempotency_key)
|
|
169
|
+
WHERE idempotency_key IS NOT NULL AND status IN ('committed', 'pending');
|
|
170
|
+
CREATE TABLE IF NOT EXISTS transactions (
|
|
171
|
+
id TEXT PRIMARY KEY,
|
|
172
|
+
session_id TEXT NOT NULL,
|
|
173
|
+
owner_actor_id TEXT NOT NULL,
|
|
174
|
+
connection_id TEXT NOT NULL,
|
|
175
|
+
state TEXT NOT NULL,
|
|
176
|
+
isolation_level TEXT NOT NULL,
|
|
177
|
+
start_version TEXT NOT NULL,
|
|
178
|
+
created_at TEXT NOT NULL,
|
|
179
|
+
ended_at TEXT
|
|
180
|
+
);
|
|
181
|
+
CREATE TABLE IF NOT EXISTS plans (
|
|
182
|
+
id TEXT PRIMARY KEY,
|
|
183
|
+
session_id TEXT NOT NULL,
|
|
184
|
+
owner_actor_id TEXT NOT NULL,
|
|
185
|
+
connection_id TEXT NOT NULL,
|
|
186
|
+
sql TEXT NOT NULL,
|
|
187
|
+
parameters TEXT NOT NULL,
|
|
188
|
+
statement_type TEXT NOT NULL,
|
|
189
|
+
state_version TEXT NOT NULL,
|
|
190
|
+
state_signature TEXT NOT NULL,
|
|
191
|
+
destructive INTEGER NOT NULL,
|
|
192
|
+
allow_unbounded INTEGER NOT NULL,
|
|
193
|
+
allow_destructive INTEGER NOT NULL,
|
|
194
|
+
expires_at TEXT NOT NULL,
|
|
195
|
+
applied_operation_id TEXT,
|
|
196
|
+
claim_token TEXT,
|
|
197
|
+
created_at TEXT NOT NULL
|
|
198
|
+
);
|
|
199
|
+
CREATE TABLE IF NOT EXISTS history (
|
|
200
|
+
id TEXT PRIMARY KEY,
|
|
201
|
+
timestamp TEXT NOT NULL,
|
|
202
|
+
session_id TEXT NOT NULL,
|
|
203
|
+
actor_id TEXT NOT NULL,
|
|
204
|
+
command TEXT NOT NULL,
|
|
205
|
+
handle TEXT,
|
|
206
|
+
executed INTEGER NOT NULL,
|
|
207
|
+
cached INTEGER NOT NULL,
|
|
208
|
+
success INTEGER NOT NULL,
|
|
209
|
+
error_code TEXT
|
|
210
|
+
);
|
|
211
|
+
CREATE INDEX IF NOT EXISTS history_session ON history(session_id);
|
|
212
|
+
`);
|
|
213
|
+
}
|
|
214
|
+
function migrateSharedSessionActors(db) {
|
|
215
|
+
db.exec(`
|
|
216
|
+
CREATE TABLE IF NOT EXISTS session_members (
|
|
217
|
+
session_id TEXT NOT NULL,
|
|
218
|
+
actor_id TEXT NOT NULL UNIQUE,
|
|
219
|
+
attached_at TEXT NOT NULL,
|
|
220
|
+
PRIMARY KEY(session_id, actor_id),
|
|
221
|
+
FOREIGN KEY(session_id) REFERENCES sessions(id)
|
|
222
|
+
)
|
|
223
|
+
`);
|
|
224
|
+
addColumn(db, "operations", "actor_id", "TEXT");
|
|
225
|
+
addColumn(db, "transactions", "owner_actor_id", "TEXT");
|
|
226
|
+
addColumn(db, "plans", "owner_actor_id", "TEXT");
|
|
227
|
+
addColumn(db, "plans", "claim_token", "TEXT");
|
|
228
|
+
addColumn(db, "history", "actor_id", "TEXT");
|
|
229
|
+
db.exec(`
|
|
230
|
+
INSERT OR IGNORE INTO session_members(session_id, actor_id, attached_at)
|
|
231
|
+
SELECT id, name, created_at FROM sessions;
|
|
232
|
+
UPDATE operations SET actor_id = (
|
|
233
|
+
SELECT name FROM sessions WHERE sessions.id = operations.session_id
|
|
234
|
+
) WHERE actor_id IS NULL;
|
|
235
|
+
UPDATE transactions SET owner_actor_id = (
|
|
236
|
+
SELECT name FROM sessions WHERE sessions.id = transactions.session_id
|
|
237
|
+
) WHERE owner_actor_id IS NULL;
|
|
238
|
+
UPDATE plans SET owner_actor_id = (
|
|
239
|
+
SELECT name FROM sessions WHERE sessions.id = plans.session_id
|
|
240
|
+
) WHERE owner_actor_id IS NULL;
|
|
241
|
+
UPDATE history SET actor_id = (
|
|
242
|
+
SELECT name FROM sessions WHERE sessions.id = history.session_id
|
|
243
|
+
) WHERE actor_id IS NULL;
|
|
244
|
+
`);
|
|
245
|
+
}
|
|
246
|
+
function validateSharedSessionActors(db) {
|
|
247
|
+
requireTables(db, [...CORE_TABLES, "session_members"]);
|
|
248
|
+
requireColumns(db, "operations", ["actor_id"]);
|
|
249
|
+
requireColumns(db, "transactions", ["owner_actor_id"]);
|
|
250
|
+
requireColumns(db, "plans", ["owner_actor_id", "claim_token"]);
|
|
251
|
+
requireColumns(db, "history", ["actor_id"]);
|
|
252
|
+
requireForeignKey(db, "session_members", "session_id", "sessions", "id");
|
|
253
|
+
const invalidMemberships = db.prepare(`SELECT COUNT(*) AS count FROM sessions
|
|
254
|
+
LEFT JOIN session_members
|
|
255
|
+
ON session_members.actor_id = sessions.name
|
|
256
|
+
AND session_members.session_id = sessions.id
|
|
257
|
+
WHERE session_members.actor_id IS NULL`).get();
|
|
258
|
+
if (invalidMemberships.count) {
|
|
259
|
+
throw new Error("State migration could not establish legacy session membership.");
|
|
260
|
+
}
|
|
261
|
+
for (const [table, column] of [
|
|
262
|
+
["operations", "actor_id"],
|
|
263
|
+
["transactions", "owner_actor_id"],
|
|
264
|
+
["plans", "owner_actor_id"],
|
|
265
|
+
["history", "actor_id"],
|
|
266
|
+
]) {
|
|
267
|
+
const row = db.prepare(`SELECT COUNT(*) AS count FROM ${table} WHERE ${column} IS NULL`).get();
|
|
268
|
+
if (row.count)
|
|
269
|
+
throw new Error(`State migration left ${table}.${column} empty.`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function addColumn(db, table, column, definition) {
|
|
273
|
+
const columns = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
274
|
+
if (!columns.some((candidate) => candidate.name === column)) {
|
|
275
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function requireTables(db, tables) {
|
|
279
|
+
const found = new Set(db.prepare("SELECT name FROM sqlite_schema WHERE type = 'table'").all().map((row) => row.name));
|
|
280
|
+
for (const table of tables) {
|
|
281
|
+
if (!found.has(table))
|
|
282
|
+
throw new Error(`State migration did not create table "${table}".`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function requireIndexes(db, required) {
|
|
286
|
+
const indexes = new Set(db.prepare("SELECT name FROM sqlite_schema WHERE type = 'index'").all().map((index) => index.name));
|
|
287
|
+
for (const index of required) {
|
|
288
|
+
if (!indexes.has(index))
|
|
289
|
+
throw new Error(`State migration did not create index "${index}".`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function requireForeignKey(db, table, from, target, to) {
|
|
293
|
+
const keys = db.prepare(`PRAGMA foreign_key_list(${table})`).all();
|
|
294
|
+
if (!keys.some((key) => key.table === target && key.from === from && key.to === to)) {
|
|
295
|
+
throw new Error(`State migration requires ${table}.${from} to reference ${target}.${to}.`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function requireColumns(db, table, required) {
|
|
299
|
+
const columns = new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name));
|
|
300
|
+
for (const column of required) {
|
|
301
|
+
if (!columns.has(column)) {
|
|
302
|
+
throw new Error(`State migration did not create ${table}.${column}.`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { OperationRecord, ProfileRecord, SessionRecord, TransactionRecord } from "./store.js";
|
|
2
|
-
import type { Row, Warning } from "./types.js";
|
|
3
|
-
export declare function sessionData(session: SessionRecord):
|
|
4
|
-
export declare function profileData(profile: ProfileRecord):
|
|
5
|
-
export declare function operationData(operation: OperationRecord):
|
|
6
|
-
export declare function transactionData(transaction: TransactionRecord, statements: number):
|
|
2
|
+
import type { OperationData, ProfileData, Row, SessionData, TransactionData, Warning } from "./types.js";
|
|
3
|
+
export declare function sessionData(session: SessionRecord): SessionData;
|
|
4
|
+
export declare function profileData(profile: ProfileRecord): ProfileData;
|
|
5
|
+
export declare function operationData(operation: OperationRecord): OperationData;
|
|
6
|
+
export declare function transactionData(transaction: TransactionRecord, statements: number): TransactionData;
|
|
7
7
|
export declare function paginationWarnings(ordered: boolean): Warning[];
|
|
8
8
|
export declare function rowsToCsv(rows: Row[], columns: string[]): string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import { DatabaseSync } from "node:sqlite";
|
|
3
|
-
import { hash, parseJson, toJsonSafe } from "./util.js";
|
|
3
|
+
import { hash, isSqlParameters, parseJson, toJsonSafe } from "./util.js";
|
|
4
4
|
let db;
|
|
5
5
|
let source;
|
|
6
6
|
let readOnly = true;
|
|
@@ -97,7 +97,7 @@ function execute(request) {
|
|
|
97
97
|
}
|
|
98
98
|
try {
|
|
99
99
|
for (const operation of operations) {
|
|
100
|
-
const result = bindRun(database.prepare(operation.sql), parseJson(operation.parameters,
|
|
100
|
+
const result = bindRun(database.prepare(operation.sql), parseJson(operation.parameters, `operation "${operation.id}" parameters`, isSqlParameters));
|
|
101
101
|
results.push({ affectedRows: Number(result.changes) });
|
|
102
102
|
}
|
|
103
103
|
}
|
package/dist/src/stateql.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BatchCommand, BatchOptions, ConnectOptions, ExecOptions, ExecutionOptions, FilterOptions,
|
|
1
|
+
import type { ActorLinkData, ActorResolutionData, ActorsData, ActorUnlinkData, AliasData, ApplyData, BatchCommand, BatchOptions, CapabilitiesData, CloseSessionData, ColumnsData, CommitTransactionData, ConnectOptions, ConnectionData, CountData, DisconnectData, DoctorData, ExecData, ExecOptions, ExecutionOptions, ExportData, FilterOptions, HistoryData, OperationData, PlanData, PlanOptions, ProfileData, ProfilesData, ProfileOptions, PurgeData, QueryOptions, RemovedProfileData, Response, ResultData, RollbackTransactionData, RowsData, RowsOptions, StateQLActorOptions, StateQLOptions, StateQLSnapshot, StatusData, SessionData, SessionsData, SessionSummaryData, TransactionData } from "./types.js";
|
|
2
2
|
export declare class StateQL {
|
|
3
3
|
static forActor(options: StateQLActorOptions): StateQL;
|
|
4
4
|
private readonly store;
|
|
@@ -14,48 +14,50 @@ export declare class StateQL {
|
|
|
14
14
|
private readonly signal?;
|
|
15
15
|
private readonly credentialResolver?;
|
|
16
16
|
private readonly now;
|
|
17
|
+
private closed;
|
|
17
18
|
constructor(options?: StateQLOptions);
|
|
18
19
|
close(): void;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
[Symbol.dispose](): void;
|
|
21
|
+
connect(target?: string, options?: ConnectOptions): Promise<Response<ConnectionData>>;
|
|
22
|
+
addProfile(name: string, target?: string, options?: ProfileOptions): Promise<Response<ProfileData>>;
|
|
23
|
+
listProfiles(): Promise<Response<ProfilesData>>;
|
|
24
|
+
showProfile(name: string): Promise<Response<ProfileData>>;
|
|
25
|
+
removeProfile(name: string): Promise<Response<RemovedProfileData>>;
|
|
26
|
+
disconnect(): Promise<Response<DisconnectData>>;
|
|
25
27
|
snapshot(options?: {
|
|
26
28
|
historyLimit?: number;
|
|
27
29
|
}): StateQLSnapshot;
|
|
28
|
-
status(): Promise<Response<
|
|
29
|
-
linkActor(session: string, actorId: string): Promise<Response<
|
|
30
|
-
unlinkActor(session: string, actorId: string): Promise<Response<
|
|
31
|
-
listActors(session: string): Promise<Response<
|
|
32
|
-
resolveActor(actorId: string): Promise<Response<
|
|
33
|
-
startSession(name: string): Promise<Response<
|
|
34
|
-
listSessions(): Promise<Response<
|
|
35
|
-
showSession(idOrName?: string): Promise<Response<
|
|
36
|
-
sessionSummary(): Promise<Response<
|
|
37
|
-
closeSession(): Promise<Response<
|
|
38
|
-
query(sql: string, options?: QueryOptions): Promise<Response<
|
|
39
|
-
show(idOrAlias: string): Promise<Response<
|
|
40
|
-
filter(idOrAlias: string, predicate: string, options?: FilterOptions): Promise<Response<
|
|
41
|
-
rows(idOrAlias: string, options?: RowsOptions): Promise<Response<
|
|
42
|
-
count(idOrAlias: string): Promise<Response<
|
|
43
|
-
columns(idOrAlias: string): Promise<Response<
|
|
44
|
-
setAlias(name: string, id: string): Promise<Response<
|
|
45
|
-
exportResult(idOrAlias: string, output: string, format?: "json" | "jsonl" | "csv"): Promise<Response<
|
|
46
|
-
exec(sql: string, options?: ExecOptions): Promise<Response<
|
|
47
|
-
receipt(id: string): Promise<Response<
|
|
48
|
-
beginTransaction(isolation?: string): Promise<Response<
|
|
49
|
-
transactionStatus(id?: string): Promise<Response<
|
|
50
|
-
commitTransaction(id?: string, options?: ExecutionOptions): Promise<Response<
|
|
51
|
-
rollbackTransaction(id?: string): Promise<Response<
|
|
30
|
+
status(): Promise<Response<StatusData>>;
|
|
31
|
+
linkActor(session: string, actorId: string): Promise<Response<ActorLinkData>>;
|
|
32
|
+
unlinkActor(session: string, actorId: string): Promise<Response<ActorUnlinkData>>;
|
|
33
|
+
listActors(session: string): Promise<Response<ActorsData>>;
|
|
34
|
+
resolveActor(actorId: string): Promise<Response<ActorResolutionData>>;
|
|
35
|
+
startSession(name: string): Promise<Response<SessionData>>;
|
|
36
|
+
listSessions(): Promise<Response<SessionsData>>;
|
|
37
|
+
showSession(idOrName?: string): Promise<Response<SessionData>>;
|
|
38
|
+
sessionSummary(): Promise<Response<SessionSummaryData>>;
|
|
39
|
+
closeSession(): Promise<Response<CloseSessionData>>;
|
|
40
|
+
query(sql: string, options?: QueryOptions): Promise<Response<ResultData>>;
|
|
41
|
+
show(idOrAlias: string): Promise<Response<ResultData>>;
|
|
42
|
+
filter(idOrAlias: string, predicate: string, options?: FilterOptions): Promise<Response<ResultData>>;
|
|
43
|
+
rows(idOrAlias: string, options?: RowsOptions): Promise<Response<RowsData>>;
|
|
44
|
+
count(idOrAlias: string): Promise<Response<CountData>>;
|
|
45
|
+
columns(idOrAlias: string): Promise<Response<ColumnsData>>;
|
|
46
|
+
setAlias(name: string, id: string): Promise<Response<AliasData>>;
|
|
47
|
+
exportResult(idOrAlias: string, output: string, format?: "json" | "jsonl" | "csv"): Promise<Response<ExportData>>;
|
|
48
|
+
exec(sql: string, options?: ExecOptions): Promise<Response<ExecData>>;
|
|
49
|
+
receipt(id: string): Promise<Response<OperationData>>;
|
|
50
|
+
beginTransaction(isolation?: string): Promise<Response<TransactionData>>;
|
|
51
|
+
transactionStatus(id?: string): Promise<Response<TransactionData>>;
|
|
52
|
+
commitTransaction(id?: string, options?: ExecutionOptions): Promise<Response<CommitTransactionData>>;
|
|
53
|
+
rollbackTransaction(id?: string): Promise<Response<RollbackTransactionData>>;
|
|
52
54
|
inspect(kind: string, table?: string, options?: ExecutionOptions): Promise<Response<unknown>>;
|
|
53
|
-
plan(sql: string, options?: PlanOptions): Promise<Response<
|
|
54
|
-
apply(planId: string, options?: ExecutionOptions): Promise<Response<
|
|
55
|
-
history(limit?: number): Promise<Response<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
capabilities(): Promise<Response<
|
|
55
|
+
plan(sql: string, options?: PlanOptions): Promise<Response<PlanData>>;
|
|
56
|
+
apply(planId: string, options?: ExecutionOptions): Promise<Response<ApplyData>>;
|
|
57
|
+
history(limit?: number): Promise<Response<HistoryData>>;
|
|
58
|
+
doctor(): Promise<Response<DoctorData>>;
|
|
59
|
+
purge(scope?: "expired" | "results" | "history" | "all"): Promise<Response<PurgeData>>;
|
|
60
|
+
capabilities(): Promise<Response<CapabilitiesData>>;
|
|
59
61
|
executeCommand(command: BatchCommand): Promise<Response<unknown>>;
|
|
60
62
|
batch(commands: Iterable<BatchCommand> | AsyncIterable<BatchCommand>, options?: BatchOptions): AsyncGenerator<Response<unknown>>;
|
|
61
63
|
private performExec;
|
package/dist/src/stateql.js
CHANGED
|
@@ -8,7 +8,7 @@ import { filterMaterializedRows, prepareFilterStatement, validateFilterParameter
|
|
|
8
8
|
import { operationData, paginationWarnings, profileData, rowsToCsv, sessionData, transactionData, } from "./response-data.js";
|
|
9
9
|
import { analyzeSql } from "./sql.js";
|
|
10
10
|
import { StateStore, } from "./store.js";
|
|
11
|
-
import { compactRows, defaultHome, hash, parseJson, redact, } from "./util.js";
|
|
11
|
+
import { compactRows, defaultHome, hash, isSqlParameters, parseJson, redact, } from "./util.js";
|
|
12
12
|
const DEFAULT_SNAPSHOT_HISTORY_LIMIT = 50;
|
|
13
13
|
const MAX_SNAPSHOT_HISTORY_LIMIT = 100;
|
|
14
14
|
export class StateQL {
|
|
@@ -42,6 +42,7 @@ export class StateQL {
|
|
|
42
42
|
signal;
|
|
43
43
|
credentialResolver;
|
|
44
44
|
now;
|
|
45
|
+
closed = false;
|
|
45
46
|
constructor(options = {}) {
|
|
46
47
|
this.now = options.now ?? (() => new Date());
|
|
47
48
|
this.sessionName = options.session ?? env.STQL_SESSION ?? "default";
|
|
@@ -49,24 +50,38 @@ export class StateQL {
|
|
|
49
50
|
if (!this.actorId.trim()) {
|
|
50
51
|
throw new StateQLError("INVALID_COMMAND", "Actor ID is required.");
|
|
51
52
|
}
|
|
52
|
-
this.previewRows = options.previewRows ?? 5;
|
|
53
|
-
this.cacheTtlSeconds = options.cacheTtlSeconds ?? 300;
|
|
54
|
-
this.resultTtlSeconds = options.resultTtlSeconds ?? 86_400;
|
|
55
|
-
this.maxCellCharacters = options.maxCellCharacters ?? 200;
|
|
53
|
+
this.previewRows = nonNegativeInteger(options.previewRows ?? 5, "previewRows");
|
|
54
|
+
this.cacheTtlSeconds = nonNegativeInteger(options.cacheTtlSeconds ?? 300, "cacheTtlSeconds");
|
|
55
|
+
this.resultTtlSeconds = positiveInteger(options.resultTtlSeconds ?? 86_400, "resultTtlSeconds");
|
|
56
|
+
this.maxCellCharacters = positiveInteger(options.maxCellCharacters ?? 200, "maxCellCharacters");
|
|
56
57
|
this.maxResultRows = positiveInteger(options.maxResultRows ?? 10_000, "maxResultRows");
|
|
57
58
|
this.maxResultBytes = positiveInteger(options.maxResultBytes ?? 16 * 1024 * 1024, "maxResultBytes");
|
|
58
59
|
this.timeoutMs = executionTimeout(options.timeoutMs ?? 30_000);
|
|
60
|
+
const maxStateBytes = positiveInteger(options.maxStateBytes ?? 256 * 1024 * 1024, "maxStateBytes");
|
|
59
61
|
this.signal = options.signal;
|
|
60
62
|
this.credentialResolver = options.credentialResolver;
|
|
61
63
|
if (this.maxResultRows >= Number.MAX_SAFE_INTEGER) {
|
|
62
64
|
throw new StateQLError("INVALID_COMMAND", "maxResultRows is too large.");
|
|
63
65
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
const store = new StateStore(options.home ?? defaultHome(), this.now, maxStateBytes);
|
|
67
|
+
try {
|
|
68
|
+
store.bootstrapSession(this.sessionName, this.actorId, options.actor === undefined);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
store.close();
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
this.store = store;
|
|
66
75
|
}
|
|
67
76
|
close() {
|
|
77
|
+
if (this.closed)
|
|
78
|
+
return;
|
|
79
|
+
this.closed = true;
|
|
68
80
|
this.store.close();
|
|
69
81
|
}
|
|
82
|
+
[Symbol.dispose]() {
|
|
83
|
+
this.close();
|
|
84
|
+
}
|
|
70
85
|
async connect(target, options = {}) {
|
|
71
86
|
return this.run("connect", async (session) => {
|
|
72
87
|
if (session.active_transaction_id) {
|
|
@@ -811,14 +826,16 @@ export class StateQL {
|
|
|
811
826
|
if (version(connection) !== transaction.start_version) {
|
|
812
827
|
throw new StateQLError("TRANSACTION_FAILED", "Connection state changed after the transaction began.", { suggestedAction: "Roll back and begin a new transaction." });
|
|
813
828
|
}
|
|
829
|
+
// Validate durable payloads before opening a database adapter or changing
|
|
830
|
+
// the transaction state.
|
|
831
|
+
const operations = this.store.validatedTransactionOperations(transaction.id);
|
|
814
832
|
const context = this.executionContext(options);
|
|
815
833
|
const adapterSource = await this.resolveConnectionSource(connection, session, "transaction.commit", "write", context);
|
|
816
834
|
const adapter = await this.openAdapter(connection, context, adapterSource);
|
|
817
835
|
try {
|
|
818
|
-
if (!this.store.
|
|
836
|
+
if (!this.store.claimTransactionForCommit(transaction.id, session.id, this.actorId, operations)) {
|
|
819
837
|
throw new StateQLError("TRANSACTION_FAILED", "Transaction is no longer active.");
|
|
820
838
|
}
|
|
821
|
-
const operations = this.store.transactionOperations(transaction.id);
|
|
822
839
|
if (operations.some((operation) => operation.connection_id !== connection.id)) {
|
|
823
840
|
this.store.finishTransaction(transaction.id, session.id, this.actorId, "failed");
|
|
824
841
|
throw new StateQLError("TRANSACTION_FAILED", "Transaction contains writes staged for another connection.");
|
|
@@ -1026,6 +1043,7 @@ export class StateQL {
|
|
|
1026
1043
|
if (Date.parse(plan.expires_at) <= this.now().getTime()) {
|
|
1027
1044
|
throw new StateQLError("STALE_PLAN", "Plan has expired.");
|
|
1028
1045
|
}
|
|
1046
|
+
const planParameters = parseJson(plan.parameters, `plan "${plan.id}" parameters`, isSqlParameters);
|
|
1029
1047
|
const claimToken = this.store.nextId("claim");
|
|
1030
1048
|
const claimed = this.store.claimPlan(plan.id, session.id, this.actorId, claimToken);
|
|
1031
1049
|
if (!claimed) {
|
|
@@ -1058,7 +1076,7 @@ export class StateQL {
|
|
|
1058
1076
|
await closeAdapterQuietly(adapter);
|
|
1059
1077
|
}
|
|
1060
1078
|
const result = await this.performExec(session, connection, claimed.sql, {
|
|
1061
|
-
params:
|
|
1079
|
+
params: planParameters,
|
|
1062
1080
|
allowUnbounded: Boolean(claimed.allow_unbounded),
|
|
1063
1081
|
allowDestructive: Boolean(claimed.allow_destructive),
|
|
1064
1082
|
}, context, { planId: claimed.id, claimToken }, adapterSource);
|
|
@@ -1089,6 +1107,25 @@ export class StateQL {
|
|
|
1089
1107
|
},
|
|
1090
1108
|
}));
|
|
1091
1109
|
}
|
|
1110
|
+
async doctor() {
|
|
1111
|
+
return this.run("doctor", async (session) => ({
|
|
1112
|
+
data: this.store.diagnostics(session.id),
|
|
1113
|
+
}));
|
|
1114
|
+
}
|
|
1115
|
+
async purge(scope = "expired") {
|
|
1116
|
+
return this.run("purge", async (session) => {
|
|
1117
|
+
if (!["expired", "results", "history", "all"].includes(scope)) {
|
|
1118
|
+
throw new StateQLError("INVALID_COMMAND", `Unknown purge scope "${scope}".`);
|
|
1119
|
+
}
|
|
1120
|
+
if (scope === "all" && session.active_transaction_id) {
|
|
1121
|
+
throw new StateQLError("TRANSACTION_FAILED", "Commit or roll back the active transaction before purging all data.");
|
|
1122
|
+
}
|
|
1123
|
+
return {
|
|
1124
|
+
data: { scope, deleted: this.store.purge(session.id, scope) },
|
|
1125
|
+
executed: true,
|
|
1126
|
+
};
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1092
1129
|
async capabilities() {
|
|
1093
1130
|
return this.run("capabilities", async () => ({
|
|
1094
1131
|
data: {
|
|
@@ -1104,6 +1141,9 @@ export class StateQL {
|
|
|
1104
1141
|
credential_resolver: true,
|
|
1105
1142
|
deadlines: true,
|
|
1106
1143
|
cancellation: true,
|
|
1144
|
+
state_diagnostics: true,
|
|
1145
|
+
state_purge: true,
|
|
1146
|
+
state_quota: true,
|
|
1107
1147
|
},
|
|
1108
1148
|
},
|
|
1109
1149
|
}));
|
|
@@ -1228,6 +1268,10 @@ export class StateQL {
|
|
|
1228
1268
|
return this.history(command.limit ?? 20);
|
|
1229
1269
|
case "receipt":
|
|
1230
1270
|
return this.receipt(batchString(command.handle, "handle"));
|
|
1271
|
+
case "doctor":
|
|
1272
|
+
return this.doctor();
|
|
1273
|
+
case "purge":
|
|
1274
|
+
return this.purge(command.scope ?? "expired");
|
|
1231
1275
|
case "capabilities":
|
|
1232
1276
|
return this.capabilities();
|
|
1233
1277
|
default:
|
package/dist/src/store.d.ts
CHANGED
|
@@ -111,8 +111,10 @@ export interface SessionMemberRecord {
|
|
|
111
111
|
}
|
|
112
112
|
export declare class StateStore {
|
|
113
113
|
private readonly now;
|
|
114
|
+
private readonly maxStateBytes;
|
|
114
115
|
readonly db: DatabaseSync;
|
|
115
|
-
|
|
116
|
+
private closed;
|
|
117
|
+
constructor(home: string, now: () => Date, maxStateBytes?: number);
|
|
116
118
|
close(): void;
|
|
117
119
|
nextId(prefix: string): string;
|
|
118
120
|
ensureSession(name?: string): SessionRecord;
|
|
@@ -219,6 +221,8 @@ export declare class StateStore {
|
|
|
219
221
|
}): TransactionRecord | undefined;
|
|
220
222
|
getTransaction(id: string): TransactionRecord | undefined;
|
|
221
223
|
transactionOperations(transactionId: string): OperationRecord[];
|
|
224
|
+
validatedTransactionOperations(transactionId: string): OperationRecord[];
|
|
225
|
+
claimTransactionForCommit(transactionId: string, sessionId: string, actorId: string, expectedOperations: OperationRecord[]): boolean;
|
|
222
226
|
markTransactionCommitting(transactionId: string, sessionId: string, actorId: string): boolean;
|
|
223
227
|
markTransactionOutcomeUnknown(transactionId: string, sessionId: string, actorId: string): void;
|
|
224
228
|
finishTransaction(transactionId: string, sessionId: string, actorId: string, state: "rolled_back" | "failed"): boolean;
|
|
@@ -275,8 +279,22 @@ export declare class StateStore {
|
|
|
275
279
|
knownResults(sessionId: string, limit: number): Array<ResultRecord & {
|
|
276
280
|
alias: string | null;
|
|
277
281
|
}>;
|
|
282
|
+
diagnostics(sessionId: string): {
|
|
283
|
+
integrity: "ok" | "issues";
|
|
284
|
+
issues: Array<{
|
|
285
|
+
code: string;
|
|
286
|
+
record?: string;
|
|
287
|
+
}>;
|
|
288
|
+
migrations: string[];
|
|
289
|
+
storage: {
|
|
290
|
+
result_bytes: number;
|
|
291
|
+
results: number;
|
|
292
|
+
history: number;
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
purge(sessionId: string, scope: "expired" | "results" | "history" | "all"): number;
|
|
296
|
+
private enforceResultQuota;
|
|
297
|
+
private resultBytes;
|
|
278
298
|
private deleteExpiredData;
|
|
279
299
|
private recoverStaleCommittingTransactions;
|
|
280
|
-
private migrate;
|
|
281
|
-
private addColumn;
|
|
282
300
|
}
|