@ultimat3/db 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ // Compile-time pins for this package's disposable resources. Source, not a `.test.ts`, on
2
+ // purpose: `tsconfig.json` excludes `src/**/*.test.ts`, so `tsc -b` never reads a test file and a
3
+ // type-level assertion written there can never fail. This module emits nothing and exports
4
+ // nothing anybody imports — a regression is a build error, the only enforcement that counts
5
+ // (axiom 3). `DbConnection` and `Turn` both went through a session where `release()`/`close()`
6
+ // left a resource cached or unreturned; the fix each time was RAII (`Disposable` + `using`), and
7
+ // this pin is what stops a future edit from quietly dropping `Disposable` off either interface —
8
+ // the one place a regression here would otherwise surface is a leaked connection under load, not
9
+ // a red test.
10
+
11
+ import type { DbConnection } from './client';
12
+ import type { Turn } from './pglite-turns';
13
+
14
+ /** Fails to compile when `T` is anything but `true`. The whole mechanism. */
15
+ type Assert<T extends true> = T;
16
+
17
+ /**
18
+ * The pinned handle `client.reserve()` returns must stay `Disposable`, or `using connection =
19
+ * await client.reserve()` in `transaction.ts` / `readonly-query.ts` stops compiling as a
20
+ * scope-bound resource and degrades silently back into a hand-rolled `try`/`finally`.
21
+ */
22
+ export type _DbConnectionIsDisposable = Assert<[DbConnection] extends [Disposable] ? true : false>;
23
+
24
+ /**
25
+ * PGlite's single-session turn must stay `Disposable` too, or `TurnQueue.run()`'s `using turn =
26
+ * await this.take()` in `pglite-turns.ts` loses the same guarantee — the connection never gets
27
+ * queued back to the next waiter on a throw.
28
+ */
29
+ export type _TurnIsDisposable = Assert<[Turn] extends [Disposable] ? true : false>;
package/src/readonly.ts DELETED
@@ -1,111 +0,0 @@
1
- // Single responsibility: a `DbClient` that cannot mutate — for any caller that cannot open its
2
- // own transaction. An LLM with a Postgres connection and no gate is an outage waiting to be
3
- // prompted into existence. (MCP's `db.query` reaches past this for the stronger `readOnlyQuery`:
4
- // a SELECT-only role inside `BEGIN READ ONLY`, where Postgres refuses the write, not a regex.)
5
- // Detection strips comments and string literals first, because
6
- // `/* x */ update ...` and `WITH t AS (INSERT ...) SELECT` are exactly how a naive check is beaten.
7
-
8
- import type { DbClient } from './client';
9
- import { readonlyViolation } from './errors';
10
- import { raw, type SqlFragment } from './sql';
11
-
12
- const MUTATING = [
13
- 'insert',
14
- 'update',
15
- 'delete',
16
- 'truncate',
17
- 'drop',
18
- 'alter',
19
- 'create',
20
- 'grant',
21
- 'revoke',
22
- 'copy',
23
- 'set',
24
- 'call',
25
- 'do',
26
- 'refresh',
27
- 'vacuum',
28
- 'reindex',
29
- 'cluster',
30
- 'lock',
31
- 'merge',
32
- 'analyze',
33
- 'prepare',
34
- 'execute',
35
- ] as const;
36
-
37
- const MUTATING_PATTERN = new RegExp(`\\b(${MUTATING.join('|')})\\b`, 'i');
38
-
39
- /**
40
- * Blank out anything a keyword could legitimately hide inside: line comments, block comments,
41
- * single-quoted literals, dollar-quoted bodies and quoted identifiers. Blanking (rather than
42
- * deleting) keeps offsets stable so the reported statement still reads correctly.
43
- */
44
- export function stripSqlNoise(text: string): string {
45
- return text
46
- .replace(/\$([A-Za-z_]\w*)?\$[\s\S]*?\$\1?\$/g, ' ')
47
- .replace(/--[^\n]*/g, ' ')
48
- .replace(/\/\*[\s\S]*?\*\//g, ' ')
49
- .replace(/'(?:[^']|'')*'/g, " '' ")
50
- .replace(/"(?:[^"]|"")*"/g, ' "" ');
51
- }
52
-
53
- export interface MutationVerdict {
54
- readonly mutating: boolean;
55
- readonly keyword: string | null;
56
- }
57
-
58
- /**
59
- * Whole-text scan, not a leading-keyword check: multi-statement strings and CTEs that end in a
60
- * writing branch must both be caught, and a false positive here is far cheaper than a false
61
- * negative. `updated_at` and `offset` do not match — `\b` requires a non-word boundary.
62
- */
63
- export function inspectStatement(text: string): MutationVerdict {
64
- const match = MUTATING_PATTERN.exec(stripSqlNoise(text));
65
- if (match === null) return { mutating: false, keyword: null };
66
- return { mutating: true, keyword: match[1] ?? match[0] };
67
- }
68
-
69
- export function assertReadOnly(fragment: SqlFragment): void {
70
- const verdict = inspectStatement(fragment.text);
71
- if (!verdict.mutating) return;
72
- throw readonlyViolation(fragment.text.trim().slice(0, 160), verdict.keyword ?? 'mutating');
73
- }
74
-
75
- export interface ReadOnlyOptions {
76
- /** Also ask Postgres to enforce it. Off only for clients that cannot run `SET TRANSACTION`. */
77
- readonly seal?: boolean | undefined;
78
- }
79
-
80
- /**
81
- * Belt and braces: the regex is the gate, `SET TRANSACTION READ ONLY` is the backstop for
82
- * anything the regex was too clever to catch. Sealing is best-effort — outside a transaction
83
- * block Postgres only warns, and a driver that rejects it must not break every read.
84
- */
85
- export function readOnly(client: DbClient, options: ReadOnlyOptions = {}): DbClient {
86
- let sealed = options.seal === false;
87
-
88
- async function seal(): Promise<void> {
89
- if (sealed) return;
90
- sealed = true;
91
- await client.execute(raw('SET TRANSACTION READ ONLY')).catch(() => undefined);
92
- }
93
-
94
- return {
95
- async query<T>(fragment: SqlFragment): Promise<readonly T[]> {
96
- assertReadOnly(fragment);
97
- await seal();
98
- return client.query<T>(fragment);
99
- },
100
- async one<T>(fragment: SqlFragment): Promise<T | null> {
101
- assertReadOnly(fragment);
102
- await seal();
103
- return client.one<T>(fragment);
104
- },
105
- async execute(fragment: SqlFragment): Promise<number> {
106
- assertReadOnly(fragment);
107
- await seal();
108
- return client.execute(fragment);
109
- },
110
- };
111
- }