@fadhilp/stateql 0.4.1 → 0.4.4

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,20 +1,28 @@
1
1
  # StateQL
2
2
 
3
- StateQL is a stateful database CLI for AI agents and automation. It provides a
4
- safe interface for querying, changing, and inspecting databases while keeping
5
- results reusable and operations traceable across commands.
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, and MySQL databases while keeping results reusable and
6
+ operations traceable across commands.
7
+
8
+ StateQL is built around durable handles:
9
+
10
+ 1. Run a query and receive a result handle such as `q_1`.
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.
6
14
 
7
15
  Requires Node.js 22.5 or newer.
8
16
 
9
17
  ## Quick start
10
18
 
19
+ Install the CLI:
20
+
11
21
  ```bash
12
22
  npm install -g @fadhilp/stateql
13
23
  ```
14
24
 
15
- Connect to an existing SQLite database, then run a filtered, parameterized
16
- query. Parameters keep values separate from SQL; `ORDER BY` makes paging
17
- stable, while `LIMIT` bounds work at the database.
25
+ Connect to an existing SQLite database and run a bounded, parameterized query:
18
26
 
19
27
  ```bash
20
28
  export STQL_SESSION=audit
@@ -27,14 +35,16 @@ stql query \
27
35
  --param 2026-01-01
28
36
  ```
29
37
 
30
- Example output:
38
+ Parameters keep values separate from SQL. `ORDER BY` makes paging stable, and
39
+ `LIMIT` bounds work at the database. The default `agent` output is compact,
40
+ one-line JSON:
31
41
 
32
42
  ```json
33
43
  {"ok":true,"handle":"q_1","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}
34
44
  ```
35
45
 
36
- `q_1` is a durable result handle. Filter its stored snapshot without querying
37
- the original database; the result becomes another durable handle:
46
+ `q_1` is a durable snapshot. Filter it locally without accessing the original
47
+ database:
38
48
 
39
49
  ```bash
40
50
  stql filter q_1 "email LIKE ?" --param "%@example.com"
@@ -44,8 +54,8 @@ stql filter q_1 "email LIKE ?" --param "%@example.com"
44
54
  {"ok":true,"handle":"q_2","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}
45
55
  ```
46
56
 
47
- Give the derived result a readable alias, page through it, inspect its count,
48
- or export it—all without rerunning SQL:
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:
49
59
 
50
60
  ```bash
51
61
  stql alias set example-users q_2
@@ -62,7 +72,22 @@ Example first page:
62
72
  ```
63
73
 
64
74
  Running the same normalized query with the same parameters reuses `q_1` while
65
- its cache is valid. Use `--cache bypass` when a fresh read is required.
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`, or
80
+ `--profile`.
81
+
82
+ ```bash
83
+ stql connect <sqlite-path|postgres-url|mysql-url> [--name NAME] [--read-write]
84
+ stql connect --env ENV [--name NAME] [--read-write]
85
+ stql connect --profile NAME
86
+ stql disconnect
87
+ stql status
88
+ ```
89
+
90
+ ### Environment-backed credentials
66
91
 
67
92
  PostgreSQL and MySQL credentials should come from environment variables. The
68
93
  variable must contain the complete connection URL, not only its password.
@@ -79,16 +104,45 @@ export SQLITE_DATABASE='sqlite:./app.sqlite'
79
104
  stql connect --env SQLITE_DATABASE --name local --read-only
80
105
  ```
81
106
 
82
- A connection accepts exactly one direct target, `--env`, or `--profile` source.
83
- MySQL uses positional `?` parameters. MariaDB compatibility is not currently
84
- claimed.
107
+ StateQL stores no PostgreSQL or MySQL password. Credential-bearing URLs must be
108
+ supplied through `--env`. SQLite paths remain persisted as connection metadata.
109
+
110
+ ### Local profiles
111
+
112
+ Profiles store connection targets, read-only policy, and environment-variable
113
+ names. Credential values are never stored. Profiles persist under `STQL_HOME`
114
+ with other StateQL metadata.
115
+
116
+ ```bash
117
+ stql profile add local ./app.sqlite --read-write
118
+ stql profile add production --env PROD_DATABASE_URL --read-only
119
+ stql profile list
120
+ stql profile show production
121
+ stql connect local
122
+ stql connect --profile production
123
+ ```
124
+
125
+ A bare connection target matching a profile name resolves to that profile;
126
+ otherwise it remains a path or database URL.
127
+
128
+ ### Driver notes
85
129
 
