@kequtech/tenon-orm 0.2.1 → 0.3.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.
- package/CHANGELOG.md +52 -36
- package/README.md +27 -70
- package/dist/affected-rows.js +6 -6
- package/dist/baseline.js +4 -4
- package/dist/client.js +1 -1
- package/dist/connection.d.ts +15 -16
- package/dist/connection.js +124 -14
- package/dist/database.js +36 -17
- package/dist/driver/types.d.ts +1 -1
- package/dist/driver/types.js +11 -11
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +15 -0
- package/dist/generate.js +7 -7
- package/dist/generation/catalog.d.ts +2 -2
- package/dist/generation/catalog.js +4 -4
- package/dist/generation/schema-summary.d.ts +3 -3
- package/dist/generation/schema-summary.js +11 -11
- package/dist/generation/table-types.js +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -1
- package/dist/inspection/catalog.d.ts +4 -3
- package/dist/inspection/catalog.js +13 -10
- package/dist/inspection/cli.js +3 -3
- package/dist/inspection/connection.d.ts +2 -2
- package/dist/inspection/connection.js +22 -22
- package/dist/inspection/options.d.ts +3 -3
- package/dist/inspection/options.js +9 -1
- package/dist/inspection/protocol.d.ts +7 -0
- package/dist/inspection/protocol.js +45 -0
- package/dist/inspection/result.d.ts +1 -1
- package/dist/inspection/result.js +6 -6
- package/dist/inspection/types.d.ts +10 -10
- package/dist/migrations/connection.d.ts +3 -2
- package/dist/migrations/connection.js +18 -20
- package/dist/migrations/history.d.ts +4 -4
- package/dist/migrations/history.js +5 -5
- package/dist/migrations.js +7 -7
- package/dist/query-options.js +5 -5
- package/dist/query.js +2 -2
- package/dist/transaction.d.ts +2 -2
- package/dist/transaction.js +19 -14
- package/dist/types.d.ts +7 -3
- package/docs/adoption-guide.md +109 -122
- package/docs/agent-guide.md +124 -228
- package/docs/human-guide.md +44 -55
- package/docs/mutations.md +58 -76
- package/docs/query-inspection.md +97 -89
- package/docs/reference.md +264 -319
- package/package.json +4 -4
- package/dist/driver/array.d.ts +0 -5
- package/dist/driver/array.js +0 -70
- package/dist/driver/lifecycle.d.ts +0 -15
- package/dist/driver/lifecycle.js +0 -57
- package/dist/driver/session.d.ts +0 -8
- package/dist/driver/session.js +0 -92
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kequtech/tenon-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A PostgreSQL ORM designed for coding agents, with explicit SQL, typed queries, and migrations",
|
|
@@ -51,11 +51,11 @@
|
|
|
51
51
|
"prepublishOnly": "npm run check",
|
|
52
52
|
"format": "prettier --write .",
|
|
53
53
|
"format:check": "prettier --check .",
|
|
54
|
-
"benchmark": "npm run build && node scripts/benchmark.mjs"
|
|
55
|
-
"test:pilots": "npm run build && node test/pilots/run.mjs"
|
|
54
|
+
"benchmark": "npm run build && node scripts/benchmark.mjs"
|
|
56
55
|
},
|
|
57
56
|
"dependencies": {
|
|
58
|
-
"
|
|
57
|
+
"pg": "8.23.1",
|
|
58
|
+
"@types/pg": "8.23.1"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/node": "^24.19.2",
|
package/dist/driver/array.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type postgres from 'postgres';
|
|
2
|
-
/** Preserve driver scalar parsers and serializers; replace only lossy array parsing. */
|
|
3
|
-
export declare function prepareArrays(sql: postgres.Sql): Promise<void>;
|
|
4
|
-
/** PostgreSQL text arrays: only unquoted NULL is null; quoted values use the scalar parser. */
|
|
5
|
-
export declare function parseArray(text: string, parse: (value: string) => unknown, delimiter?: string): unknown[];
|
package/dist/driver/array.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
const initialization = new WeakMap();
|
|
2
|
-
/** Preserve driver scalar parsers and serializers; replace only lossy array parsing. */
|
|
3
|
-
export function prepareArrays(sql) {
|
|
4
|
-
const existing = initialization.get(sql);
|
|
5
|
-
if (existing)
|
|
6
|
-
return existing;
|
|
7
|
-
const ready = (async () => {
|
|
8
|
-
const customArrays = new Set(Object.values(sql.options.types).flatMap((type) => type.from));
|
|
9
|
-
const arrays = await sql `
|
|
10
|
-
SELECT t.typarray AS oid, t.oid AS element, t.typdelim AS delimiter
|
|
11
|
-
FROM pg_catalog.pg_type t WHERE t.typarray <> 0`;
|
|
12
|
-
for (const type of arrays) {
|
|
13
|
-
if (customArrays.has(type.oid))
|
|
14
|
-
continue;
|
|
15
|
-
const scalar = sql.options.parsers[type.element] ?? ((value) => value);
|
|
16
|
-
sql.options.parsers[type.oid] = (value) => parseArray(value, scalar, type.delimiter);
|
|
17
|
-
}
|
|
18
|
-
})();
|
|
19
|
-
initialization.set(sql, ready);
|
|
20
|
-
void ready.catch(() => initialization.delete(sql));
|
|
21
|
-
return ready;
|
|
22
|
-
}
|
|
23
|
-
/** PostgreSQL text arrays: only unquoted NULL is null; quoted values use the scalar parser. */
|
|
24
|
-
export function parseArray(text, parse, delimiter = ',') {
|
|
25
|
-
let position = text.startsWith('[') ? text.indexOf('=') + 1 : 0;
|
|
26
|
-
function array() {
|
|
27
|
-
if (text[position++] !== '{')
|
|
28
|
-
throw new Error('Invalid PostgreSQL array.');
|
|
29
|
-
const values = [];
|
|
30
|
-
while (position < text.length && text[position] !== '}') {
|
|
31
|
-
if (text[position] === '{')
|
|
32
|
-
values.push(array());
|
|
33
|
-
else {
|
|
34
|
-
let value = '';
|
|
35
|
-
const quoted = text[position] === '"';
|
|
36
|
-
if (quoted)
|
|
37
|
-
position++;
|
|
38
|
-
while (position < text.length) {
|
|
39
|
-
const char = text[position];
|
|
40
|
-
if (char === '\\') {
|
|
41
|
-
position++;
|
|
42
|
-
value += text[position++] ?? '';
|
|
43
|
-
}
|
|
44
|
-
else if (quoted && char === '"') {
|
|
45
|
-
position++;
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
else if (!quoted && (char === delimiter || char === '}'))
|
|
49
|
-
break;
|
|
50
|
-
else {
|
|
51
|
-
value += char;
|
|
52
|
-
position++;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
values.push(!quoted && value === 'NULL' ? null : parse(value));
|
|
56
|
-
}
|
|
57
|
-
if (text[position] === delimiter)
|
|
58
|
-
position++;
|
|
59
|
-
else if (text[position] !== '}')
|
|
60
|
-
throw new Error('Invalid PostgreSQL array separator.');
|
|
61
|
-
}
|
|
62
|
-
if (text[position++] !== '}')
|
|
63
|
-
throw new Error('Unterminated PostgreSQL array.');
|
|
64
|
-
return values;
|
|
65
|
-
}
|
|
66
|
-
const result = array();
|
|
67
|
-
if (position !== text.length)
|
|
68
|
-
throw new Error('Unexpected PostgreSQL array suffix.');
|
|
69
|
-
return result;
|
|
70
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type postgres from 'postgres';
|
|
2
|
-
export interface Watch {
|
|
3
|
-
failure?: Error;
|
|
4
|
-
check(): void;
|
|
5
|
-
closed: Promise<never>;
|
|
6
|
-
reject(error: Error): void;
|
|
7
|
-
}
|
|
8
|
-
interface State {
|
|
9
|
-
acquiring: Set<(error: Error) => void>;
|
|
10
|
-
active: Map<number, Watch>;
|
|
11
|
-
capture(marker: string, watch: Watch): () => void;
|
|
12
|
-
}
|
|
13
|
-
/** Observe the driver's public connection callbacks; never reuse a reconnected transaction. */
|
|
14
|
-
export declare function observeConnections(sql: postgres.Sql): State;
|
|
15
|
-
export {};
|
package/dist/driver/lifecycle.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
const states = new WeakMap();
|
|
2
|
-
function disconnected() {
|
|
3
|
-
return Object.assign(new Error('The reserved PostgreSQL connection closed.'), {
|
|
4
|
-
code: 'CONNECTION_CLOSED',
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
/** Observe the driver's public connection callbacks; never reuse a reconnected transaction. */
|
|
8
|
-
export function observeConnections(sql) {
|
|
9
|
-
const previous = states.get(sql);
|
|
10
|
-
if (previous)
|
|
11
|
-
return previous;
|
|
12
|
-
const markers = new Map();
|
|
13
|
-
let debug = sql.options.debug;
|
|
14
|
-
// Capture the bootstrap connection ID, then restore debug before application work.
|
|
15
|
-
// Leaving it enabled would make the driver include SQL and values in enumerable errors.
|
|
16
|
-
const captureDebug = (id, text, values, types) => {
|
|
17
|
-
const watch = markers.get(text);
|
|
18
|
-
if (watch) {
|
|
19
|
-
state.active.set(id, watch);
|
|
20
|
-
markers.delete(text);
|
|
21
|
-
}
|
|
22
|
-
if (!markers.size)
|
|
23
|
-
sql.options.debug = debug;
|
|
24
|
-
if (typeof debug === 'function')
|
|
25
|
-
debug(id, text, values, types);
|
|
26
|
-
};
|
|
27
|
-
const state = {
|
|
28
|
-
acquiring: new Set(),
|
|
29
|
-
active: new Map(),
|
|
30
|
-
capture(marker, watch) {
|
|
31
|
-
if (!markers.size) {
|
|
32
|
-
debug = sql.options.debug;
|
|
33
|
-
sql.options.debug = captureDebug;
|
|
34
|
-
}
|
|
35
|
-
markers.set(marker, watch);
|
|
36
|
-
return () => {
|
|
37
|
-
markers.delete(marker);
|
|
38
|
-
if (!markers.size)
|
|
39
|
-
sql.options.debug = debug;
|
|
40
|
-
};
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
const onclose = sql.options.onclose;
|
|
44
|
-
sql.options.onclose = (id) => {
|
|
45
|
-
const error = disconnected();
|
|
46
|
-
const watch = state.active.get(id);
|
|
47
|
-
if (watch) {
|
|
48
|
-
watch.failure ??= error;
|
|
49
|
-
watch.reject(watch.failure);
|
|
50
|
-
}
|
|
51
|
-
for (const reject of state.acquiring)
|
|
52
|
-
reject(error);
|
|
53
|
-
onclose?.(id);
|
|
54
|
-
};
|
|
55
|
-
states.set(sql, state);
|
|
56
|
-
return state;
|
|
57
|
-
}
|
package/dist/driver/session.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type postgres from 'postgres';
|
|
2
|
-
import type { SqlConnection } from '../connection.js';
|
|
3
|
-
/** Pin a healthy session for transactions and advisory locks, with explicit failure cleanup. */
|
|
4
|
-
export declare function reserveConnection(sql: postgres.Sql, initial?: 'SELECT 1' | 'BEGIN'): Promise<{
|
|
5
|
-
sql: SqlConnection;
|
|
6
|
-
check: () => void;
|
|
7
|
-
release(discard?: boolean): Promise<void>;
|
|
8
|
-
}>;
|
package/dist/driver/session.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { prepareArrays } from './array.js';
|
|
2
|
-
import { observeConnections } from './lifecycle.js';
|
|
3
|
-
let nextLease = 0;
|
|
4
|
-
/** Pin a healthy session for transactions and advisory locks, with explicit failure cleanup. */
|
|
5
|
-
export async function reserveConnection(sql, initial = 'SELECT 1') {
|
|
6
|
-
await prepareArrays(sql);
|
|
7
|
-
const state = observeConnections(sql);
|
|
8
|
-
// Warm a connection before reserve(): 3.4.9 can strand a cold reserve after reconnect.
|
|
9
|
-
await sql `SELECT 1`;
|
|
10
|
-
let abandoned = false;
|
|
11
|
-
let rejectAcquisition;
|
|
12
|
-
const closed = new Promise((_, reject) => {
|
|
13
|
-
rejectAcquisition = reject;
|
|
14
|
-
});
|
|
15
|
-
state.acquiring.add(rejectAcquisition);
|
|
16
|
-
const pending = sql.reserve();
|
|
17
|
-
void pending.then((client) => {
|
|
18
|
-
if (abandoned)
|
|
19
|
-
client.release();
|
|
20
|
-
}, () => { });
|
|
21
|
-
let client;
|
|
22
|
-
try {
|
|
23
|
-
client = await Promise.race([pending, closed]);
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
abandoned = true;
|
|
27
|
-
throw error;
|
|
28
|
-
}
|
|
29
|
-
finally {
|
|
30
|
-
state.acquiring.delete(rejectAcquisition);
|
|
31
|
-
}
|
|
32
|
-
let rejectClosed;
|
|
33
|
-
const closedSession = new Promise((_, reject) => {
|
|
34
|
-
rejectClosed = reject;
|
|
35
|
-
});
|
|
36
|
-
void closedSession.catch(() => { });
|
|
37
|
-
const watch = {
|
|
38
|
-
closed: closedSession,
|
|
39
|
-
reject: rejectClosed,
|
|
40
|
-
check() {
|
|
41
|
-
if (watch.failure)
|
|
42
|
-
throw watch.failure;
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
const safe = {
|
|
46
|
-
async unsafe(text, input = [], options) {
|
|
47
|
-
watch.check();
|
|
48
|
-
// Driver dispatch runs in the next microtask, before another socket event.
|
|
49
|
-
const query = client.unsafe(text, input, options).execute();
|
|
50
|
-
return Promise.race([query, watch.closed]);
|
|
51
|
-
},
|
|
52
|
-
array: client.array,
|
|
53
|
-
typed: client.typed,
|
|
54
|
-
};
|
|
55
|
-
const connection = {
|
|
56
|
-
sql: safe,
|
|
57
|
-
check: watch.check,
|
|
58
|
-
async release(discard = false) {
|
|
59
|
-
for (const [id, active] of state.active)
|
|
60
|
-
if (active === watch)
|
|
61
|
-
state.active.delete(id);
|
|
62
|
-
if (watch.failure)
|
|
63
|
-
return; // The driver already removed the closed session.
|
|
64
|
-
if (discard) {
|
|
65
|
-
// Failed recovery must not return an unknown transaction/lock state to the pool.
|
|
66
|
-
try {
|
|
67
|
-
await client.unsafe('SELECT pg_terminate_backend(pg_backend_pid())').execute();
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
/* The backend closes itself. */
|
|
71
|
-
}
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
client.release();
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
// Capture the connection on BEGIN itself when opening a transaction.
|
|
78
|
-
const marker = `${initial} /* tenon_session_${++nextLease} */`;
|
|
79
|
-
const stopCapture = state.capture(marker, watch);
|
|
80
|
-
try {
|
|
81
|
-
await safe.unsafe(marker, [], { simple: false, prepare: false });
|
|
82
|
-
watch.check();
|
|
83
|
-
return connection;
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
await connection.release(true);
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
89
|
-
finally {
|
|
90
|
-
stopCapture();
|
|
91
|
-
}
|
|
92
|
-
}
|