@fadhilp/stateql 0.13.0 → 0.13.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/README.md CHANGED
@@ -1,18 +1,17 @@
1
1
  # StateQL
2
2
 
3
3
  StateQL is a stateful database CLI and TypeScript library for AI agents and
4
- automation. It provides a safe interface for querying, changing, and inspecting
5
- SQLite, PostgreSQL, MySQL, MongoDB, and Redis databases while keeping results
6
- reusable and operations traceable across commands.
4
+ automation. Query, change, and inspect databases while keeping results reusable
5
+ and operations traceable across commands.
7
6
 
8
- StateQL is built around durable handles:
7
+ - **SQL:** SQLite, PostgreSQL, and MySQL.
8
+ - **Native commands:** MongoDB (EJSON) and Redis (JSON).
9
+ - **Durable handles:** reuse, filter, page, count, alias, or export stored results
10
+ without rerunning the original query.
11
+ - **Tracked writes:** inspect operations, review plans, and stage transactions
12
+ where supported by the database.
9
13
 
10
- 1. Run a query and receive a result handle such as `q_k7m2v5x9c3d6f8h4j2n7p5r9tw`.
11
- 2. Reuse, filter, page, count, alias, or export that stored result without
12
- rerunning the original SQL.
13
- 3. Use operation, plan, and transaction handles to inspect and control writes.
14
-
15
- Requires Node.js 22.16 or newer for the required `node:sqlite` APIs.
14
+ Requires **Node.js 22.16 or newer** for the required `node:sqlite` APIs.
16
15
 
17
16
  ## Quick start
18
17
 
@@ -22,11 +21,13 @@ Install the CLI:
22
21
  npm install -g @fadhilp/stateql
23
22
  ```
24
23
 
25
- Connect to an existing SQLite database and run a bounded, parameterized query:
24
+ Connect to an existing SQLite database containing a `users` table. These shell
25
+ examples use Bash; see the [usage guide](docs/usage.md) for connection options and
26
+ SQL parameter handling.
26
27
 
27
28
  ```bash
28
29
  export STQL_SESSION=audit
29
- stql profile add local ./app.sqlite
30
+ stql profile add local ./app.sqlite --read-only
30
31
  stql connect local
31
32
 
32
33
  stql query \
@@ -37,838 +38,62 @@ stql query \
37
38
 
38
39
  Parameters keep values separate from SQL. `ORDER BY` makes paging stable, and
39
40
  `LIMIT` bounds work at the database. The default `agent` output is compact,