86
- ## Commands
130
+ - **SQLite:** use a filesystem path for direct connections or `sqlite:` for an
131
+ environment-backed path.
132
+ - **PostgreSQL:** StateQL preserves strict TLS verification by normalizing
133
+ `sslmode=prefer`, `require`, and `verify-ca` to `verify-full` before opening
134
+ the adapter. Use `sslmode=verify-full` explicitly for clarity. Setting
135
+ `uselibpqcompat=true` opts out and keeps libpq-compatible SSL semantics.
136
+ - **MySQL:** uses positional `?` parameters. MariaDB compatibility is not
137
+ currently claimed.
138
+
139
+ ## CLI reference
87
140
 
88
141
  ```text
89
142
  stql connect <sqlite-path|postgres-url|mysql-url> [--name NAME] [--read-write]
90
143
  stql connect --env ENV [--name NAME] [--read-write]
91
144
  stql connect --profile NAME
145
+ stql disconnect
92
146
  stql status
93
147
  stql profile add|list|show|remove
94
148
  stql session start|list|show|summary|close
@@ -98,6 +152,7 @@ stql exec <sql> [--params JSON | --param VALUE...] [--idempotency-key KEY] [--re
98
152
  [--allow-unbounded] [--allow-destructive]
99
153
  stql show|count|columns <result-handle>
100
154
  stql rows <result-handle> [--offset N] [--limit N]
155
+ stql alias set <name> <result-handle>
101
156
  stql export <result-handle> --output FILE [--format json|jsonl|csv]
102
157
  stql inspect schema|table|columns|indexes|constraints [table]
103
158
  stql transaction begin|status|commit|rollback [--isolation LEVEL]
@@ -110,56 +165,128 @@ stql batch [commands.json|commands.jsonl|-] [--continue-on-error]
110
165
  stql pipe [--continue-on-error]
111
166
  ```
112
167
 
113
- Database commands accept `--timeout-ms N`; default is 30,000 ms. `Ctrl+C`
114
- cancels active work. SQLite runs in a killable child process so long synchronous
115
- statements cannot block StateQL's event loop. PostgreSQL uses server-side
116
- `statement_timeout` plus client deadlines. MySQL deadlines destroy the active
117
- connection. A timed-out write may return `OUTCOME_UNKNOWN` when commit status
118
- cannot be proven.
168
+ ### SQL parameters
119
169
 
120
- ## Output modes
170
+ For shell-safe positional parameters, repeat `--param`. JSON scalars become
171
+ their native types; other values remain strings.
121
172
 
122
- CLI output defaults to compact, one-line `agent` JSON. Successes flatten useful
123
- data and expose the primary durable ID as `handle`; errors retain their complete
124
- error object. Empty warnings and tracing metadata are omitted.
173
+ ```powershell
174
+ stql exec "INSERT INTO users (name, status) VALUES (?, ?)" `
175
+ --param Ada --param trial
176
+ ```
177
+
178
+ Use `--params JSON` for a JSON array or named parameters. Use
179
+ `--params-file FILE` when JSON is awkward to quote; `--params-file -` reads
180
+ JSON from standard input.
181
+
182
+ ### Output modes
183
+
184
+ CLI output defaults to compact, one-line `agent` JSON. Successful responses
185
+ flatten useful data and expose the primary durable ID as `handle`. Errors retain
186
+ their complete error object. Empty warnings and tracing metadata are omitted.
125
187
 
126
188
  ```json
127
189
  {"ok":false,"error":{"code":"UNBOUNDED_MUTATION","message":"Mutation has no WHERE clause.","retryable":false,"executed":false,"override_flag":"--allow-unbounded"}}
