@fadhilp/stateql 0.4.4 → 0.5.2
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 +19 -2
- package/dist/src/adapters.js +11 -8
- 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 -11
- 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 +4 -2
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ StateQL is built around durable handles:
|
|
|
12
12
|
rerunning the original SQL.
|
|
13
13
|
3. Use operation, plan, and transaction handles to inspect and control writes.
|
|
14
14
|
|
|
15
|
-
Requires Node.js 22.
|
|
15
|
+
Requires Node.js 22.16 or newer for the required `node:sqlite` APIs.
|
|
16
16
|
|
|
17
17
|
## Quick start
|
|
18
18
|
|
|
@@ -160,6 +160,8 @@ stql plan <sql> [--allow-unbounded] [--allow-destructive]
|
|
|
160
160
|
stql apply <plan-handle>
|
|
161
161
|
stql history [--limit N]
|
|
162
162
|
stql receipt <operation-handle>
|
|
163
|
+
stql doctor
|
|
164
|
+
stql purge [expired|results|history|all]
|
|
163
165
|
stql capabilities
|
|
164
166
|
stql batch [commands.json|commands.jsonl|-] [--continue-on-error]
|
|
165
167
|
stql pipe [--continue-on-error]
|
|
@@ -244,6 +246,20 @@ Command history keeps the latest 10,000 entries per session. SQLite cache reuse
|
|
|
244
246
|
also checks the database file signature. PostgreSQL and MySQL cache reuse is
|
|
245
247
|
labeled `ttl_based` and is never authoritative.
|
|
246
248
|
|
|
249
|
+
StateQL limits persisted result payloads to 256 MiB by default. When that quota
|
|
250
|
+
is reached it removes the oldest unaliased results; aliases remain protected. A
|
|
251
|
+
single result that cannot fit fails with `STATE_QUOTA_EXCEEDED`. Configure the
|
|
252
|
+
limit with `maxStateBytes` in the library or `--max-state-bytes` in the CLI.
|
|
253
|
+
Cache and result retention can be configured with `cacheTtlSeconds` and
|
|
254
|
+
`resultTtlSeconds`, or their `--cache-ttl-seconds` and
|
|
255
|
+
`--result-ttl-seconds` CLI equivalents.
|
|
256
|
+
|
|
257
|
+
`stql doctor` checks SQLite integrity and stored payload shapes without printing
|
|
258
|
+
SQL, parameters, or result values. `stql purge` removes expired data by default;
|
|
259
|
+
use `results`, `history`, or `all` for explicit session cleanup. On POSIX
|
|
260
|
+
systems, StateQL removes group and world access from its state directory,
|
|
261
|
+
database, and SQLite sidecar files.
|
|
262
|
+
|
|
247
263
|
### Local filtering
|
|
248
264
|
|
|
249
265
|
`filter` evaluates one scalar SQLite predicate against a stored result. It
|
|
@@ -347,6 +363,7 @@ const stateql = StateQL.forActor({
|
|
|
347
363
|
actor: "pi-session-id",
|
|
348
364
|
timeoutMs: 30_000,
|
|
349
365
|
maxResultBytes: 16 * 1024 * 1024,
|
|
366
|
+
maxStateBytes: 256 * 1024 * 1024,
|
|
350
367
|
});
|
|
351
368
|
|
|
352
369
|
const controller = new AbortController();
|
|
@@ -437,4 +454,4 @@ and keeping values out of their own logs and model-visible data.
|
|
|
437
454
|
For writes, credential resolution happens after StateQL atomically reserves the
|
|
438
455
|
operation for duplicate protection. A resolution failure keeps a non-executed
|
|
439
456
|
`failed` audit record, does not consume the idempotency key, and permits a safe
|
|
440
|
-
retry.
|
|
457
|
+
retry.
|
package/dist/src/adapters.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fork } from "node:child_process";
|
|
2
2
|
import { createConnection as createMySqlConnection, } from "mysql2";
|
|
3
3
|
import { Client, types as pgTypes } from "pg";
|
|
4
|
-
import { parseJson, toJsonSafe } from "./util.js";
|
|
4
|
+
import { isSqlParameters, parseJson, toJsonSafe } from "./util.js";
|
|
5
5
|
export class AdapterExecutionError extends Error {
|
|
6
6
|
reason;
|
|
7
7
|
outcomeUnknown;
|
|
@@ -53,6 +53,7 @@ class SQLiteAdapter {
|
|
|
53
53
|
pending = new Map();
|
|
54
54
|
nextId = 1;
|
|
55
55
|
closed = false;
|
|
56
|
+
closePromise;
|
|
56
57
|
constructor(source, readOnly, context) {
|
|
57
58
|
this.source = source;
|
|
58
59
|
this.readOnly = readOnly;
|
|
@@ -100,12 +101,12 @@ class SQLiteAdapter {
|
|
|
100
101
|
return this.call("inspect", [kind, table], false, false);
|
|
101
102
|
}
|
|
102
103
|
async close() {
|
|
103
|
-
if (this.
|
|
104
|
-
return;
|
|
104
|
+
if (this.closePromise)
|
|
105
|
+
return this.closePromise;
|
|
105
106
|
this.closed = true;
|
|
106
107
|
if (this.child.exitCode !== null || this.child.signalCode !== null)
|
|
107
108
|
return;
|
|
108
|
-
|
|
109
|
+
this.closePromise = new Promise((resolve) => {
|
|
109
110
|
const done = () => {
|
|
110
111
|
clearTimeout(timer);
|
|
111
112
|
resolve();
|
|
@@ -117,7 +118,6 @@ class SQLiteAdapter {
|
|
|
117
118
|
catch {
|
|
118
119
|
// Process already exited.
|
|
119
120
|
}
|
|
120
|
-
resolve();
|
|
121
121
|
}, 1_000);
|
|
122
122
|
timer.unref();
|
|
123
123
|
this.child.once("exit", done);
|
|
@@ -133,6 +133,7 @@ class SQLiteAdapter {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
});
|
|
136
|
+
return this.closePromise;
|
|
136
137
|
}
|
|
137
138
|
async call(operation, args, outcomeUnknown, batch) {
|
|
138
139
|
throwIfStopped(this.context, false);
|
|
@@ -148,7 +149,9 @@ class SQLiteAdapter {
|
|
|
148
149
|
readOnly: this.readOnly,
|
|
149
150
|
operation,
|
|
150
151
|
args,
|
|
151
|
-
|
|
152
|
+
// Let the client deadline terminate the worker before SQLite's
|
|
153
|
+
// lock timeout races it and reports a known, non-executed failure.
|
|
154
|
+
busyTimeoutMs: Math.min(5_000, remainingMilliseconds(this.context) + 250),
|
|
152
155
|
}, (error) => {
|
|
153
156
|
if (error)
|
|
154
157
|
reject(error);
|
|
@@ -288,7 +291,7 @@ class PostgresAdapter {
|
|
|
288
291
|
try {
|
|
289
292
|
for (const operation of operations) {
|
|
290
293
|
await this.setLocalDeadline();
|
|
291
|
-
const result = await this.query(operation.sql, postgresParams(parseJson(operation.parameters,
|
|
294
|
+
const result = await this.query(operation.sql, postgresParams(parseJson(operation.parameters, `operation "${operation.id}" parameters`, isSqlParameters)), true);
|
|
292
295
|
results.push({ affectedRows: result.rowCount ?? 0 });
|
|
293
296
|
}
|
|
294
297
|
}
|
|
@@ -506,7 +509,7 @@ class MySqlAdapter {
|
|
|
506
509
|
const results = [];
|
|
507
510
|
try {
|
|
508
511
|
for (const operation of operations) {
|
|
509
|
-
const [result] = await this.query(operation.sql, mysqlParams(parseJson(operation.parameters,
|
|
512
|
+
const [result] = await this.query(operation.sql, mysqlParams(parseJson(operation.parameters, `operation "${operation.id}" parameters`, isSqlParameters)), true, true);
|
|
510
513
|
results.push({ affectedRows: mysqlAffectedRows(result) });
|
|
511
514
|
}
|
|
512
515
|
}
|
package/dist/src/cli.js
CHANGED
|
@@ -25,6 +25,9 @@ const parsed = parseArgs({
|
|
|
25
25
|
offset: { type: "string" },
|
|
26
26
|
limit: { type: "string" },
|
|
27
27
|
"timeout-ms": { type: "string" },
|
|
28
|
+
"max-state-bytes": { type: "string" },
|
|
29
|
+
"cache-ttl-seconds": { type: "string" },
|
|
30
|
+
"result-ttl-seconds": { type: "string" },
|
|
28
31
|
format: { type: "string" },
|
|
29
32
|
output: { type: "string" },
|
|
30
33
|
isolation: { type: "string" },
|
|
@@ -50,6 +53,15 @@ const stateql = new StateQL({
|
|
|
50
53
|
? {}
|
|
51
54
|
: { timeoutMs: Number(values["timeout-ms"]) }),
|
|
52
55
|
...(process.env.STQL_ACTOR ? { actor: process.env.STQL_ACTOR } : {}),
|
|
56
|
+
...(values["max-state-bytes"] === undefined
|
|
57
|
+
? {}
|
|
58
|
+
: { maxStateBytes: Number(values["max-state-bytes"]) }),
|
|
59
|
+
...(values["cache-ttl-seconds"] === undefined
|
|
60
|
+
? {}
|
|
61
|
+
: { cacheTtlSeconds: Number(values["cache-ttl-seconds"]) }),
|
|
62
|
+
...(values["result-ttl-seconds"] === undefined
|
|
63
|
+
? {}
|
|
64
|
+
: { resultTtlSeconds: Number(values["result-ttl-seconds"]) }),
|
|
53
65
|
signal: abortController.signal,
|
|
54
66
|
});
|
|
55
67
|
try {
|
|
@@ -195,6 +207,10 @@ async function dispatch() {
|
|
|
195
207
|
return stateql.history(numberOption(values.limit, 20));
|
|
196
208
|
case "receipt":
|
|
197
209
|
return stateql.receipt(requireValue(subcommand, "operation handle"));
|
|
210
|
+
case "doctor":
|
|
211
|
+
return stateql.doctor();
|
|
212
|
+
case "purge":
|
|
213
|
+
return stateql.purge(purgeScope(subcommand));
|
|
198
214
|
case "capabilities":
|
|
199
215
|
return stateql.capabilities();
|
|
200
216
|
default:
|
|
@@ -325,6 +341,13 @@ function parseBatchJson(value, location) {
|
|
|
325
341
|
function numberOption(value, fallback) {
|
|
326
342
|
return value === undefined ? fallback : Number(value);
|
|
327
343
|
}
|
|
344
|
+
function purgeScope(value) {
|
|
345
|
+
if (!value || value === "expired")
|
|
346
|
+
return "expired";
|
|
347
|
+
if (value === "results" || value === "history" || value === "all")
|
|
348
|
+
return value;
|
|
349
|
+
throw new Error("purge scope must be expired, results, history, or all.");
|
|
350
|
+
}
|
|
328
351
|
function cacheMode(value) {
|
|
329
352
|
if (!value || value === "auto")
|
|
330
353
|
return "auto";
|
|
@@ -515,12 +538,13 @@ Commands:
|
|
|
515
538
|
alias set
|
|
516
539
|
inspect schema|table|columns|indexes|constraints
|
|
517
540
|
transaction begin|status|commit|rollback
|
|
518
|
-
plan, apply, history, receipt, capabilities
|
|
541
|
+
plan, apply, history, receipt, doctor, purge, capabilities
|
|
519
542
|
batch [file.json|file.jsonl|-]
|
|
520
543
|
pipe
|
|
521
544
|
|
|
522
545
|
SQL parameters: --params JSON, repeated --param VALUE, or --params-file FILE.
|
|
523
546
|
Deadline: --timeout-ms N (default: 30000). Ctrl+C cancels database work.
|
|
547
|
+
State: --max-state-bytes N, --cache-ttl-seconds N, --result-ttl-seconds N.
|
|
524
548
|
Output: --output agent|json|jsonl|text|silent (default: agent).
|
|
525
549
|
Batch/pipe accept JSON array files or JSONL streams. Stop on first error.`;
|
|
526
550
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { StateQL } from "./stateql.js";
|
|
2
2
|
export { CredentialResolutionError, StateQLError, exitCodeFor, } from "./errors.js";
|
|
3
3
|
export type { CredentialResolutionFailure } from "./errors.js";
|
|
4
|
-
export type { BatchCommand, BatchCommandName, BatchOptions, ConnectOptions, CredentialAccess, CredentialOperation, CredentialRequest, CredentialResolver, ExecOptions, ExecutionOptions, Failure, FilterOptions, HistoryEntry, PlanOptions, ProfileOptions, QueryOptions, Response, RowsOptions, SqlParameters, StateQLActorOptions, StateQLOptions, StateQLSnapshot, Success, } from "./types.js";
|
|
4
|
+
export type { ActorData, ActorLinkData, ActorResolutionData, ActorsData, ActorUnlinkData, AliasData, ApplyData, BatchCommand, BatchCommandName, BatchOptions, CapabilitiesData, CloseSessionData, Column, ColumnsData, CommitTransactionData, ConnectOptions, ConnectionData, CountData, CredentialAccess, CredentialOperation, CredentialRequest, CredentialResolver, DisconnectData, DoctorData, Driver, ExecData, ExecOptions, ExecutionOptions, ExportData, Failure, FilterOptions, HistoryData, HistoryEntry, OperationData, PlanData, PlanOptions, ProfileData, ProfileOptions, ProfilesData, PurgeData, QueryOptions, RecentOperationData, RecentResultData, RemovedProfileData, Response, ResultData, RollbackTransactionData, Row, RowsData, RowsOptions, SqlParameters, StateQLActorOptions, StateQLOptions, StateConfidence, StateQLSnapshot, StatusConnectionData, StatusData, Success, SessionData, SessionListItem, SessionsData, SessionSummaryData, TransactionData, TransactionReferenceData, Warning, } from "./types.js";
|
|
@@ -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;
|