40
- one-line JSON:
41
-
42
- ```json
43
- {"ok":true,"handle":"q_k7m2v5x9c3d6f8h4j2n7p5r9tw","rows":[{"id":7,"name":"Ada","email":"ada@example.com"},{"id":12,"name":"Grace","email":"grace@example.com"},{"id":18,"name":"Linus","email":"linus@kernel.org"}],"truncated":false,"cached":false,"total":3,"next_offset":null}
44
- ```
45
-
46
- `q_k7m2v5x9c3d6f8h4j2n7p5r9tw` is a durable snapshot. Filter it locally without accessing the original
47
- database:
48
-
49
- ```bash
50
- stql filter q_k7m2v5x9c3d6f8h4j2n7p5r9tw "email LIKE ?" --param "%@example.com"
51
- ```
52
-
53
- ```json
54
- {"ok":true,"handle":"q_z4n8c2v6b3m7k5j9h2g4f6d8sa","rows":[{"id":7,"name":"Ada","email":"ada@example.com"},{"id":12,"name":"Grace","email":"grace@example.com"}],"truncated":false,"cached":false,"total":2,"next_offset":null}
55
- ```
56
-
57
- The filtered snapshot receives its own handle. Give it a readable alias, page
58
- through it, inspect its count, or export it without rerunning SQL:
59
-
60
- ```bash
61
- stql alias set example-users q_z4n8c2v6b3m7k5j9h2g4f6d8sa
62
- stql rows example-users --offset 0 --limit 1
63
- stql rows example-users --offset 1 --limit 1
64
- stql count example-users
65
- stql export example-users --output example-users.csv --format csv
66
- ```
67
-
68
- Example first page:
69
-
70
- ```json
71
- {"ok":true,"handle":"q_z4n8c2v6b3m7k5j9h2g4f6d8sa","rows":[{"id":7,"name":"Ada","email":"ada@example.com"}],"total":2,"truncated":true,"next_offset":1}
72
- ```
73
-
74
- Running the same normalized query with the same parameters reuses `q_k7m2v5x9c3d6f8h4j2n7p5r9tw` while
75
- its cache entry is valid. Use `--cache bypass` when a fresh read is required.
76
-
77
- ## Connections and profiles
78
-
79
- A connection accepts exactly one source: a direct target, `--env`,
80
- `--credential-ref`, or `--profile`. Library and batch callers may additionally
81
- attach `passwordRef`/`password_ref` to a literal password-free remote target;
82
- it is not a fourth source.
83
-
84
- ```bash
85
- stql connect <sqlite-path|postgres-url|mysql-url|mongodb-url> [--name NAME] [--read-write]
86
- stql connect --env ENV [--name NAME] [--read-write]
87
- stql connect --credential-ref REF [--name NAME] [--read-write]
88
- stql connect --profile NAME
89
- stql disconnect
90
- stql status
91
- ```
92
-
93
- ### Environment-backed credentials
94
-
95
- PostgreSQL, MySQL, and MongoDB credentials should come from environment
96
- variables. The variable must contain the complete connection URL, not only its
97
- password. Environment-backed SQLite paths require an explicit `sqlite:` prefix.
98
-
99
- ```bash
100
- export APP_DATABASE_URL='postgres://user:password@host/app'
101
- stql connect --env APP_DATABASE_URL --name app --read-only
102
-
103
- export MYSQL_DATABASE_URL='mysql://user:password@host/app'
104
- stql connect --env MYSQL_DATABASE_URL --name mysql-app --read-only
105
-
106
- export MONGODB_URL='mongodb://user:password@host/app'
107
- stql connect --env MONGODB_URL --name mongo-app --read-only
108
-
109
- export SQLITE_DATABASE='sqlite:./app.sqlite'
110
- stql connect --env SQLITE_DATABASE --name local --read-only
111
- ```
112
-
113
- StateQL stores no PostgreSQL, MySQL, MongoDB, or Redis password.
114
- Credential-bearing URLs must be supplied through `--env` or an opaque full-URL
115
- credential reference. SQLite paths remain persisted as connection metadata.
116
-
117
- ### Local profiles
118
-
119
- Profiles store exactly one connection target, environment-variable name, or
120
- opaque credential reference together with read-only policy. A remote literal
121
- target may additionally store a `password_ref`; SQLite, environment-backed, and
122
- full-URL `credential_ref` profiles cannot. Credential values are never stored.
123
- Profiles persist under `STQL_HOME` with other StateQL metadata, and list/show
124
- responses include nullable `credential_ref` and `password_ref` fields.
125
-
126
- ```bash
127
- stql profile add local ./app.sqlite --read-write
128
- stql profile add production --env PROD_DATABASE_URL --read-only
129
- stql profile add hosted --credential-ref 'vault://team/app' --read-only
130
- stql profile list
131
- stql profile show production
132
- stql connect local
133
- stql connect --profile production
134
- ```
135
-
136
- Credential references are bounded nonempty opaque strings; StateQL does not
137
- apply environment-variable syntax or normalization to them. They can only be
138
- resolved by a trusted host `CredentialResolver`, so the standalone CLI may
139
- store them in profiles but cannot connect with them.
140
-
141
- Library callers can keep nonsecret endpoint, username, database, TLS, and CA
142
- options in the literal URL while resolving only its password:
143
-
144
- ```ts
145
- await stateql.connect(
146
- "postgres://app@db.example/app?sslmode=verify-full&sslrootcert=/etc/app-ca.pem",
147
- { passwordRef: "vault://database/app/password", readOnly: true },
148
- );
149
- ```
150
-
151
- The same field is accepted by `addProfile`, `updateProfile`, and batch
152
- `connect`/`profile.add`/`profile.update` commands (snake case in batch input).
153
- Targets with an embedded password or query parameters that override endpoint or
154
- credential fields are rejected before credential resolution or driver access.
155
-
156
- A bare connection target matching a profile name resolves to that profile;
157
- otherwise it remains a path or database URL.
158
-
159
- ### Driver notes
160
-
161
- - **SQLite:** use a filesystem path for direct connections or `sqlite:` for an
162
- environment-backed path.
163
- - **PostgreSQL:** StateQL preserves strict TLS verification by normalizing
164
- `sslmode=prefer`, `require`, and `verify-ca` to `verify-full` before opening
165
- the adapter. Use `sslmode=verify-full` explicitly for clarity. Setting
166
- `uselibpqcompat=true` opts out and keeps libpq-compatible SSL semantics.
167
- - **MySQL:** uses positional `?` parameters. MariaDB compatibility is not
168
- currently claimed.
169
- - **MongoDB:** supports `mongodb://` and `mongodb+srv://` URLs with an explicit
170
- database path. SQL methods are rejected; use the native MongoDB methods below.
171
-
172
- ## CLI reference
173
-
174
- ```text
175
- stql connect <sqlite-path|postgres-url|mysql-url|mongodb-url> [--name NAME] [--read-write]
176
- stql connect --env ENV [--name NAME] [--read-write]
177
- stql connect --profile NAME
178
- stql disconnect
179
- stql status
180
- stql profile add|list|show|remove
181
- stql session start|list|show|summary|close
182
- stql query <sql> [--params JSON | --param VALUE...] [--cache auto|bypass|require]
183
- stql filter <result-handle> <predicate> [--params JSON | --param VALUE...]
184
- stql exec <sql> [--params JSON | --param VALUE...] [--idempotency-key KEY] [--replay]
185
- [--allow-unbounded] [--allow-destructive]
186
- stql mongo query|exec|plan '<EJSON command>' [--cache MODE] [--idempotency-key KEY]
187
- [--replay] [--allow-unbounded] [--allow-destructive]
188
- stql show|count|columns <result-handle>
189
- stql rows <result-handle> [--offset N] [--limit N]
190
- stql alias set <name> <result-handle>
191
- stql export <result-handle> --output FILE [--format json|jsonl|csv]
192
- stql inspect schema|table|collection|collections|columns|indexes|constraints [name]
193
- stql transaction begin|status|commit|rollback [--isolation LEVEL]
194
- stql plan <sql> [--allow-unbounded] [--allow-destructive]
195
- stql apply <plan-handle>
196
- stql history [--limit N]
197
- stql receipt <operation-handle>
198
- stql doctor
199
- stql purge [expired|results|history|all]
200
- stql capabilities
201
- stql batch [commands.json|commands.jsonl|-] [--continue-on-error]
202
- stql pipe [--continue-on-error]
203
- ```
204
-
205
- ### SQL parameters
206
-
207
- For shell-safe positional parameters, repeat `--param`. JSON scalars become
208
- their native types; other values remain strings.
209
-
210
- ```powershell
211
- stql exec "INSERT INTO users (name, status) VALUES (?, ?)" `
212
- --param Ada --param trial
213
- ```
214
-
215
- Use `--params JSON` for a JSON array or named parameters. Use
216
- `--params-file FILE` when JSON is awkward to quote; `--params-file -` reads
217
- JSON from standard input.
218
-
219
- ### Dialect upserts
220
-
221
- PostgreSQL `INSERT ... ON CONFLICT DO NOTHING|UPDATE` and MySQL `INSERT ... ON
222
- DUPLICATE KEY UPDATE` are structurally validated and recorded with statement
223
- type `upsert`. Finite `VALUES` and MySQL `INSERT ... SET` sources use normal
224
- write policy. An update-upsert fed by `SELECT` requires `--allow-unbounded`
225
- because its candidate row count is not statically bounded. Upserts support direct
226
- `exec`, `plan`/`apply`, and staged transactions; hidden additional writes are rejected.
227
-
228
- MySQL `INSERT IGNORE` remains a non-overwriting `insert`. SQLite `INSERT OR
229
- REPLACE` retains destructive-operation approval, while SQLite modern `ON
230
- CONFLICT ... DO UPDATE` and every `MERGE` form remain blocked until the parser
231
- can expose their complete mutation structure.
232
-
233
- ### PostgreSQL diagnostics and maintenance
234
-
235
- Run PostgreSQL plans through `query`:
236
-
237
- ```bash
238
- stql query "EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT * FROM jobs WHERE id = 42"
239
- ```
240
-
241
- Plain `EXPLAIN` may plan a structurally validated `SELECT`, `INSERT`, `UPDATE`,
242
- or `DELETE`. Because `EXPLAIN ANALYZE` executes its inner statement, StateQL
243
- accepts only a validated read-only `SELECT`; `SELECT INTO`, writing CTEs, and
244
- mutations are rejected. Diagnostics execute inside PostgreSQL `BEGIN READ ONLY`
245
- and are never reused from cache. `--cache require` therefore returns
246
- `CACHE_MISS` without executing the diagnostic.
247
-
248
- StateQL supports PostgreSQL 14–18. Top-level `VALUES` is a bounded read and
249
- accepts normal PostgreSQL positional parameters. It is conservatively
250
- non-cacheable because expressions may be volatile. The following narrow `SHOW`
251
- allowlist is also available as non-cacheable diagnostics:
252
- `server_version`, `server_version_num`, `transaction_read_only`,
253
- `transaction_isolation`, and `default_transaction_isolation`. `SHOW ALL` and
254
- other settings remain blocked. Syntax accepted by StateQL but introduced by a
255
- newer PostgreSQL release may be rejected safely by an older server.
256
-
257
- `VACUUM`, `ANALYZE`, `REINDEX`, and `CLUSTER` are PostgreSQL maintenance writes:
258
-
259
- ```bash
260
- stql exec "VACUUM (ANALYZE) public.jobs" --allow-destructive
261
- stql plan "REINDEX TABLE public.jobs" --allow-destructive
262
- ```
263
-
264
- The PostgreSQL 14–18 grammar includes parenthesized `REINDEX CONCURRENTLY`,
265
- PostgreSQL 16 `BUFFER_USAGE_LIMIT` for `VACUUM`/`ANALYZE`, optional
266
- `DATABASE`/`SYSTEM` reindex names, and PostgreSQL 18 `ONLY table *` maintenance
267
- targets. Memory sizes accept an integer number of kilobytes or a quoted
268
- `B|kB|MB|GB|TB` value. Older servers may reject newer forms after dispatch, so
269
- StateQL retains conservative unknown-outcome handling.
270
-
271
- They require a read-write connection and `--allow-destructive`, reject StateQL
272
- parameters, and run as individually tracked autocommit operations. They cannot
273
- be staged in a StateQL transaction. A timeout or cancellation after dispatch is
274
- reported as `OUTCOME_UNKNOWN`; inspect database state before replaying it. Raw
275
- `BEGIN`, `COMMIT`, `ROLLBACK`, savepoint, and other transaction-control SQL
276
- remain unsupported—use `stql transaction` commands instead. See
277
- [`SQL_COMMAND_ROADMAP.md`](SQL_COMMAND_ROADMAP.md) for the exact implemented
278
- boundary and deferred command categories.
279
-
280
- ### SQLite and MySQL diagnostics and maintenance
281
-
282
- SQLite supports `EXPLAIN QUERY PLAN` for structurally read-only `SELECT`
283
- statements. MySQL supports `EXPLAIN SELECT` plus bare `SHOW TABLES`,
284
- `SHOW COLUMNS FROM table`, and `SHOW INDEX|INDEXES FROM table`. Broader
285
- `EXPLAIN`, `SHOW`, and write-bearing forms remain blocked.
286
-
287
- MySQL executable comments (`/*! ... */`) are rejected throughout SQL. Because
288
- StateQL does not assume a server `sql_mode`, quoting that could expose these
289
- comments under `ANSI_QUOTES` or `NO_BACKSLASH_ESCAPES` is also rejected.
290
-
291
- These diagnostics use `query`, work with read-only connections, preserve the
292
- original statement instead of applying StateQL's limiting SQL wrapper, and are
293
- never reused from cache. Materialized results still receive StateQL's row and
294
- byte checks.
295
-
296
- SQLite also supports bare `VACUUM`, plus `ANALYZE [target]` and
297
- `REINDEX [target]` with at most one unqualified or double-quoted target:
298
-
299
- ```bash
300
- stql exec "ANALYZE jobs" --allow-destructive
301
- stql exec "REINDEX jobs_created_at_idx" --allow-destructive
302
- stql exec "VACUUM" --allow-destructive
303
- ```
304
-
305
- These commands require a read-write connection, reject parameters, run as
306
- individually tracked autocommit operations, and cannot be staged. A timeout,
307
- cancellation, or error after dispatch is reported as `OUTCOME_UNKNOWN`.
308
- `VACUUM INTO`, schema-qualified targets, paths, `ATTACH`, and arbitrary `PRAGMA`
309
- remain blocked.
310
-
311
- MySQL supports one optionally qualified bare or backtick-quoted target for
312
- `ANALYZE TABLE`, `OPTIMIZE TABLE`, and `CHECK TABLE`:
313
-
314
- ```bash
315
- stql exec "ANALYZE TABLE jobs" --allow-destructive
316
- stql plan "OPTIMIZE TABLE jobs" --allow-destructive
317
- stql query "CHECK TABLE jobs"
318
- ```
319
-
320
- `ANALYZE` and `OPTIMIZE` are durable autocommit writes requiring a read-write
321
- connection and destructive approval; server-reported error rows become known
322
- failed operations, while timeout or cancellation after dispatch remains
323
- `OUTCOME_UNKNOWN`. `CHECK TABLE` is an unwrapped, non-cacheable autocommit read
324
- that works on read-only connections and retains normal result limits. All three
325
- reject StateQL parameters, options, multiple targets, and additional
326
- statements, and none can run during a staged transaction.
327
-
328
-
329
- ### Native MongoDB
330
-
331
- MongoDB commands use official Extended JSON (EJSON), so BSON values survive the
332
- CLI boundary:
333
-
334
- ```bash
335
- stql mongo query '{"operation":"find","collection":"users","filter":{"_id":{"$oid":"507f1f77bcf86cd799439011"}}}'
336
- stql mongo exec '{"operation":"updateOne","collection":"users","filter":{"_id":{"$oid":"507f1f77bcf86cd799439011"}},"update":{"$set":{"seen_at":{"$date":"2026-01-01T00:00:00Z"}}}}'
337
- stql mongo plan '{"operation":"deleteMany","collection":"users","filter":{"disabled":true}}' --allow-destructive
338
- ```
339
-
340
- The TypeScript equivalents are `mongoQuery(command)`, `mongoExec(command)`, and
341
- `mongoPlan(command)`. Supported reads are `find` and `aggregate`; writes are
342
- `insertOne`, `insertMany`, `updateOne`, `updateMany`, `replaceOne`, `deleteOne`,
343
- and `deleteMany`. Result documents are JSON-safe, order-preserving EJSON: for example,
344
- ObjectIds and dates appear as `{ "$oid": "..." }` and
345
- `{ "$date": { "$numberLong": "..." } }`.
346
-
347
- Empty update, replacement, or delete filters require `--allow-unbounded`;
348
- deletes and replacements also require `--allow-destructive`. Mongo inspection accepts `collections`,
349
- `collection`, `columns`, `indexes`, and `constraints` (`schema` and `table`
350
- remain aliases shared with SQL drivers). MongoDB cache confidence is TTL-based:
351
- external writes are not detected, so use `--cache bypass` for a fresh read.
352
-
353
- ```ts
354
- const result = await stateql.mongoQuery({
355
- operation: "find",
356
- collection: "users",
357
- filter: { active: true },
358
- options: { sort: { _id: 1 }, limit: 50 },
359
- });
360
- ```
361
- ### Output modes
362
-
363
- CLI output defaults to compact, one-line `agent` JSON. Successful responses
364
- flatten useful data and expose the primary durable ID as `handle`. Errors retain
365
- their complete error object. Empty warnings and tracing metadata are omitted.
41
+ one-line JSON. An example response:
366
42
 
367
43
  ```json