128
190
  ```
129
191
 
130
- Use `--output json` for the original pretty, verbose envelope, or
131
- `--output jsonl` for that envelope on one line. `--output text` prints a short
132
- human status; `--output silent` prints only a successful handle. Set
133
- `STQL_OUTPUT` to choose a mode globally. For `export`, `--output` names the
134
- file, so use `STQL_OUTPUT` to choose its response mode. Library responses keep
135
- the full envelope regardless of CLI mode.
192
+ Other modes are:
136
193
 
137
- For shell-safe positional parameters, repeat `--param`. JSON scalars become
138
- their native types; other values remain strings.
194
+ - `--output json`: original pretty, verbose envelope.
195
+ - `--output jsonl`: verbose envelope on one line.
196
+ - `--output text`: short human-readable status.
197
+ - `--output silent`: only a successful handle.
139
198
 
140
- ```powershell
141
- stql exec "INSERT INTO users (name, status) VALUES (?, ?)" `
142
- --param Ada --param trial
143
- ```
199
+ Set `STQL_OUTPUT` to choose a mode globally. For `export`, `--output` names the
200
+ file, so use `STQL_OUTPUT` to choose the command's response mode. Library
201
+ responses always keep the full envelope.
202
+
203
+ ### Deadlines and cancellation
204
+
205
+ Database commands accept `--timeout-ms N`; the default is 30,000 ms. `Ctrl+C`
206
+ cancels active work.
207
+
208
+ - SQLite runs in a killable child process so long synchronous statements cannot
209
+ block StateQL's event loop.
210
+ - PostgreSQL combines server-side `statement_timeout` with client deadlines.
211
+ - MySQL deadlines destroy the active connection.
212
+
213
+ A timed-out write may return `OUTCOME_UNKNOWN` when its commit status cannot be
214
+ proven.
215
+
216
+ ## Durable state and result reuse
217
+
218
+ State metadata lives under `STQL_HOME`, or the platform data directory when
219
+ unset. StateQL keeps connections, sessions, handles, aliases, cache entries,
220
+ plans, transactions, history, and receipts available across CLI invocations.
221
+
222
+ ### Sessions and actors
223
+
224
+ Set `STQL_SESSION` to select a named session and `STQL_ACTOR` to select an
225
+ attached actor. A session is a shared workspace: attached actors reuse its
226
+ connection, handles, aliases, cache, and state version. Plans and staged
227
+ transactions remain owned by the actor that created them.
228
+
229
+ Callers that omit `actor` keep the legacy behavior where the actor ID is the
230
+ session name.
144
231
 
145
- Use `--params-file FILE` for arrays or named parameters that are awkward to
146
- quote. `--params-file -` reads JSON from standard input.
232
+ ### Result lifetime and limits
147
233
 
148
- ## Local profiles
234
+ SQLite result rows are materialized locally for durable access. Read cache
235
+ entries expire after five minutes, and materialized handles expire after 24
236
+ hours. Expired results and plans are deleted the next time StateQL opens.
149
237
 
150
- Profiles persist under `STQL_HOME` with other StateQL metadata.
238
+ Queries exceeding 10,000 rows or 16 MiB of serialized row data fail before
239
+ persistence. Narrow the `WHERE` clause, add `LIMIT`, or select fewer columns.
240
+ These caps bound persisted materialization; the independent deadline bounds
241
+ execution time.
242
+
243
+ Command history keeps the latest 10,000 entries per session. SQLite cache reuse
244
+ also checks the database file signature. PostgreSQL and MySQL cache reuse is
245
+ labeled `ttl_based` and is never authoritative.
246
+
247
+ ### Local filtering
248
+
249
+ `filter` evaluates one scalar SQLite predicate against a stored result. It
250
+ preserves source order, state metadata, and expiry, and never accesses the
251
+ original database.
252
+
253
+ Use parameters for values. Subqueries, query-shaping clauses, and
254
+ non-allowlisted functions are rejected. Common deterministic functions such as
255
+ `lower`, `upper`, `length`, and `coalesce` are supported.
256
+
257
+ ## Write safety
258
+
259
+ Destructive and unbounded operations require `--allow-destructive` and
260
+ `--allow-unbounded`, respectively. The flags are independent.
261
+
262
+ `plan` validates and stores a write for later application. A plan persists only
263
+ the flags explicitly supplied when it is created; `apply` never adds
264
+ authorization.
265
+
266
+ Use an idempotency key to protect retryable writes from duplicate execution:
151
267
 
152
268
  ```bash
153
- stql profile add local ./app.sqlite --read-write
154
- stql profile add production --env PROD_DATABASE_URL --read-only
155
- stql profile list
156
- stql connect local
157
- stql connect --profile production
269
+ stql exec "UPDATE jobs SET claimed = 1 WHERE id = ?" \
270
+ --param 42 \
271
+ --idempotency-key claim-job-42
158
272
  ```
159
273
 
