@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.
@@ -0,0 +1,225 @@
1
+ # Database support
2
+
3
+ [Home](../README.md) · [Usage](usage.md) · [Database support](databases.md) · [TypeScript library](library.md)
4
+
5
+ - [Driver notes](#driver-notes)
6
+ - [SQL statement support](#sql-statement-support)
7
+ - [Dialect upserts](#dialect-upserts)
8
+ - [PostgreSQL diagnostics and maintenance](#postgresql-diagnostics-and-maintenance)
9
+ - [SQLite and MySQL diagnostics and maintenance](#sqlite-and-mysql-diagnostics-and-maintenance)
10
+ - [Native MongoDB](#native-mongodb)
11
+ - [Native Redis](#native-redis)
12
+
13
+ Connect using the [usage guide](usage.md#connections-and-profiles). All writes
14
+ require a read-write connection; SQL/MongoDB approvals and retry rules are
15
+ covered under [write safety](usage.md#write-safety).
16
+
17
+ ## Driver notes
18
+
19
+ - **SQLite:** use a filesystem path for direct connections or `sqlite:` for an
20
+ environment-backed path.
21
+ - **PostgreSQL:** StateQL preserves strict TLS verification by normalizing
22
+ `sslmode=prefer`, `require`, and `verify-ca` to `verify-full` before opening
23
+ the adapter. Use `sslmode=verify-full` explicitly for clarity. Setting
24
+ `uselibpqcompat=true` opts out and keeps libpq-compatible SSL semantics.
25
+ - **MySQL:** uses positional `?` parameters. MariaDB compatibility is not
26
+ currently claimed.
27
+ - **MongoDB:** supports `mongodb://` and `mongodb+srv://` URLs with an explicit
28
+ database path. SQL methods are rejected; use the native MongoDB methods below.
29
+ - **Redis:** supports `redis://` and TLS-backed `rediss://` URLs. SQL methods and
30
+ legacy `inspect` commands are unavailable; use native commands and catalog
31
+ inspection instead.
32
+
33
+ ## SQL statement support
34
+
35
+ StateQL parses and classifies one `SELECT`, `INSERT`, `REPLACE`, `UPDATE`,
36
+ `DELETE`, `CREATE`, `ALTER`, `DROP`, or `TRUNCATE` statement through
37
+ `node-sql-parser`, subject to dialect support and safety validation. Reads use
38
+ `query`; mutations and DDL use durable `exec`/`plan`/`apply` operations. Raw
39
+ transaction-control SQL is unsupported: use
40
+ [staged transactions](usage.md#transactions), not `BEGIN` or `COMMIT` statements.
41
+ MongoDB and Redis use separate native command APIs.
42
+
43
+ Unrecognized or parser-unsupported forms remain blocked. The
44
+ [SQL command roadmap](../SQL_COMMAND_ROADMAP.md) describes future categories,
45
+ not permission to execute them.
46
+
47
+ ## Dialect upserts
48
+
49
+ PostgreSQL `INSERT ... ON CONFLICT DO NOTHING|UPDATE` and MySQL `INSERT ... ON
50
+ DUPLICATE KEY UPDATE` are structurally validated and recorded with statement
51
+ type `upsert`. Finite `VALUES` and MySQL `INSERT ... SET` sources use normal
52
+ write policy. An update-upsert fed by `SELECT` requires `--allow-unbounded`
53
+ because its candidate row count is not statically bounded. Upserts support direct
54
+ `exec`, `plan`/`apply`, and staged transactions; hidden additional writes are rejected.
55
+
56
+ MySQL `INSERT IGNORE` and SQLite `INSERT OR IGNORE` remain non-overwriting
57
+ inserts. SQLite `INSERT OR REPLACE` retains destructive-operation approval,
58
+ while SQLite modern `ON CONFLICT ... DO UPDATE` and every `MERGE` form remain
59
+ blocked until the parser can expose their complete mutation structure.
60
+
61
+ ## PostgreSQL diagnostics and maintenance
62
+
63
+ Run PostgreSQL plans through `query`:
64
+
65
+ ```bash
66
+ stql query "EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT * FROM jobs WHERE id = 42"
67
+ ```
68
+
69
+ Plain `EXPLAIN` may plan a structurally validated `SELECT`, `INSERT`, `UPDATE`,
70
+ or `DELETE`. Because `EXPLAIN ANALYZE` executes its inner statement, StateQL
71
+ accepts only a validated read-only `SELECT`; `SELECT INTO`, writing CTEs, and
72
+ mutations are rejected. Diagnostics execute inside PostgreSQL `BEGIN READ ONLY`
73
+ and are never reused from cache. `--cache require` therefore returns
74
+ `CACHE_MISS` without executing the diagnostic.
75
+
76
+ Legacy `EXPLAIN [ANALYZE] [VERBOSE] statement` is also supported. Option lists
77
+ allow PostgreSQL boolean diagnostic options plus `FORMAT` and `SERIALIZE`;
78
+ unknown, duplicate, or malformed options fail closed. PostgreSQL's read-only
79
+ transaction protects database writes, but cannot contain external effects from
80
+ user-defined or incorrectly labelled functions.
81
+
82
+ StateQL supports PostgreSQL 14–18. Top-level `VALUES` is a bounded read and
83
+ accepts normal PostgreSQL positional parameters. It is conservatively
84
+ non-cacheable because expressions may be volatile. The following narrow `SHOW`
85
+ allowlist is also available as non-cacheable diagnostics:
86
+ `server_version`, `server_version_num`, `transaction_read_only`,
87
+ `transaction_isolation`, and `default_transaction_isolation`. `SHOW ALL` and
88
+ other settings remain blocked. Syntax accepted by StateQL but introduced by a
89
+ newer PostgreSQL release may be rejected safely by an older server.
90
+
91
+ `VACUUM`, `ANALYZE`, `REINDEX`, and `CLUSTER` are PostgreSQL maintenance writes:
92
+
93
+ ```bash
94
+ stql exec "VACUUM (ANALYZE) public.jobs" --allow-destructive
95
+ stql plan "REINDEX TABLE public.jobs" --allow-destructive
96
+ ```
97
+
98
+ The PostgreSQL 14–18 grammar includes parenthesized `REINDEX CONCURRENTLY`,
99
+ PostgreSQL 16 `BUFFER_USAGE_LIMIT` for `VACUUM`/`ANALYZE`, optional
100
+ `DATABASE`/`SYSTEM` reindex names, and PostgreSQL 18 `ONLY table *` maintenance
101
+ targets. Memory sizes accept an integer number of kilobytes or a quoted
102
+ `B|kB|MB|GB|TB` value. Older servers may reject newer forms after dispatch, so
103
+ StateQL retains conservative unknown-outcome handling.
104
+
105
+ They require a read-write connection and `--allow-destructive`, reject StateQL
106
+ parameters and optimistic row-count preconditions, and run as individually
107
+ tracked autocommit operations. They cannot be staged in a StateQL transaction,
108
+ even where a PostgreSQL variant could run inside a database transaction. A
109
+ timeout, cancellation, or error after dispatch is reported as `OUTCOME_UNKNOWN`;
110
+ inspect database state before replaying it.
111
+
112
+ ## SQLite and MySQL diagnostics and maintenance
113
+
114
+ SQLite supports `EXPLAIN QUERY PLAN` for structurally read-only `SELECT`
115
+ statements. MySQL supports `EXPLAIN SELECT` plus bare `SHOW TABLES`,
116
+ `SHOW COLUMNS FROM table`, and `SHOW INDEX|INDEXES FROM table`. Broader
117
+ `EXPLAIN`, `SHOW`, and write-bearing forms remain blocked.
118
+
119
+ MySQL executable comments (`/*! ... */`) are rejected throughout SQL. Because
120
+ StateQL does not assume a server `sql_mode`, quoting that could expose these
121
+ comments under `ANSI_QUOTES` or `NO_BACKSLASH_ESCAPES` is also rejected.
122
+
123
+ These diagnostics use `query`, work with read-only connections, preserve the
124
+ original statement instead of applying StateQL's limiting SQL wrapper, and are
125
+ never reused from cache. Materialized results still receive StateQL's row and
126
+ byte checks.
127
+
128
+ SQLite also supports bare `VACUUM`, plus `ANALYZE [target]` and
129
+ `REINDEX [target]` with at most one unqualified or double-quoted target:
130
+
131
+ ```bash
132
+ stql exec "ANALYZE jobs" --allow-destructive
133
+ stql exec "REINDEX jobs_created_at_idx" --allow-destructive
134
+ stql exec "VACUUM" --allow-destructive
135
+ ```
136
+
137
+ These commands require a read-write connection, reject parameters, run as
138
+ individually tracked autocommit operations, and cannot be staged. A timeout,
139
+ cancellation, or error after dispatch is reported as `OUTCOME_UNKNOWN`.
140
+ `VACUUM INTO`, schema-qualified targets, paths, `ATTACH`, and arbitrary `PRAGMA`
141
+ remain blocked.
142
+
143
+ MySQL supports one optionally qualified bare or backtick-quoted target for
144
+ `ANALYZE TABLE`, `OPTIMIZE TABLE`, and `CHECK TABLE`:
145
+
146
+ ```bash
147
+ stql exec "ANALYZE TABLE jobs" --allow-destructive
148
+ stql plan "OPTIMIZE TABLE jobs" --allow-destructive
149
+ stql query "CHECK TABLE jobs"
150
+ ```
151
+
152
+ `ANALYZE` and `OPTIMIZE` are durable autocommit writes requiring a read-write
153
+ connection and destructive approval; server-reported error rows become known
154
+ failed operations, while timeout or cancellation after dispatch remains
155
+ `OUTCOME_UNKNOWN`. `CHECK TABLE` is an unwrapped, non-cacheable autocommit read
156
+ that works on read-only connections and retains normal result limits. All three
157
+ reject StateQL parameters, options, multiple targets, and additional
158
+ statements, and none can run during a staged transaction.
159
+ ## Native MongoDB
160
+
161
+ MongoDB commands use official Extended JSON (EJSON), so BSON values survive the
162
+ CLI boundary:
163
+
164
+ ```bash
165
+ stql mongo query '{"operation":"find","collection":"users","filter":{"_id":{"$oid":"507f1f77bcf86cd799439011"}}}'
166
+ stql mongo exec '{"operation":"updateOne","collection":"users","filter":{"_id":{"$oid":"507f1f77bcf86cd799439011"}},"update":{"$set":{"seen_at":{"$date":"2026-01-01T00:00:00Z"}}}}'
167
+ stql mongo plan '{"operation":"deleteMany","collection":"users","filter":{"disabled":true}}' --allow-destructive
168
+ ```
169
+
170
+ The TypeScript equivalents are `mongoQuery(command)`, `mongoExec(command)`, and
171
+ `mongoPlan(command)`. Supported reads are `find` and `aggregate`; writes are
172
+ `insertOne`, `insertMany`, `updateOne`, `updateMany`, `replaceOne`, `deleteOne`,
173
+ and `deleteMany`. Result documents are JSON-safe, order-preserving EJSON: for example,
174
+ ObjectIds and dates appear as `{ "$oid": "..." }` and
175
+ `{ "$date": { "$numberLong": "..." } }`.
176
+
177
+ Empty update, replacement, or delete filters require `--allow-unbounded`;
178
+ deletes and replacements also require `--allow-destructive`. Mongo inspection
179
+ accepts `collections`, `collection`, `columns`, `indexes`, and `constraints`
180
+ (`schema` and `table` remain aliases shared with SQL drivers). MongoDB cache
181
+ confidence is TTL-based: external writes are not detected, so use
182
+ `--cache bypass` for a fresh read.
183
+
184
+ ```ts
185
+ const result = await stateql.mongoQuery({
186
+ operation: "find",
187
+ collection: "users",
188
+ filter: { active: true },
189
+ options: { sort: { _id: 1 }, limit: 50 },
190
+ });
191
+ ```
192
+
193
+ ## Native Redis
194
+
195
+ Redis/Rediss URLs support URL database selection, password or ACL username,
196
+ and TLS (`rediss`). Supply credential-bearing URLs through `--env` or a trusted
197
+ full-URL credential reference. Library/batch callers may instead use a
198
+ [password reference](library.md#password-references) with a password-free target.
199
+ Native commands accept `{command: string, args?: string[]}`:
200
+
201
+ ```bash
202
+ stql redis query '{"command":"GET","args":["app:status"]}'
203
+ stql redis exec '{"command":"SET","args":["app:status","ready"]}' --idempotency-key status-ready
204
+ stql redis plan '{"command":"SET","args":["app:status","paused"]}'
205
+ stql objects key --search app --limit 50
206
+ stql object key app:status
207
+ ```
208
+
209
+ Writes and plans require a read-write connection. The TypeScript methods are:
210
+
211
+ - `redisQuery`: `GET`, `MGET`, `TYPE`, `EXISTS`, `TTL`, `PTTL`, `HGET`, `HMGET`,
212
+ bounded `LRANGE`, and bounded `SCAN`/`HSCAN`/`SSCAN`/`ZSCAN`.
213
+ - `redisExec` and `redisPlan`: one-key `SET`, `DEL`, `HSET`, `HDEL`, `LPUSH`,
214
+ `RPUSH`, `SADD`, `SREM`, `ZADD`, or `ZREM` mutation.
215
+ - `describeObject({kind:"key",name})`: bounded string/hash/list/set/zset value
216
+ inspection with TTL and continuation metadata where applicable.
217
+
218
+ Arguments are UTF-8 strings, at most 100 values/256 KiB; materialized replies are
219
+ at most 1 MiB. `KEYS`, scripts, modules, pub/sub, blocking commands, admin/flush,
220
+ and arbitrary commands are rejected. Key discovery always uses SCAN. A Redis
221
+ plan snapshots one bounded key and `apply` uses an isolated `WATCH` + one-command
222
+ `MULTI/EXEC`; a pre-apply content or expiry change returns `ROW_CONFLICT` and is
223
+ never retried automatically. Direct `redisExec` has Redis single-command
224
+ atomicity only. Redis has no SQL rollback or StateQL staged transaction support;
225
+ a lost write/EXEC reply is reported as `OUTCOME_UNKNOWN` and remains blocked.
@@ -0,0 +1,268 @@
1
+ # TypeScript library
2
+
3
+ [Home](../README.md) · [Usage](usage.md) · [Database support](databases.md) · [TypeScript library](library.md)
4
+
5
+ - [Getting started](#getting-started)
6
+ - [Responses and command context](#responses-and-command-context)
7
+ - [Actor workspaces](#actor-workspaces)
8
+ - [Credential resolution](#credential-resolution)
9
+ - [Safe profile updates](#safe-profile-updates)
10
+
11
+ ## Getting started
12
+
13
+ Install with `npm install @fadhilp/stateql`. This example connects to an existing
14
+ SQLite database containing a `users` table, queries it, and filters the stored
15
+ result. `forActor()` resolves or creates a session; it does not establish a
16
+ database connection for a new session.
17
+
18
+ ```ts
19
+ import { StateQL } from "@fadhilp/stateql";
20
+
21
+ const stateql = StateQL.forActor({
22
+ home: "./.stql",
23
+ actor: "audit",
24
+ });
25
+
26
+ try {
27
+ const connected = await stateql.connect("./app.sqlite", { readOnly: true });
28
+ if (!connected.ok) throw new Error(connected.error.message);
29
+
30
+ const result = await stateql.query(
31
+ "SELECT id, email FROM users WHERE status = ? ORDER BY id LIMIT 50",
32
+ { params: ["active"], timeoutMs: 5_000 },
33
+ );
34
+ if (!result.ok) throw new Error(result.error.message);
35
+
36
+ const filtered = await stateql.filter(result.data.result_id, "email LIKE ?", {
37
+ params: ["%@example.com"],
38
+ });
39
+ if (!filtered.ok) throw new Error(filtered.error.message);
40
+
41
+ console.log(filtered.data.preview);
42
+ } finally {
43
+ stateql.close();
44
+ }
45
+ ```
46
+
47
+ `close()` releases the client's local store; it does not disconnect or delete
48
+ the durable session. Await active commands before closing. Use `disconnect()`
49
+ only when you intend to remove the session's shared active connection.
50
+
51
+ Options include `timeoutMs`, `credentialTimeoutMs`, `maxResultBytes`,
52
+ `maxStateBytes`, `cacheTtlSeconds`, and `resultTtlSeconds`. Database calls also
53
+ accept `signal` for cancellation. See [limits](usage.md#result-lifetime-and-limits)
54
+ and [deadlines](usage.md#deadlines-and-cancellation) for defaults and behavior.
55
+ The same [write safety](usage.md#write-safety) and
56
+ [database restrictions](databases.md) apply to library calls.
57
+
58
+ ## Responses and command context
59
+
60
+ Library responses retain the full response envelope regardless of the CLI
61
+ output mode. Check `response.ok` before reading `response.data`; failures expose
62
+ `response.error`. Queries return typed `ResultData` with `result_id`, `preview`,
63
+ and result metadata. Reuse `result_id` with methods such as `filter`, `rows`,
64
+ `count`, and `exportResult`; the CLI instead presents its primary ID as `handle`.
65
+
66
+ The following snippets assume an open client and an appropriate connection.
67
+
68
+ Hosts that dispatch batch-shaped commands can attach trusted metadata out of
69
+ band. `origin` is audit/source metadata only; it never changes actor membership,
70
+ workspace access, or write authorization.
71
+
72
+ ```ts
73
+ const controller = new AbortController();
74
+ await stateql.executeCommand(
75
+ { command: "query", sql: "SELECT id, email FROM users ORDER BY id LIMIT 50", cache: "bypass" },
76
+ { origin: "user", signal: controller.signal },
77
+ );
78
+
79
+ const userHistory = await stateql.history(50, { origin: "user" });
80
+ await stateql.executeCommand(
81
+ { command: "history", limit: 50, history_origin: "user" },
82
+ { origin: "model" },
83
+ );
84
+ ```
85
+
86
+ Supported origins are `legacy`, `user`, `model`, `system`, and `api`. Existing
87
+ direct calls and `executeCommand(command)` calls are recorded as `legacy`.
88
+ `history_origin` is only a retrieval filter; putting an `origin` field in a
89
+ `BatchCommand` cannot attribute the command. `batch` accepts the same trusted
90
+ context as `options.executionContext` for all commands in that batch.
91
+
92
+ ## Actor workspaces
93
+
94
+ `StateQL.forWorkspace(...)` is a trusted-host primitive that atomically creates
95
+ or reopens a durable workspace, attaches the requested actor, and returns a
96
+ client bound to that actor:
97
+
98
+ ```ts
99
+ const stateql = StateQL.forWorkspace({
100
+ home: "./.stql",
101
+ workspace: "team-audit",
102
+ actor: "audit-worker-1",
103
+ credentialResolver,
104
+ signal,
105
+ });
106
+ ```
107
+
108
+ Repeated opens of the same actor and workspace are idempotent. An actor already
109
+ attached elsewhere fails with a `StateQLError` whose code is
110
+ `PERMISSION_DENIED`; StateQL never moves or merges it. All actor options,
111
+ including limits, credential resolution, cancellation, `home`, and `now`, are
112
+ preserved. The workspace name also reserves a same-named actor identity for
113
+ legacy compatibility, so workspace and actor identifiers must be globally
114
+ collision-free. The returned client is still bound only to `actor`, preserving
115
+ plan, transaction, operation, and history ownership.
116
+
117
+ `StateQL.forActor(...)` retains its existing behavior: it resolves the actor's
118
+ attached session directly from StateQL storage and creates a legacy-compatible
119
+ session named after the actor on first use. Use `new StateQL({ session, actor })`
120
+ when the session and membership are already known.
121
+
122
+ Membership management and `forWorkspace` are library-only host capabilities,
123
+ not batch or CLI commands. Existing member-authorized management remains
124
+ available through `linkActor(session, actorId)`, `unlinkActor(session, actorId)`,
125
+ `listActors(session)`, and `resolveActor(actorId)`. Integrations should ask for
126
+ user confirmation before changing membership or the shared connection; a host
127
+ calling `forWorkspace` is responsible for authorizing that workspace access.
128
+
129
+ ## Credential resolution
130
+
131
+ Library integrations can resolve environment-variable names, opaque full-URL
132
+ credential references, or password-only references through a trusted approval
133
+ or secret-storage layer instead of mutating `process.env`.
134
+
135
+ Integrations pinned to an older published package should gate setup before
136
+ sending `password_ref`:
137
+
138
+ ```ts
139
+ if ((StateQL.passwordReferenceVersion ?? 0) < 1) {
140
+ throw new Error("Installed StateQL does not support password references.");
141
+ }
142
+ ```
143
+
144
+ `passwordReferenceVersion = 1` guarantees the password-only resolver request,
145
+ validation, persistence, reconnect, and redaction contract documented below.
146
+
147
+ In this adapter example, `credentialBroker` is supplied by the host application;
148
+ it is not a StateQL export.
149
+
150
+ ```ts
151
+ import {
152
+ CredentialResolutionError,
153
+ StateQL,
154
+ type CredentialRequest,
155
+ } from "@fadhilp/stateql";
156
+
157
+ async function resolveCredential(
158
+ request: CredentialRequest,
159
+ ): Promise<string | undefined> {
160
+ const approved = await credentialBroker.request({
161
+ reference: request.reference,
162
+ source: request.source ?? "secret_env",
163
+ actor: request.actorId,
164
+ session: request.session.id,
165
+ operation: request.operation,
166
+ access: request.access,
167
+ signal: request.signal,
168
+ });
169
+
170
+ if (approved.denied) throw new CredentialResolutionError("denied");
171
+ return approved.value;
172
+ }
173
+
174
+ const stateql = StateQL.forActor({
175
+ actor: "agent-session-id",
176
+ credentialResolver: resolveCredential,
177
+ });
178
+ ```
179
+
180
+ Credential resolution has its own two-minute default deadline
181
+ (`credentialTimeoutMs`) and remains cancellable through `request.signal`.
182
+ The database-operation timeout begins after a credential is resolved.
183
+
184
+ When no custom resolver is configured, StateQL reads only `secret_env`
185
+ references from `process.env`; `credential_ref` and `password_ref` never fall
186
+ back to the environment. A configured resolver is authoritative for all
187
+ sources: returning `undefined` produces `CREDENTIAL_UNAVAILABLE` and never falls
188
+ back to the process environment. Resolver requests retain `reference` and
189
+ include `source` (`secret_env`, `credential_ref`, or `password_ref`); source may
190
+ be omitted only on legacy secret-environment request objects. A `password_ref`
191
+ request additionally includes the exact password-free effective `target`.
192
+ Resolvers may throw `CredentialResolutionError` with `denied`, `cancelled`,
193
+ `timeout`, or `unavailable` to produce controlled, secret-free failures. Unknown
194
+ resolver errors are replaced with a generic `CREDENTIAL_RESOLUTION_FAILED`
195
+ response.
196
+
197
+ StateQL calls the resolver only immediately before database access, after SQL
198
+ safety and duplicate checks. Requests contain actor and session identity, the
199
+ operation's effective read/write access, an abort signal, and sanitized
200
+ connection metadata.
201
+
202
+ For `secret_env` and `credential_ref`, returned values must be complete
203
+ PostgreSQL, MySQL, MongoDB, or Redis URLs, or explicit `sqlite:` sources. For
204
+ `password_ref`, the resolver returns only the password; an explicit empty string
205
+ is a resolved password, while `undefined` fails closed. StateQL percent-encodes
206
+ and injects only that password into the original target for adapter use, leaving
207
+ all nonsecret URL/TLS/CA bytes unchanged. It persists only the original target
208
+ and reference. Resolved credentials never enter connection metadata, history,
209
+ snapshots, cache keys, responses, or stored errors.
210
+
211
+ Hosts remain responsible for approval policy, binding lifetime, revocation,
212
+ and keeping values out of their own logs and model-visible data.
213
+
214
+ For writes, credential resolution happens after StateQL atomically reserves the
215
+ operation for duplicate protection. A resolution failure keeps a non-executed
216
+ `failed` audit record, does not consume the idempotency key, and permits a safe
217
+ retry.
218
+
219
+ ### Password references
220
+
221
+ Library callers can keep nonsecret endpoint, username, database, TLS, and CA
222
+ options in the literal URL while resolving only its password:
223
+
224
+ ```ts
225
+ await stateql.connect(
226
+ "postgres://app@db.example/app?sslmode=verify-full&sslrootcert=/etc/app-ca.pem",
227
+ { passwordRef: "vault://database/app/password", readOnly: true },
228
+ );
229
+ ```
230
+
231
+ The same field is accepted by `addProfile`, `updateProfile`, and batch
232
+ `connect`/`profile.add`/`profile.update` commands (snake case in batch input).
233
+ Targets with an embedded password or query parameters that override endpoint or
234
+ credential fields are rejected before credential resolution or driver access.
235
+
236
+ ## Safe profile updates
237
+
238
+ `updateProfile(name, options)` accepts optional `target`, `secretEnv`,
239
+ `credentialRef`, `passwordRef`, and `readOnly` fields. The four string fields
240
+ also accept `null` for explicit clearing.
241
+
242
+ ```ts
243
+ const updated = await stateql.updateProfile("production", { readOnly: true });
244
+ if (!updated.ok) throw new Error(updated.error.message);
245
+ ```
246
+
247
+ Omitting all source and password-reference fields keeps the existing source and
248
+ adjunct reference. Supplying any source field replaces the source atomically:
249
+ exactly one non-null source is required and the other source columns are
250
+ cleared. An omitted `passwordRef` is preserved when the existing literal target
251
+ is unchanged; changing the target clears it unless the update explicitly supplies
252
+ a replacement. Replacing the source with `secretEnv` or `credentialRef` clears
253
+ it. An explicit non-null password reference combined with either
254
+ reference-backed source is rejected. Direct URLs
255
+ with embedded passwords or secret-like query parameters are rejected.
256
+ `profile.list/show/update` return only
257
+ `{profile,target,secret_env,credential_ref,password_ref,read_only}`. Profile
258
+ changes affect subsequent `connect` calls and do not silently mutate an
259
+ already-open connection.
260
+
261
+ The `password_refs_v1` migration adds nullable `password_ref` columns to both
262
+ profiles and connections and enforces that they accompany only literal target
263
+ configuration. It rejects incompatible profile schemas/rows instead of dropping
264
+ references. Downgrading a state home containing password references is
265
+ unsupported: older binaries do not resolve this source and may attempt the
266
+ password-free target using ambient/trust authentication; constraint-protected
267
+ source replacements may also fail. Use the same or newer StateQL binary, or
268
+ explicitly clear all password references before downgrade.