368
- {"ok":false,"error":{"code":"UNBOUNDED_MUTATION","message":"Mutation has no WHERE clause.","retryable":false,"executed":false,"override_flag":"--allow-unbounded"}}
44
+ {"ok":true,"handle":"q_k7m2v5x9c3d6f8h4j2n7p5r9tw","rows":[{"id":7,"name":"Ada","email":"ada@example.com"}],"truncated":false,"cached":false,"total":1,"next_offset":null}
369
45
  ```
370
46
 
371
- Other modes are:
372
-
373
- - `--output json`: original pretty, verbose envelope.
374
- - `--output jsonl`: verbose envelope on one line.
375
- - `--output text`: short human-readable status.
376
- - `--output silent`: only a successful handle.
377
-
378
- Set `STQL_OUTPUT` to choose a mode globally. For `export`, `--output` names the
379
- file, so use `STQL_OUTPUT` to choose the command's response mode. Library
380
- responses always keep the full envelope.
381
-
382
- ### Deadlines and cancellation
383
-
384
- Database commands accept `--timeout-ms N`; the default is 30,000 ms. `Ctrl+C`
385
- cancels active work.
386
-
387
- - SQLite runs in a killable child process so long synchronous statements cannot
388
- block StateQL's event loop.
389
- - PostgreSQL combines server-side `statement_timeout` with client deadlines.
390
- - MySQL deadlines destroy the active connection.
391
- - MongoDB uses driver deadlines and closes stopped operations.
392
-
393
- A timed-out or cancelled write may return `OUTCOME_UNKNOWN` when its commit
394
- status cannot be proven. Cancellation stops that command's driver work; it does
395
- not close the `StateQL` actor, and later commands remain usable.
396
-
397
- ## Durable state and result reuse
398
-
399
- State metadata lives under `STQL_HOME`, or the platform data directory when
400
- unset. StateQL keeps connections, sessions, handles, aliases, cache entries,
401
- plans, transactions, history, and receipts available across CLI invocations.
402
-
403
- ### Sessions and actors
404
-
405
- Set `STQL_SESSION` to select a named session and `STQL_ACTOR` to select an
406
- attached actor. A session is a shared workspace: attached actors reuse its
407
- connection, handles, aliases, cache, and state version. Plans and staged
408
- transactions remain owned by the actor that created them.
409
-
410
- Callers that omit `actor` keep the legacy behavior where the actor ID is the
411
- session name.
412
-
413
- ### Result lifetime and limits
414
-
415
- SQLite result rows are materialized locally for durable access. Read cache
416
- entries expire after five minutes, and materialized handles expire after 24
417
- hours. Expired results and plans are deleted the next time StateQL opens.
418
-
419
- Queries exceeding 10,000 rows or 16 MiB of serialized row data fail before
420
- persistence. Narrow the `WHERE` clause, add `LIMIT`, or select fewer columns.
421
- These caps bound persisted materialization; the independent deadline bounds
422
- execution time.
423
-
424
- Command history keeps the latest 10,000 entries per session. SQLite cache reuse
425
- also checks the database file signature. PostgreSQL, MySQL, and MongoDB cache
426
- reuse is labeled `ttl_based` and is never authoritative.
427
-
428
- StateQL limits persisted result payloads to 256 MiB by default. When that quota
429
- is reached it removes the oldest unaliased results; aliases remain protected. A
430
- single result that cannot fit fails with `STATE_QUOTA_EXCEEDED`. Configure the
431
- limit with `maxStateBytes` in the library or `--max-state-bytes` in the CLI.
432
- Cache and result retention can be configured with `cacheTtlSeconds` and
433
- `resultTtlSeconds`, or their `--cache-ttl-seconds` and
434
- `--result-ttl-seconds` CLI equivalents.
435
-
436
- `stql doctor` checks SQLite integrity and stored payload shapes without printing
437
- SQL, parameters, or result values. `stql purge` removes expired data by default;
438
- use `results`, `history`, or `all` for explicit session cleanup. On POSIX
439
- systems, StateQL removes group and world access from its state directory,
440
- database, and SQLite sidecar files.
441
-
442
- ### Local filtering
443
-
444
- `filter` evaluates one scalar SQLite predicate against a stored result. It
445
- preserves source order, state metadata, and expiry, and never accesses the
446
- original database.
447
-
448
- Use parameters for values. Subqueries, query-shaping clauses, and
449
- non-allowlisted functions are rejected. Common deterministic functions such as
450
- `lower`, `upper`, `length`, and `coalesce` are supported.
451
-
452
- ## Write safety
453
-
454
- Destructive and unbounded operations require `--allow-destructive` and
455
- `--allow-unbounded`, respectively. The flags are independent.
456
-
457
- `plan` validates and stores a write for later application. A plan persists only
458
- the flags explicitly supplied when it is created; `apply` never adds
459
- authorization.
460
-
461
- Use an idempotency key to protect retryable writes from duplicate execution:
47
+ Use the handle returned by your query in place of the example handle below.
48
+ Give the snapshot a readable alias, then work with the stored data:
462
49
 
463
50
  ```bash