160
- A bare connection target matching a profile name resolves to that profile;
161
- otherwise it remains a path or database URL. Profiles store targets, read-only
162
- policy, and environment-variable names. Credential values are never stored.
274
+ If a write starts but StateQL cannot safely record its final outcome, it returns
275
+ `OUTCOME_UNKNOWN` and blocks automatic replay. Inspect database state before
276
+ using `--replay`. Interrupted commits remain fail-closed; stale `committing`
277
+ records become `outcome_unknown` after five minutes.
278
+
279
+ ### Transactions
280
+
281
+ Transactions are staged in local state so they survive CLI invocations, then
282
+ executed atomically on commit. While a transaction is active, StateQL rejects
283
+ database reads, plans, connection changes, and disconnects. Commit or roll back
284
+ first.
285
+
286
+ SQLite supports `serializable`. PostgreSQL and MySQL also support
287
+ `repeatable read`, `read committed`, and `read uncommitted`. Server reads run
288
+ inside database-enforced read-only transactions. MySQL staged transactions
289
+ reject DDL because MySQL implicitly commits those statements.
163
290
 
164
291
  ## Batch and pipes
165
292
 
@@ -168,6 +295,8 @@ policy, and environment-variable names. Credential values are never stored.
168
295
  the first error unless `--continue-on-error` is set. Output defaults to one
169
296
  compact `agent` JSON object per line.
170
297
 
298
+ Pipe commands directly:
299
+
171
300
  ```bash
172
301
  printf '%s\n' \
173
302
  '{"command":"query","sql":"SELECT id, email FROM users ORDER BY id","as":"users"}' \
@@ -176,6 +305,8 @@ printf '%s\n' \
176
305
  stql pipe
177
306
  ```
178
307
 
308
+ Or save a JSON array as `commands.json`:
309
+
179
310
  ```json
180
311
  [
181
312
  {
@@ -192,55 +323,21 @@ printf '%s\n' \
192
323
  ]
193
324
  ```
194
325
 
195
- Run the array with `stql batch commands.json`. Batch fields use snake case;
196
- supported command names match CLI paths, such as `filter`,
197
- `transaction.begin`, `session.summary`, `alias.set`, `plan`, and `apply`.
198
- Batch filters use `where` for the predicate and may assign the derived result
199
- with `as`. Database commands may set `timeout_ms`; otherwise they use the
200
- 30-second default.
201
-
202
- State metadata lives under `STQL_HOME`, or the platform data directory when
203
- unset. Set `STQL_SESSION` to select a named session and `STQL_ACTOR` to select
204
- an attached actor for CLI invocations. A session is a shared workspace:
205
- attached actors reuse its connection, handles, aliases, cache, and
206
- state version, while plans and staged transactions remain owned by their
207
- creating actor. Callers that omit `actor` keep the legacy behavior where the
208
- actor ID is the session name.
209
-
210
- Read cache entries expire after five minutes; materialized handles expire after
211
- 24 hours. Expired results and plans are deleted when StateQL next opens. Queries
212
- exceeding 10,000 rows or 16 MiB of serialized row data fail before persistence;
213
- add a narrower `WHERE` clause, `LIMIT`, or smaller column selection. These caps
214
- bound persisted materialization, while the independent deadline bounds execution
215
- time. Command history keeps the latest 10,000 entries per session. SQLite cache reuse also checks
216
- the database file signature; PostgreSQL and MySQL reuse is labeled `ttl_based`,
217
- never authoritative. Transactions are staged in local state so they survive CLI
218
- invocations, then executed atomically on commit. Database reads, plans,
219
- connection changes, and disconnects are rejected while a transaction is active;
220
- commit or roll back first. SQLite supports `serializable`;
221
- PostgreSQL and MySQL also support `repeatable read`, `read committed`, and
222
- `read uncommitted`. Server reads run inside database-enforced read-only
223
- transactions. MySQL staged transactions reject DDL because MySQL implicitly
224
- commits those statements.
326
+ ```bash
327
+ stql batch commands.json
328
+ ```
225
329
 
226
- StateQL stores no PostgreSQL or MySQL password. Credential-bearing URLs must be
227
- supplied through `--env`. SQLite result rows are materialized locally for
228
- durable access.
229
- `filter` evaluates one scalar SQLite predicate against those stored rows, keeps
230
- source order, state metadata, and expiry, and never accesses the original
231
- database. Use parameters for values. Subqueries, query-shaping clauses, and
232
- non-allowlisted functions are rejected; common deterministic functions such as
233
- `lower`, `upper`, `length`, and `coalesce` are supported.
330
+ Batch fields use snake case. Supported command names match CLI paths, such as
331
+ `filter`, `transaction.begin`, `session.summary`, `alias.set`, `plan`, and
332
+ `apply`. Batch filters use `where` for the predicate and may assign the derived
333
+ result with `as`. Database commands may set `timeout_ms`; otherwise they use the
334
+ 30-second default.
234
335
 
235
- Destructive and unbounded operations require their respective flags
236
- independently. Plans persist only flags explicitly supplied when the plan is
237
- created; `apply` never adds authorization. If a database write starts but its
238
- final outcome cannot be recorded safely, StateQL returns `OUTCOME_UNKNOWN` and
239
- blocks automatic replay. Inspect database state before using `--replay`.
240
- Interrupted commits remain fail-closed; stale `committing` records become
241
- `outcome_unknown` after five minutes.
336
+ ## TypeScript library
242
337
 
243
- ## Library
338
+ The package exports the same stateful operations for programmatic use. Library
339
+ responses retain the full response envelope regardless of the configured CLI
340
+ output mode.
244
341
 
245
342
  ```ts
246
343
  import { StateQL } from "@fadhilp/stateql";
@@ -251,11 +348,13 @@ const stateql = StateQL.forActor({
251
348
  timeoutMs: 30_000,
252
349
  maxResultBytes: 16 * 1024 * 1024,
253
350
  });
351
+
254
352
  const controller = new AbortController();
255
353
  const response = await stateql.query("SELECT * FROM users", {
256
354
  signal: controller.signal,
257
355
  timeoutMs: 5_000,
258
356
  });
357
+
259
358
  if (response.ok) {
260
359
  const handle = (response.data as { result_id: string }).result_id;
261
360
  await stateql.filter(handle, "email LIKE ?", {
@@ -264,6 +363,19 @@ if (response.ok) {
264
363
  }
265
364
  ```
266
365
 
366
+ ### Actor workspaces
367
+
368
+ `StateQL.forActor(...)` resolves the actor's attached session directly from
369
+ StateQL storage, avoiding a duplicate actor-to-session mapping in integrations.
370
+ On first use, it creates a legacy-compatible session named after the actor. Use
371
+ `new StateQL({ session, actor })` when the session is already known.
372
+
373
+ Membership is managed only through the library API, not batch commands:
374
+ `linkActor(session, actorId)`, `unlinkActor(session, actorId)`,
375
+ `listActors(session)`, and `resolveActor(actorId)`. An existing member must link
376
+ an actor before that actor opens an existing workspace. Integrations should ask
377
+ for user confirmation before changing membership or the shared connection.
378
+
267
379
  ### Harness credential resolution
268
380
 
269
381
  Library integrations can resolve a profile's credential reference through a
@@ -287,6 +399,7 @@ async function resolveCredential(
287
399
  access: request.access,
288
400
  signal: request.signal,
289
401
  });
402
+
290
403
  if (approved.denied) throw new CredentialResolutionError("denied");
291
404
  return approved.value;
292
405
  }
@@ -297,39 +410,31 @@ const stateql = StateQL.forActor({
297
410
  });