464
- stql exec "UPDATE jobs SET claimed = 1 WHERE id = ?" \
465
- --param 42 \
466
- --idempotency-key claim-job-42
51
+ stql alias set active-users q_k7m2v5x9c3d6f8h4j2n7p5r9tw
52
+ stql rows active-users --offset 0 --limit 10
53
+ stql count active-users
54
+ stql export active-users --output active-users.csv --format csv
55
+ stql filter active-users "email LIKE ?" --param "%@example.com"
467
56
  ```
468
57
 
469
- If a write starts but StateQL cannot safely record its final outcome, it returns
470
- `OUTCOME_UNKNOWN` and blocks automatic replay. Inspect database state before
471
- using `--replay`. Interrupted commits remain fail-closed; stale `committing`
472
- records become `outcome_unknown` after five minutes.
58
+ These commands do not access the original database. `filter` creates a new
59
+ snapshot with its own handle. Repeating a query can reuse a valid cached result;
60
+ use `--cache bypass` when a fresh database read is required.
473
61
 
474
- ### Transactions
62
+ ## Safety and state
475
63
 
476
- Transactions are staged in local state so they survive CLI invocations, then
477
- executed atomically on commit. While a transaction is active, StateQL rejects
478
- database reads, plans, connection changes, and disconnects. Commit or roll back
479
- first.
64
+ - Connections are **read-only by default**. Writes require a read-write connection.
65
+ - Destructive and unbounded SQL/MongoDB operations require independent
66
+ `--allow-destructive` and `--allow-unbounded` approvals, respectively.
67
+ - `plan` stores a write for review; `apply` never adds authorization. Use
68
+ idempotency keys to protect retryable writes from duplicate execution.
69
+ - A write reported as `OUTCOME_UNKNOWN` must be inspected at the database before
70
+ replaying it. Transactions are staged locally, not live interactive SQL
71
+ transactions.
72
+ - Supply credential-bearing URLs through environment variables or a trusted
73
+ library credential resolver, not literal command arguments or profiles.
74
+ - Results and history persist under `STQL_HOME` (or the platform data directory).
75
+ Treat that state as database data; retention and size limits are configurable.
480
76
 
481
- SQLite supports `serializable`. PostgreSQL and MySQL also support
482
- `repeatable read`, `read committed`, and `read uncommitted`. Server reads run
483
- inside database-enforced read-only transactions. MySQL staged transactions
484
- reject DDL because MySQL implicitly commits those statements.
485
- MongoDB transactions use `snapshot` isolation and require a replica set or
486
- sharded deployment; standalone servers do not support them.
77
+ See [write safety](docs/usage.md#write-safety),
78
+ [credentials](docs/usage.md#environment-backed-credentials), and
79
+ [database restrictions](docs/databases.md) before using production data.
487
80
 
488
- ## Batch and pipes
81
+ ## Documentation
489
82
 
490
- `batch` reads a JSON array from a `.json` file or JSONL from a `.jsonl` file.
491
- `pipe` reads JSONL from standard input. Commands run sequentially and stop on
492
- the first error unless `--continue-on-error` is set. Output defaults to one
493
- compact `agent` JSON object per line.
83
+ | Guide | Contents |
84
+ | --- | --- |
85
+ | [Usage](docs/usage.md) | Connections, CLI reference, results, sessions, limits, writes, transactions, batch and pipes |
86
+ | [Database support](docs/databases.md) | SQL dialects, diagnostics, maintenance, native MongoDB and Redis commands |
87
+ | [TypeScript library](docs/library.md) | Connection setup, response handling, cleanup, actors, workspaces, and credential resolution |
88
+ | [SQL command roadmap](SQL_COMMAND_ROADMAP.md) | Development priorities and requirements for extending SQL support |
494
89
 
495
- Pipe commands directly:
90
+ Run `stql --help` for command syntax or `stql capabilities` for capability details.
496
91
 
497
- ```bash
498
- printf '%s\n' \
499
- '{"command":"query","sql":"SELECT id, email FROM users ORDER BY id","as":"users"}' \
500
- '{"command":"filter","handle":"users","where":"email LIKE ?","params":["%@example.com"],"as":"example_users"}' \
501
- '{"command":"rows","handle":"example_users","limit":10}' |
502
- stql pipe
503
- ```
504
-
505
- Or save a JSON array as `commands.json`:
506
-
507
- ```json
508
- [
509
- {
510
- "command": "exec",
511
- "sql": "UPDATE jobs SET claimed = 1 WHERE id = ?",
512
- "params": [42],
513
- "idempotency_key": "claim-job-42"
514
- },
515
- {
516
- "command": "query",
517
- "sql": "SELECT * FROM jobs WHERE id = ?",
518
- "params": [42]
519
- }
520
- ]
521
- ```
92
+ ## TypeScript installation
522
93
 
523
94
  ```bash