298
411
  ```
299
412
 
300
- When no custom resolver is configured, StateQL continues to read references
301
- from `process.env`. A configured resolver is authoritative: returning
302
- `undefined` produces `CREDENTIAL_UNAVAILABLE` and never falls back to the
303
- process environment. Resolvers may throw `CredentialResolutionError` with
304
- `denied`, `cancelled`, `timeout`, or `unavailable` to produce controlled,
305
- secret-free failures. Unknown resolver errors are replaced with a generic
413
+ When no custom resolver is configured, StateQL reads references from
414
+ `process.env`. A configured resolver is authoritative: returning `undefined`
415
+ produces `CREDENTIAL_UNAVAILABLE` and never falls back to the process
416
+ environment. Resolvers may throw `CredentialResolutionError` with `denied`,
417
+ `cancelled`, `timeout`, or `unavailable` to produce controlled, secret-free
418
+ failures. Unknown resolver errors are replaced with a generic
306
419
  `CREDENTIAL_RESOLUTION_FAILED` response.
307
420
 
308
421
  StateQL calls the resolver only immediately before database access, after SQL
309
- safety and duplicate checks. Requests contain actor/session identity, the
422
+ safety and duplicate checks. Requests contain actor and session identity, the
310
423
  operation's effective read/write access, an abort signal, and sanitized
311
- connection metadata. Returned values must be complete PostgreSQL/MySQL URLs or
312
- explicit `sqlite:` sources. StateQL validates the source and its stored driver
313
- before adapter construction, and normalizes SQLite paths. Credential-bearing
314
- PostgreSQL and MySQL URLs are redacted before connection
315
- metadata is persisted and never enter history, snapshots, cache keys, or
316
- responses. SQLite paths remain persisted connection metadata, as they are for
317
- direct SQLite connections. Harnesses remain responsible for approval policy,
318
- binding lifetime, revocation, and keeping values out of their own logs and
319
- model-visible data.
424
+ connection metadata.
425
+
426
+ Returned values must be complete PostgreSQL or MySQL URLs, or explicit
427
+ `sqlite:` sources. StateQL validates the source and its stored driver before
428
+ adapter construction and normalizes SQLite paths. Credential-bearing
429
+ PostgreSQL and MySQL URLs are redacted before connection metadata is persisted
430
+ and never enter history, snapshots, cache keys, or responses. SQLite paths
431
+ remain persisted connection metadata, as they are for direct SQLite
432
+ connections.
433
+
434
+ Harnesses remain responsible for approval policy, binding lifetime, revocation,
435
+ and keeping values out of their own logs and model-visible data.
320
436
 
321
437
  For writes, credential resolution happens after StateQL atomically reserves the
322
438
  operation for duplicate protection. A resolution failure keeps a non-executed
323
439
  `failed` audit record, does not consume the idempotency key, and permits a safe
324
440
  retry.
325
-
326
- `StateQL.forActor(...)` resolves the actor's attached session directly from
327
- StateQL storage, avoiding a duplicate actor-to-session mapping in integrations.
328
- On first use, it creates a legacy-compatible session named after the actor.
329
- Use `new StateQL({ session, actor })` when the session is already known.
330
-
331
- Membership is managed only through the library API, not batch commands:
332
- `linkActor(session, actorId)`, `unlinkActor(session, actorId)`,
333
- `listActors(session)`, and `resolveActor(actorId)`. An existing member must link
334
- an actor before that actor opens an existing workspace. Integrations should ask
335
- for user confirmation before changing membership or the shared connection.
@@ -37,3 +37,4 @@ export declare function createAdapterContext(timeoutMs: number, signal?: AbortSi
37
37
  export declare function createAdapter(connection: ConnectionRecord, context: AdapterContext, input: {
38
38
  source: string;
39
39
  }): Promise<Adapter>;
40
+ export declare function normalizePostgresConnectionString(source: string): string;
@@ -182,6 +182,23 @@ class SQLiteAdapter {
182
182
  this.pending.clear();
183
183
  }
184
184
  }
185
+ const STRICT_POSTGRES_SSL_MODE_ALIASES = new Set(["prefer", "require", "verify-ca"]);
186
+ export function normalizePostgresConnectionString(source) {
187
+ try {
188
+ const url = new URL(source);
189
+ const parameters = [...url.searchParams.entries()];
190
+ const libpqCompat = parameters.filter(([key]) => key === "uselibpqcompat").at(-1)?.[1];
191
+ const sslMode = parameters.filter(([key]) => key === "sslmode").at(-1)?.[1];
192
+ if (libpqCompat === "true" || !sslMode || !STRICT_POSTGRES_SSL_MODE_ALIASES.has(sslMode))
193
+ return source;
194
+ url.searchParams.delete("sslmode");
195
+ url.searchParams.append("sslmode", "verify-full");
196
+ return url.toString();
197
+ }
198
+ catch {
199
+ return source;
200
+ }
201
+ }
185
202
  class PostgresAdapter {
186
203
  readOnly;
187
204
  context;
@@ -194,7 +211,7 @@ class PostgresAdapter {
194
211
  this.context = context;
195
212
  const timeout = Math.min(2_147_483_647, remainingMilliseconds(context));
196
213
  this.client = new Client({
197
- connectionString: source,
214
+ connectionString: normalizePostgresConnectionString(source),
198
215
  connectionTimeoutMillis: timeout,
199
216
  statement_timeout: timeout,
200
217
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fadhilp/stateql",
3
- "version": "0.4.1",
3
+ "version": "0.4.4",
4
4
  "description": "Stateful, agent-oriented database CLI for safe result reuse",
5
5
  "repository": {
6
6
  "type": "git",