524
- stql batch commands.json
525
- ```
526
-
527
- Batch fields use snake case. Supported command names match CLI paths, such as
528
- `filter`, `transaction.begin`, `session.summary`, `alias.set`, `plan`, and
529
- `apply`. Native MongoDB batches use `mongo.query`, `mongo.exec`, or `mongo.plan`
530
- with the command object in `mongo`; the same cache, replay, idempotency, safety,
531
- and timeout fields apply. Database commands may set `timeout_ms`; otherwise they
532
- use the 30-second default.
533
-
534
- ## TypeScript library
535
-
536
- The package exports the same stateful operations for programmatic use. Library
537
- responses retain the full response envelope regardless of the configured CLI
538
- output mode.
539
-
540
- ```ts
541
- import { StateQL } from "@fadhilp/stateql";
542
-
543
- const stateql = StateQL.forActor({
544
- home: "./.stql",
545
- actor: "pi-session-id",
546
- timeoutMs: 30_000,
547
- credentialTimeoutMs: 120_000,
548
- maxResultBytes: 16 * 1024 * 1024,
549
- maxStateBytes: 256 * 1024 * 1024,
550
- });
551
-
552
- const controller = new AbortController();
553
- const response = await stateql.query("SELECT * FROM users", {
554
- signal: controller.signal,
555
- timeoutMs: 5_000,
556
- });
557
-
558
- if (response.ok) {
559
- const handle = (response.data as { result_id: string }).result_id;
560
- await stateql.filter(handle, "email LIKE ?", {
561
- params: ["%@example.com"],
562
- });
563
- }
564
- ```
565
-
566
- Hosts that dispatch batch-shaped commands can attach trusted metadata out of
567
- band. `origin` is audit/source metadata only; it never changes actor membership,
568
- workspace access, or write authorization.
569
-
570
- ```ts
571
- const controller = new AbortController();
572
- await stateql.executeCommand(
573
- { command: "query", sql: "SELECT * FROM users", cache: "bypass" },
574
- { origin: "user", signal: controller.signal },
575
- );
576
-
577
- const userHistory = await stateql.history(50, { origin: "user" });
578
- await stateql.executeCommand(
579
- { command: "history", limit: 50, history_origin: "user" },
580
- { origin: "model" },
581
- );
582
- ```
583
-
584
- Supported origins are `legacy`, `user`, `model`, `system`, and `api`. Existing
585
- direct calls and `executeCommand(command)` calls are recorded as `legacy`.
586
- `history_origin` is only a retrieval filter; putting an `origin` field in a
587
- `BatchCommand` cannot attribute the command. `batch` accepts the same trusted
588
- context as `options.executionContext` for all commands in that batch.
589
-
590
- ### Actor workspaces
591
-
592
- `StateQL.forWorkspace(...)` is a trusted-host primitive that atomically creates
593
- or reopens a durable workspace, attaches the requested actor, and returns a
594
- client bound to that actor:
595
-
596
- ```ts
597
- const stateql = StateQL.forWorkspace({
598
- home: "./.stql",
599
- workspace: "pylon-global",
600
- actor: "pylon-session:abc123",
601
- credentialResolver,
602
- signal,
603
- });
604
- ```
605
-
606
- Repeated opens of the same actor and workspace are idempotent. An actor already
607
- attached elsewhere fails with a `StateQLError` whose code is
608
- `PERMISSION_DENIED`; StateQL never moves or merges it. All actor options,
609
- including limits, credential resolution, cancellation, `home`, and `now`, are
610
- preserved. The workspace name also reserves a same-named actor identity for
611
- legacy compatibility, so workspace and actor identifiers must be globally
612
- collision-free. The returned client is still bound only to `actor`, preserving
613
- plan, transaction, operation, and history ownership.
614
-
615
- `StateQL.forActor(...)` retains its existing behavior: it resolves the actor's
616
- attached session directly from StateQL storage and creates a legacy-compatible
617
- session named after the actor on first use. Use `new StateQL({ session, actor })`
618
- when the session and membership are already known.
619
-
620
- Membership management and `forWorkspace` are library-only host capabilities,
621
- not batch or CLI commands. Existing member-authorized management remains
622
- available through `linkActor(session, actorId)`, `unlinkActor(session, actorId)`,
623
- `listActors(session)`, and `resolveActor(actorId)`. Integrations should ask for
624
- user confirmation before changing membership or the shared connection; a host
625
- calling `forWorkspace` is responsible for authorizing that workspace access.
626
-
627
- ### Harness credential resolution
628
-
629
- Library integrations can resolve environment-variable names, opaque full-URL
630
- credential references, or password-only references through a trusted approval
631
- or secret-storage layer instead of mutating `process.env`:
632
-
633
- Integrations pinned to an older published package should gate setup before
634
- sending `password_ref`:
635
-
636
- ```ts
637
- if ((StateQL.passwordReferenceVersion ?? 0) < 1) {
638
- throw new Error("Installed StateQL does not support password references.");
639
- }
640
- ```
641
-
642
- `passwordReferenceVersion = 1` guarantees the password-only resolver request,
643
- validation, persistence, reconnect, and redaction contract documented below.
644
-
645
- ```ts
646
- import {
647
- CredentialResolutionError,
648
- StateQL,
649
- type CredentialRequest,
650
- } from "@fadhilp/stateql";
651
-
652
- async function resolveCredential(
653
- request: CredentialRequest,
654
- ): Promise<string | undefined> {
655
- const approved = await credentialBroker.request({
656
- reference: request.reference,
657
- source: request.source ?? "secret_env",
658
- actor: request.actorId,
659
- session: request.session.id,
660
- operation: request.operation,
661
- access: request.access,
662
- signal: request.signal,
663
- });
664
-
665
- if (approved.denied) throw new CredentialResolutionError("denied");
666
- return approved.value;
667
- }
668
-
669
- const stateql = StateQL.forActor({
670
- actor: "agent-session-id",
671
- credentialResolver: resolveCredential,
672
- });
673
- ```
674
-
675
- Credential resolution has its own two-minute default deadline
676
- (`credentialTimeoutMs`) and remains cancellable through `request.signal`.
677
- The database-operation timeout begins after a credential is resolved.
678
-
679
- When no custom resolver is configured, StateQL reads only `secret_env`
680
- references from `process.env`; `credential_ref` and `password_ref` never fall
681
- back to the environment. A configured resolver is authoritative for all
682
- sources: returning `undefined` produces `CREDENTIAL_UNAVAILABLE` and never falls
683
- back to the process environment. Resolver requests retain `reference` and
684
- include `source` (`secret_env`, `credential_ref`, or `password_ref`); source may
685
- be omitted only on legacy secret-environment request objects. A `password_ref`
686
- request additionally includes the exact password-free effective `target`.
687
- Resolvers may throw `CredentialResolutionError` with `denied`, `cancelled`,
688
- `timeout`, or `unavailable` to produce controlled, secret-free failures. Unknown
689
- resolver errors are replaced with a generic `CREDENTIAL_RESOLUTION_FAILED`
690
- response.
691
-
692
- StateQL calls the resolver only immediately before database access, after SQL
693
- safety and duplicate checks. Requests contain actor and session identity, the
694
- operation's effective read/write access, an abort signal, and sanitized
695
- connection metadata.
696
-
697
- For `secret_env` and `credential_ref`, returned values must be complete
698
- PostgreSQL, MySQL, MongoDB, or Redis URLs, or explicit `sqlite:` sources. For
699
- `password_ref`, the resolver returns only the password; an explicit empty string
700
- is a resolved password, while `undefined` fails closed. StateQL percent-encodes
701
- and injects only that password into the original target for adapter use, leaving
702
- all nonsecret URL/TLS/CA bytes unchanged. It persists only the original target
703
- and reference. Resolved credentials never enter connection metadata, history,
704
- snapshots, cache keys, responses, or stored errors.
705
-
706
- Harnesses remain responsible for approval policy, binding lifetime, revocation,
707
- and keeping values out of their own logs and model-visible data.
708
-
709
- For writes, credential resolution happens after StateQL atomically reserves the
710
- operation for duplicate protection. A resolution failure keeps a non-executed
711
- `failed` audit record, does not consume the idempotency key, and permits a safe
712
- retry.
713
-
714
- ## Pylon database integration API (0.9.0)
715
-
716
- ### Result identities and aliases
717
-
718
- Every materialized SQL, MongoDB, Redis, table, or derived result keeps its
719
- immutable canonical `q_*` `result_id`. New canonical resource IDs use a
720
- cryptographically random 26-character lowercase base32 suffix; existing
721
- incremental IDs such as `q_121` remain valid and are not rewritten. Results also
722
- receive a random 10-character lowercase base32 `display_alias`.
723
- `ResultData.alias` normally equals that alias. When a batch command supplies
724
- `as`, `alias` remains the caller alias for backward compatibility while
725
- `display_alias` remains canonical. Generated aliases are session-scoped,
726
- allocated atomically with the result, stable on cache reuse, and cannot be
727
- reassigned by `setAlias`; explicit aliases and all old handles continue to
728
- resolve.
729
-
730
- Connections likewise retain canonical `conn_*` IDs with random 26-character
731
- suffixes and receive persistent random 10-character lowercase base32 aliases,
732
- exposed as `alias` and `display_alias` by `connect()` and
733
- `snapshot().connection` (optional in snapshot types for older producers).
734
- Connection aliases are unique within the state store, allocated atomically with
735
- the connection, and backfilled for existing records on startup. They survive
736
- reopening; reconnecting creates a new ID and alias. They are display identities
737
- only, separate from result aliases; internal references and lookups continue to
738
- use canonical connection IDs.
739
-
740
- ### Safe profile updates
741
-
742
- ```ts
743
- updateProfile(name, {
744
- target?: string | null,
745
- secretEnv?: string | null,
746
- credentialRef?: string | null,
747
- passwordRef?: string | null,
748
- readOnly?: boolean,
749
- })
750
- ```
751
-
752
- Omitting all source and password-reference fields keeps the existing source and
753
- adjunct reference. Supplying any source field replaces the source atomically:
754
- exactly one non-null source is required and the other source columns are
755
- cleared. An omitted `passwordRef` is preserved when the existing literal target
756
- is unchanged; changing the target clears it unless the update explicitly supplies
757
- a replacement. Replacing the source with `secretEnv` or `credentialRef` clears
758
- it. An explicit non-null password reference combined with either
759
- reference-backed source is rejected. Direct URLs
760
- with embedded passwords or secret-like query parameters are rejected.
761
- `profile.list/show/update` return only
762
- `{profile,target,secret_env,credential_ref,password_ref,read_only}`. Profile
763
- changes affect subsequent `connect` calls and do not silently mutate an
764
- already-open connection.
765
-
766
- The `password_refs_v1` migration adds nullable `password_ref` columns to both
767
- profiles and connections and enforces that they accompany only literal target
768
- configuration. It rejects incompatible profile schemas/rows instead of dropping
769
- references. Downgrading a state home containing password references is
770
- unsupported: older binaries do not resolve this source and may attempt the
771
- password-free target using ambient/trust authentication; constraint-protected
772
- source replacements may also fail. Use the same or newer StateQL binary, or
773
- explicitly clear all password references before downgrade.
774
-
775
- ### Bounded catalog
776
-
777
- ```ts
778
- listObjects(
779
- { kind?, schema?, search?, offset?, limit? },
780
- { timeoutMs?, signal? },
781
- ) -> { objects, next_offset, supported_kinds }
782
-
783
- describeObject(
784
- { kind, schema?, name, identity? },
785
- { timeoutMs?, signal? },
786
- ) -> { object, definition? }
787
- ```
788
-
789
- SQL/MongoDB offsets are non-negative numbers; limits default to 50 and are at
790
- most 200. Redis `offset` and `next_offset` are opaque numeric SCAN cursor strings;
791
- its limit is a SCAN `COUNT` hint with a hard 200-item response bound. Redis pages
792
- are not snapshots and can be empty or contain duplicates while keys change.
793
- Search is a case-insensitive name substring for SQL/MongoDB and escaped glob
794
- substring matching for Redis. No exact counts are forced.
795
-
796
- Supported kinds are returned on every page: SQLite `table,view,trigger`;
797
- PostgreSQL `table,view,function,trigger,enum`; MySQL
798
- `table,view,function,trigger`; MongoDB `collection,view`; Redis `key`.
799
- PostgreSQL function identities include identity arguments, so overloads remain
800
- distinct. `describeObject` is read-only and requires the structured identity;
801
- legacy `inspect` behavior is unchanged (and intentionally unavailable for Redis).
802
-
803
- ### Reviewed multi-row table edits
804
-
805
- ```ts
806
- planTableUpdates(
807
- Array<{ row_token: string; changes: { set?: object; unset?: string[] } }>,
808
- options?,
809
- ) -> PlanData
810
- ```
811
-
812
- Batches contain 1-100 distinct row identities and at most 256 KiB. All tokens,
813
- connection/state versions, expiries, metadata, editable columns, and values are
814
- validated before one plan is stored; expiry is the earliest token expiry.
815
- `apply(plan_id)` executes all conditional row updates in one SQLite/PostgreSQL/
816
- MySQL transaction and requires every row predicate to match, otherwise all are
817
- rolled back. MongoDB uses one snapshot transaction and rejects deployments that
818
- do not support transactions. Redis and active staged StateQL transactions are
819
- rejected. The existing `planTableUpdate` and `apply` APIs remain supported.
820
- Plans are actor-owned, claimed once, and retained as non-replayable when the
821
- remote commit outcome is uncertain.
822
-
823
- ### Redis native commands
824
-
825
- Redis/Rediss URLs support URL database selection, password or ACL username,
826
- and TLS (`rediss`). Credential-bearing URLs must come from `secretEnv` or
827
- `credentialRef`. Native methods accept `{command: string, args?: string[]}`:
828
-
829
- - `redisQuery`: `GET`, `MGET`, `TYPE`, `EXISTS`, `TTL`, `PTTL`, `HGET`, `HMGET`,
830
- bounded `LRANGE`, and bounded `SCAN`/`HSCAN`/`SSCAN`/`ZSCAN`.
831
- - `redisExec` and `redisPlan`: one-key `SET`, `DEL`, `HSET`, `HDEL`, `LPUSH`,
832
- `RPUSH`, `SADD`, `SREM`, `ZADD`, or `ZREM` mutation.
833
- - `describeObject({kind:"key",name})`: bounded string/hash/list/set/zset value
834
- inspection with TTL and continuation metadata where applicable.
835
-
836
- Arguments are UTF-8 strings, at most 100 values/256 KiB; materialized replies are
837
- at most 1 MiB. `KEYS`, scripts, modules, pub/sub, blocking commands, admin/flush,
838
- and arbitrary commands are rejected. Key discovery always uses SCAN. A Redis
839
- plan snapshots one bounded key and `apply` uses an isolated `WATCH` + one-command
840
- `MULTI/EXEC`; a pre-apply content or expiry change returns `ROW_CONFLICT` and is
841
- never retried automatically. Direct `redisExec` has Redis single-command
842
- atomicity only. Redis has no SQL rollback or StateQL staged transaction support;
843
- a lost write/EXEC reply is reported as `OUTCOME_UNKNOWN` and remains blocked.
844
-
845
- ### Lean history
846
-
847
- ```ts
848
- history(limit?, {
849
- origin?,
850
- category?: "statement" | "introspection" | "management",
851
- internal?: boolean,
852
- offset?: number,
853
- })
854
- ```
855
-
856
- `category` and trusted-host `internal` filters are applied in SQLite before
857
- `ORDER BY`, `LIMIT`, and `OFFSET`, so introspection cannot starve statement
858
- history. `CommandExecutionContext.internal` is trusted host metadata and cannot
859
- be supplied inside a batch command. Existing calls and origin filtering remain
860
- compatible; old rows are classified from their command name and migrate as `internal: false`.
861
-
862
- The synchronous, non-mutating snapshot bridge accepts the same classification
863
- filters without entering the command queue or writing a history row:
864
-
865
- ```ts
866
- stateql.snapshot({
867
- historyLimit: 50,
868
- historyCategory: "statement",
869
- historyInternal: false,
870
- });
95
+ npm install @fadhilp/stateql
871
96
  ```
872
97
 
873
- Both snapshot filters are applied by the store before `historyLimit`. Calling
874
- `snapshot()` with no options preserves the legacy 50-entry CLI snapshot.
98
+ The package exports `StateQL` and public TypeScript types. Start with the
99
+ [connected, cleanup-safe example](docs/library.md#getting-started).