kern-orm 0.9.0 → 0.10.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/README.md +5 -5
- package/dist/db/node-postgres.d.mts +1 -0
- package/dist/db/node-postgres.mjs +2 -0
- package/dist/db/postgres-js.d.mts +1 -0
- package/dist/db/postgres-js.mjs +2 -0
- package/dist/db/types.d.mts +1 -0
- package/dist/db/types.mjs +2 -0
- package/dist/db.d.mts +1 -0
- package/dist/db.mjs +2 -0
- package/dist/{index-2z5iTzXf.d.mts → index-aiyU5HR5.d.mts} +84 -894
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +4035 -270
- package/dist/testing.d.mts +1 -26
- package/dist/testing.mjs +2 -720
- package/dist/{text-CIUxPWPl.mjs → text-ucmJCsCN.mjs} +49 -92
- package/dist/types.d.mts +3 -3
- package/dist/types.mjs +62 -2
- package/package.json +10 -3
- package/dist/database-t66EaIGE.mjs +0 -5192
- package/dist/types-D3d35qR2.mjs +0 -239
package/dist/testing.mjs
CHANGED
|
@@ -1,720 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { _ as timestamptz, a as numeric, b as array, d as jsonb, f as enumOf, g as timestamp, h as time, i as integer, l as inet, m as interval, n as bigint, p as date, r as doublePrecision, t as uuid, v as boolean, y as bytea } from "./types-D3d35qR2.mjs";
|
|
4
|
-
import assert from "node:assert/strict";
|
|
5
|
-
// src/execution/testing.ts
|
|
6
|
-
/** Creates the test helpers of a database created with `test: true`, registering fixtures with the hooks of a test runner. */
|
|
7
|
-
function createTestHelpers(database, hooks) {
|
|
8
|
-
const { coordinator } = database;
|
|
9
|
-
if (!coordinator.test) throw new Error("createTestHelpers requires a database created with test: true");
|
|
10
|
-
return {
|
|
11
|
-
testTransaction: (callback, options) => coordinator.testTransaction(callback, options),
|
|
12
|
-
useTestFixture(seed, options) {
|
|
13
|
-
let pending;
|
|
14
|
-
let fixture;
|
|
15
|
-
hooks.before(async () => {
|
|
16
|
-
const current = coordinator.startTestFixture(seed, options);
|
|
17
|
-
pending = current;
|
|
18
|
-
try {
|
|
19
|
-
const opened = await current.fixture;
|
|
20
|
-
if (pending === current) fixture = opened;
|
|
21
|
-
} catch (error) {
|
|
22
|
-
if (pending === current) throw error;
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
hooks.after(async () => {
|
|
26
|
-
const current = pending;
|
|
27
|
-
pending = void 0;
|
|
28
|
-
fixture = void 0;
|
|
29
|
-
await current?.close();
|
|
30
|
-
});
|
|
31
|
-
return { get data() {
|
|
32
|
-
if (!fixture) throw new Error("Test fixture is not open: read its data in the tests and hooks of the describe block or test file that uses it");
|
|
33
|
-
return fixture.data;
|
|
34
|
-
} };
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
// src/execution/driver-contract.ts
|
|
39
|
-
/** Creates helpers that run raw statements on `target`: a database, a driver, or a reserved connection. */
|
|
40
|
-
function createStatementRunners(target) {
|
|
41
|
-
return {
|
|
42
|
-
async runStatement(text) {
|
|
43
|
-
await target.execute(createStatement(text));
|
|
44
|
-
},
|
|
45
|
-
async selectValue(text, parameters = []) {
|
|
46
|
-
const { rows } = await target.execute(createStatement(text, parameters));
|
|
47
|
-
return rows[0][0];
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Declares the tests of the contract of `Driver` for a driver.
|
|
53
|
-
*
|
|
54
|
-
* They run in the empty database of its clients and leave it unchanged.
|
|
55
|
-
*/
|
|
56
|
-
function describeDriverContract(options, runner) {
|
|
57
|
-
const { createDriver, createDriverWithFailingParsers } = options;
|
|
58
|
-
const { describe, it, before, after } = runner;
|
|
59
|
-
describe("driver contract", () => {
|
|
60
|
-
const driver = createDriver({ max: 2 });
|
|
61
|
-
const database = createDatabase({ driver });
|
|
62
|
-
after(() => database.close());
|
|
63
|
-
const { runStatement, selectValue } = createStatementRunners(database);
|
|
64
|
-
const { selectValue: selectValueOnDriver } = createStatementRunners(driver);
|
|
65
|
-
it("executes a query in a transaction", async () => {
|
|
66
|
-
const tag = defineTable("pg_tag_test", {
|
|
67
|
-
id: integer,
|
|
68
|
-
label: text
|
|
69
|
-
});
|
|
70
|
-
const db = database.bind(defineSchema({ tag }));
|
|
71
|
-
const rows = await database.transaction(async () => {
|
|
72
|
-
await runStatement("CREATE TEMP TABLE pg_tag_test (id integer, label text) ON COMMIT DROP");
|
|
73
|
-
await runStatement("INSERT INTO pg_tag_test VALUES (1, 'first'), (2, 'second')");
|
|
74
|
-
return await db.tag.select("label").where({ id: 2 });
|
|
75
|
-
});
|
|
76
|
-
assert.deepEqual(rows, [{ label: "second" }]);
|
|
77
|
-
});
|
|
78
|
-
it("executes a script of several statements on the connection of a transaction", async () => {
|
|
79
|
-
const labels = await database.transaction(async () => {
|
|
80
|
-
await database.script(`
|
|
81
|
-
CREATE TEMP TABLE pg_script_test (label text) ON COMMIT DROP;
|
|
82
|
-
INSERT INTO pg_script_test VALUES ('first'), ('second');
|
|
83
|
-
`);
|
|
84
|
-
return await selectValue("SELECT string_agg(label, ',' ORDER BY label) FROM pg_script_test");
|
|
85
|
-
});
|
|
86
|
-
assert.equal(labels, "first,second");
|
|
87
|
-
});
|
|
88
|
-
it("rejects a text of several statements without parameters before running any", async () => {
|
|
89
|
-
try {
|
|
90
|
-
await assert.rejects(database.execute(createStatement("CREATE TABLE pg_statement_test (id integer); SELECT 1")), /cannot insert multiple commands into a prepared statement/);
|
|
91
|
-
assert.equal(await selectValue("SELECT to_regclass('pg_statement_test')::text"), null);
|
|
92
|
-
} finally {
|
|
93
|
-
await database.script("SET LOCAL client_min_messages = warning; DROP TABLE IF EXISTS pg_statement_test");
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
it("returns every value as text", async () => {
|
|
97
|
-
const { rows } = await driver.execute(createStatement(`SELECT 42::integer, 12345678901234::bigint, true, '{"a":1}'::jsonb, NULL::text, ARRAY['a', 'b'], '2030-01-01 12:00'::timestamp`));
|
|
98
|
-
assert.deepEqual(rows, [[
|
|
99
|
-
"42",
|
|
100
|
-
"12345678901234",
|
|
101
|
-
"t",
|
|
102
|
-
"{\"a\": 1}",
|
|
103
|
-
null,
|
|
104
|
-
"{a,b}",
|
|
105
|
-
"2030-01-01 12:00:00"
|
|
106
|
-
]]);
|
|
107
|
-
});
|
|
108
|
-
it("keeps the values of a statement as text without the parsers of the client", async () => {
|
|
109
|
-
const parsed = createDatabase({ driver: createDriverWithFailingParsers() });
|
|
110
|
-
try {
|
|
111
|
-
const { rows } = await parsed.execute(createStatement("SELECT 1"));
|
|
112
|
-
assert.deepEqual(rows, [["1"]]);
|
|
113
|
-
} finally {
|
|
114
|
-
await parsed.close();
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
it("executes a script without the parsers of the client", async () => {
|
|
118
|
-
const parsed = createDatabase({ driver: createDriverWithFailingParsers() });
|
|
119
|
-
try {
|
|
120
|
-
await parsed.script("SELECT 1; SELECT 2");
|
|
121
|
-
} finally {
|
|
122
|
-
await parsed.close();
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
it("executes a script on the pool in one implicit transaction", async () => {
|
|
126
|
-
try {
|
|
127
|
-
await assert.rejects(database.script("CREATE TABLE pg_script_pool_test (id integer); SELECT 1/0"), (error) => error instanceof QueryError && error.code === "22012");
|
|
128
|
-
assert.equal(await selectValue("SELECT to_regclass('pg_script_pool_test')::text"), null);
|
|
129
|
-
} finally {
|
|
130
|
-
await database.script("SET LOCAL client_min_messages = warning; DROP TABLE IF EXISTS pg_script_pool_test");
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
it("reports the number of rows of the command tag", async () => {
|
|
134
|
-
const counts = await database.transaction(async () => {
|
|
135
|
-
const created = await database.execute(createStatement("CREATE TEMP TABLE pg_count_test (id integer) ON COMMIT DROP"));
|
|
136
|
-
const inserted = await database.execute(createStatement("INSERT INTO pg_count_test VALUES (1), (2), (3)"));
|
|
137
|
-
const updated = await database.execute(createStatement("UPDATE pg_count_test SET id = id + 1 WHERE id > 1"));
|
|
138
|
-
return [
|
|
139
|
-
created.count,
|
|
140
|
-
inserted.count,
|
|
141
|
-
updated.count
|
|
142
|
-
];
|
|
143
|
-
});
|
|
144
|
-
assert.deepEqual(counts, [
|
|
145
|
-
0,
|
|
146
|
-
3,
|
|
147
|
-
2
|
|
148
|
-
]);
|
|
149
|
-
});
|
|
150
|
-
it("reports the names of the result columns, also without rows", async () => {
|
|
151
|
-
const { created, selected } = await database.transaction(async () => ({
|
|
152
|
-
created: await database.execute(createStatement("CREATE TEMP TABLE pg_columns_test (id integer, label text) ON COMMIT DROP")),
|
|
153
|
-
selected: await database.execute(createStatement("SELECT id, label FROM pg_columns_test"))
|
|
154
|
-
}));
|
|
155
|
-
assert.deepEqual(created.columns, []);
|
|
156
|
-
assert.deepEqual(selected.columns, ["id", "label"]);
|
|
157
|
-
assert.deepEqual(selected.rows, []);
|
|
158
|
-
});
|
|
159
|
-
it("reports no command for a statement without a command tag", async () => {
|
|
160
|
-
for (const text of ["", "-- only a comment"]) {
|
|
161
|
-
const { command, count, columns, rows } = await database.execute(createStatement(text));
|
|
162
|
-
assert.equal(command, null);
|
|
163
|
-
assert.equal(count, 0);
|
|
164
|
-
assert.deepEqual(columns, []);
|
|
165
|
-
assert.deepEqual(rows, []);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
it("infers the types of parameters from the statement", async () => {
|
|
169
|
-
assert.equal(await selectValue("SELECT $1::integer + $2", ["1", "2"]), "3");
|
|
170
|
-
assert.equal(await selectValue("SELECT $1", ["text"]), "text");
|
|
171
|
-
assert.equal(await selectValue("SELECT $1::text IS NULL", [null]), "t");
|
|
172
|
-
});
|
|
173
|
-
it("sends a parameter as it is, whatever type PostgreSQL describes for it", async () => {
|
|
174
|
-
assert.equal(await selectValueOnDriver("SELECT $1::boolean", ["true"]), "t");
|
|
175
|
-
assert.equal(await selectValueOnDriver("SELECT $1::jsonb ->> 'a'", ["{\"a\":\"x\"}"]), "x");
|
|
176
|
-
});
|
|
177
|
-
it("runs concurrent statements of a transaction in the order of calls", async () => {
|
|
178
|
-
const values = await database.transaction(() => Promise.all([
|
|
179
|
-
selectValue("SELECT set_config('kern.test', '1', true) FROM pg_sleep(0.05)"),
|
|
180
|
-
selectValue("SELECT current_setting('kern.test')"),
|
|
181
|
-
selectValue("SELECT set_config('kern.test', '2', true)"),
|
|
182
|
-
selectValue("SELECT current_setting('kern.test')")
|
|
183
|
-
]));
|
|
184
|
-
assert.deepEqual(values, [
|
|
185
|
-
"1",
|
|
186
|
-
"1",
|
|
187
|
-
"2",
|
|
188
|
-
"2"
|
|
189
|
-
]);
|
|
190
|
-
});
|
|
191
|
-
it("runs the statements sent on a reserved connection before ROLLBACK inside its transaction", async () => {
|
|
192
|
-
const own = createDriver({ max: 1 });
|
|
193
|
-
try {
|
|
194
|
-
const connection = await own.acquire();
|
|
195
|
-
const { runStatement, selectValue } = createStatementRunners(connection);
|
|
196
|
-
try {
|
|
197
|
-
await runStatement("CREATE TEMP TABLE pg_order_test (id integer)");
|
|
198
|
-
await runStatement("BEGIN");
|
|
199
|
-
await Promise.all([
|
|
200
|
-
runStatement("INSERT INTO pg_order_test VALUES (1)"),
|
|
201
|
-
runStatement("SELECT pg_sleep(0.05)"),
|
|
202
|
-
runStatement("INSERT INTO pg_order_test VALUES (2)"),
|
|
203
|
-
runStatement("ROLLBACK")
|
|
204
|
-
]);
|
|
205
|
-
assert.equal(await selectValue("SELECT count(*) FROM pg_order_test"), "0");
|
|
206
|
-
} finally {
|
|
207
|
-
await connection.release();
|
|
208
|
-
}
|
|
209
|
-
} finally {
|
|
210
|
-
await own.close();
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
it("decodes selected expressions with their types", async () => {
|
|
214
|
-
const tag = defineTable("pg_tag_test", {
|
|
215
|
-
id: integer,
|
|
216
|
-
label: text.nullable()
|
|
217
|
-
});
|
|
218
|
-
const db = database.bind(defineSchema({ tag }));
|
|
219
|
-
const rows = await database.transaction(async () => {
|
|
220
|
-
await runStatement("CREATE TEMP TABLE pg_tag_test (id integer, label text) ON COMMIT DROP");
|
|
221
|
-
await runStatement("INSERT INTO pg_tag_test VALUES (1, 'first'), (2, NULL)");
|
|
222
|
-
return await db.tag.select((columns) => ({
|
|
223
|
-
key: columns.id,
|
|
224
|
-
name: columns.label,
|
|
225
|
-
isFirst: columns.label.eq("first"),
|
|
226
|
-
total: sql(bigint)`count(*) OVER ()`,
|
|
227
|
-
length: sql(integer.nullable())`length(${columns.label})`
|
|
228
|
-
}));
|
|
229
|
-
});
|
|
230
|
-
assert.deepEqual(rows.toSorted((a, b) => a.key - b.key), [{
|
|
231
|
-
key: 1,
|
|
232
|
-
name: "first",
|
|
233
|
-
isFirst: true,
|
|
234
|
-
total: 2n,
|
|
235
|
-
length: 5
|
|
236
|
-
}, {
|
|
237
|
-
key: 2,
|
|
238
|
-
name: null,
|
|
239
|
-
isFirst: null,
|
|
240
|
-
total: 2n,
|
|
241
|
-
length: null
|
|
242
|
-
}]);
|
|
243
|
-
});
|
|
244
|
-
it("rejects a statement that PostgreSQL rejected with QueryError and keeps its connection, on the pool and on a reserved connection", async () => {
|
|
245
|
-
const own = createDriver({ max: 1 });
|
|
246
|
-
try {
|
|
247
|
-
const pool = createStatementRunners(own);
|
|
248
|
-
await pool.runStatement("CREATE TEMP TABLE pg_unique_test (id integer CONSTRAINT pg_unique_test_key UNIQUE)");
|
|
249
|
-
await pool.runStatement("INSERT INTO pg_unique_test VALUES (1)");
|
|
250
|
-
const pid = await pool.selectValue("SELECT pg_backend_pid()");
|
|
251
|
-
/** Asserts that inserting a duplicate on `target` rejects with the violated constraint, and that its connection stays the same. */
|
|
252
|
-
async function assertRejected(target) {
|
|
253
|
-
const runners = createStatementRunners(target);
|
|
254
|
-
const error = await runners.runStatement("INSERT INTO pg_unique_test VALUES (1)").catch((error) => error);
|
|
255
|
-
assert.ok(error instanceof QueryError, `Expected QueryError, got ${String(error)}`);
|
|
256
|
-
assert.equal(error.code, "23505");
|
|
257
|
-
assert.equal(error.constraint, "pg_unique_test_key");
|
|
258
|
-
assert.equal(await runners.selectValue("SELECT pg_backend_pid()"), pid);
|
|
259
|
-
}
|
|
260
|
-
await assertRejected(own);
|
|
261
|
-
const connection = await own.acquire();
|
|
262
|
-
try {
|
|
263
|
-
await assertRejected(connection);
|
|
264
|
-
} finally {
|
|
265
|
-
await connection.release();
|
|
266
|
-
}
|
|
267
|
-
} finally {
|
|
268
|
-
await own.close();
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
it("rejects a transaction whose failed statement the callback caught", async () => {
|
|
272
|
-
const transaction = database.transaction(async () => {
|
|
273
|
-
await runStatement("SELECT 1/0").catch(() => {});
|
|
274
|
-
return "done";
|
|
275
|
-
});
|
|
276
|
-
await assert.rejects(transaction, /Transaction was rolled back/);
|
|
277
|
-
});
|
|
278
|
-
it("releases a clean connection after a failed commit", async () => {
|
|
279
|
-
const single = createDatabase({ driver: createDriver({ max: 1 }) });
|
|
280
|
-
try {
|
|
281
|
-
const transaction = single.transaction(async () => {
|
|
282
|
-
await single.execute(createStatement("CREATE TEMP TABLE pg_commit_test (id integer UNIQUE DEFERRABLE INITIALLY DEFERRED) ON COMMIT DROP"));
|
|
283
|
-
await single.execute(createStatement("INSERT INTO pg_commit_test VALUES (1), (1)"));
|
|
284
|
-
});
|
|
285
|
-
await assert.rejects(transaction, /duplicate key/);
|
|
286
|
-
await assert.rejects(single.execute(createStatement("SAVEPOINT probe")), /transaction blocks/);
|
|
287
|
-
} finally {
|
|
288
|
-
await single.close();
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
it("returns a connection to the pool after PostgreSQL rejected its last statement outside a transaction", async () => {
|
|
292
|
-
const own = createDriver({ max: 1 });
|
|
293
|
-
try {
|
|
294
|
-
const connection = await own.acquire();
|
|
295
|
-
let pid;
|
|
296
|
-
try {
|
|
297
|
-
pid = await createStatementRunners(connection).selectValue("SELECT pg_backend_pid()");
|
|
298
|
-
await assert.rejects(connection.execute(createStatement("SELECT 1/0")), QueryError);
|
|
299
|
-
} finally {
|
|
300
|
-
await connection.release();
|
|
301
|
-
}
|
|
302
|
-
assert.equal(await createStatementRunners(own).selectValue("SELECT pg_backend_pid()"), pid);
|
|
303
|
-
} finally {
|
|
304
|
-
await own.close();
|
|
305
|
-
}
|
|
306
|
-
});
|
|
307
|
-
for (const [state, text] of [["an open", "SELECT 1"], ["a failed", "SELECT 1/0"]]) it(`does not return a connection in ${state} transaction to the pool`, async () => {
|
|
308
|
-
const own = createDriver({ max: 1 });
|
|
309
|
-
try {
|
|
310
|
-
const connection = await own.acquire();
|
|
311
|
-
try {
|
|
312
|
-
await connection.execute(createStatement("BEGIN"));
|
|
313
|
-
await connection.execute(createStatement(text)).catch(() => {});
|
|
314
|
-
} finally {
|
|
315
|
-
await connection.release();
|
|
316
|
-
}
|
|
317
|
-
await assert.rejects(own.execute(createStatement("SAVEPOINT probe")), /transaction blocks/);
|
|
318
|
-
} finally {
|
|
319
|
-
await own.close();
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
describe("a statement on the pool that leaves a transaction open", () => {
|
|
323
|
-
const cases = [
|
|
324
|
-
[
|
|
325
|
-
"a statement",
|
|
326
|
-
(own) => own.execute(createStatement("BEGIN")),
|
|
327
|
-
ConnectionError
|
|
328
|
-
],
|
|
329
|
-
[
|
|
330
|
-
"a script",
|
|
331
|
-
(own) => own.executeScript("BEGIN; SELECT 1"),
|
|
332
|
-
ConnectionError
|
|
333
|
-
],
|
|
334
|
-
[
|
|
335
|
-
"a script that fails in the transaction",
|
|
336
|
-
(own) => own.executeScript("BEGIN; SELECT 1/0"),
|
|
337
|
-
Error
|
|
338
|
-
]
|
|
339
|
-
];
|
|
340
|
-
for (const [name, run, rejection] of cases) it(`rejects ${name} and leaves no transaction on the pool`, async () => {
|
|
341
|
-
const own = createDriver({ max: 2 });
|
|
342
|
-
const held = [];
|
|
343
|
-
try {
|
|
344
|
-
await assert.rejects(run(own), rejection);
|
|
345
|
-
for (let count = 0; count < 2; count++) held.push(await own.acquire());
|
|
346
|
-
for (const connection of held) await assert.rejects(connection.execute(createStatement("SAVEPOINT probe")), /transaction blocks/);
|
|
347
|
-
} finally {
|
|
348
|
-
await Promise.allSettled(held.map(async (connection) => connection.release()));
|
|
349
|
-
await own.close();
|
|
350
|
-
}
|
|
351
|
-
});
|
|
352
|
-
});
|
|
353
|
-
it("rejects a transaction whose connection is lost and keeps the pool usable", async () => {
|
|
354
|
-
const single = createDatabase({ driver: createDriver({ max: 1 }) });
|
|
355
|
-
const { selectValue: selectValueOnSingle } = createStatementRunners(single);
|
|
356
|
-
try {
|
|
357
|
-
const transaction = single.transaction(async () => {
|
|
358
|
-
const pid = await selectValueOnSingle("SELECT pg_backend_pid()");
|
|
359
|
-
await selectValue("SELECT pg_terminate_backend($1)", [pid]);
|
|
360
|
-
await selectValueOnSingle("SELECT 1");
|
|
361
|
-
});
|
|
362
|
-
await assert.rejects(transaction, (error) => error instanceof TransactionConnectionError && error.phase === "transaction");
|
|
363
|
-
assert.equal(await single.transaction(() => selectValueOnSingle("SELECT 'usable'")), "usable");
|
|
364
|
-
} finally {
|
|
365
|
-
await single.close();
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
it("rejects a statement of a transaction whose connection is lost with ConnectionError", async () => {
|
|
369
|
-
const own = createDriver({ max: 1 });
|
|
370
|
-
try {
|
|
371
|
-
const connection = await own.acquire();
|
|
372
|
-
const { runStatement, selectValue: selectValueOnConnection } = createStatementRunners(connection);
|
|
373
|
-
try {
|
|
374
|
-
await runStatement("BEGIN");
|
|
375
|
-
await runStatement("CREATE TEMP TABLE pg_lost_test (id integer) ON COMMIT DROP");
|
|
376
|
-
const pid = await selectValueOnConnection("SELECT pg_backend_pid()");
|
|
377
|
-
assert.equal(await selectValue("SELECT pg_terminate_backend($1, 5000)", [pid]), "t");
|
|
378
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
379
|
-
const error = await runStatement("INSERT INTO pg_lost_test VALUES (1)").catch((error) => error);
|
|
380
|
-
assert.ok(error instanceof ConnectionError, `Expected ConnectionError, got ${String(error)}`);
|
|
381
|
-
} finally {
|
|
382
|
-
await connection.release();
|
|
383
|
-
}
|
|
384
|
-
} finally {
|
|
385
|
-
await own.close();
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
it("replaces an idle connection whose backend exited", async () => {
|
|
389
|
-
const own = createDriver({ max: 2 });
|
|
390
|
-
const held = [];
|
|
391
|
-
try {
|
|
392
|
-
for (let count = 0; count < 2; count++) held.push(await own.acquire());
|
|
393
|
-
const pids = [];
|
|
394
|
-
for (const connection of held) pids.push(await createStatementRunners(connection).selectValue("SELECT pg_backend_pid()"));
|
|
395
|
-
while (held.length > 0) await held.shift().release();
|
|
396
|
-
for (const pid of pids) assert.equal(await selectValue("SELECT pg_terminate_backend($1, 5000)", [pid]), "t");
|
|
397
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
398
|
-
assert.equal(await createStatementRunners(own).selectValue("SELECT 'usable'"), "usable");
|
|
399
|
-
} finally {
|
|
400
|
-
await Promise.allSettled(held.map(async (connection) => connection.release()));
|
|
401
|
-
await own.close();
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
it("isolates concurrent transactions on their own connections", async () => {
|
|
405
|
-
const bothStarted = Promise.withResolvers();
|
|
406
|
-
let startedCount = 0;
|
|
407
|
-
function runTransaction(label) {
|
|
408
|
-
return database.transaction(async () => {
|
|
409
|
-
const startPid = await selectValue("SELECT pg_backend_pid()");
|
|
410
|
-
await selectValue("SELECT set_config('kern.test', $1, true)", [label]);
|
|
411
|
-
startedCount += 1;
|
|
412
|
-
if (startedCount === 2) bothStarted.resolve();
|
|
413
|
-
await bothStarted.promise;
|
|
414
|
-
return {
|
|
415
|
-
startPid,
|
|
416
|
-
endPid: await selectValue("SELECT pg_backend_pid()"),
|
|
417
|
-
label: await selectValue("SELECT current_setting('kern.test')")
|
|
418
|
-
};
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
const [result1, result2] = await Promise.all([runTransaction("1"), runTransaction("2")]);
|
|
422
|
-
assert.equal(result1.endPid, result1.startPid);
|
|
423
|
-
assert.equal(result2.endPid, result2.startPid);
|
|
424
|
-
assert.notEqual(result1.startPid, result2.startPid);
|
|
425
|
-
assert.deepEqual([result1.label, result2.label], ["1", "2"]);
|
|
426
|
-
});
|
|
427
|
-
it("runs queries outside an open transaction on the pool", async () => {
|
|
428
|
-
const transactionStarted = Promise.withResolvers();
|
|
429
|
-
const outsideFinished = Promise.withResolvers();
|
|
430
|
-
const transactionPid = database.transaction(async () => {
|
|
431
|
-
await selectValue("SELECT set_config('kern.test', 'inside', true)");
|
|
432
|
-
transactionStarted.resolve();
|
|
433
|
-
await outsideFinished.promise;
|
|
434
|
-
return selectValue("SELECT pg_backend_pid()");
|
|
435
|
-
});
|
|
436
|
-
await transactionStarted.promise;
|
|
437
|
-
const outsidePid = await selectValue("SELECT pg_backend_pid()");
|
|
438
|
-
const outsideLabel = await selectValue("SELECT current_setting('kern.test', true)");
|
|
439
|
-
outsideFinished.resolve();
|
|
440
|
-
assert.notEqual(outsidePid, await transactionPid);
|
|
441
|
-
assert.notEqual(outsideLabel, "inside");
|
|
442
|
-
});
|
|
443
|
-
describe("closing", () => {
|
|
444
|
-
/** Creates a database whose client has one connection, so that a transaction waits for the connection of another one. */
|
|
445
|
-
function createClosableDatabase() {
|
|
446
|
-
return createDatabase({ driver: createDriver({ max: 1 }) });
|
|
447
|
-
}
|
|
448
|
-
it("waits for a running transaction and for a transaction waiting for a connection", async () => {
|
|
449
|
-
const database = createClosableDatabase();
|
|
450
|
-
const { promise: started, resolve: start } = Promise.withResolvers();
|
|
451
|
-
const { promise: closeRequested, resolve: requestClose } = Promise.withResolvers();
|
|
452
|
-
const { selectValue } = createStatementRunners(database);
|
|
453
|
-
const running = database.transaction(async () => {
|
|
454
|
-
start();
|
|
455
|
-
await closeRequested;
|
|
456
|
-
return selectValue("SELECT 'running'");
|
|
457
|
-
});
|
|
458
|
-
await started;
|
|
459
|
-
const waiting = database.transaction(() => selectValue("SELECT 'waiting'"));
|
|
460
|
-
const closing = database.close();
|
|
461
|
-
requestClose();
|
|
462
|
-
assert.equal(await running, "running");
|
|
463
|
-
assert.equal(await waiting, "waiting");
|
|
464
|
-
await closing;
|
|
465
|
-
});
|
|
466
|
-
it("waits for a statement on the pool waiting for a connection", async () => {
|
|
467
|
-
const database = createClosableDatabase();
|
|
468
|
-
const { promise: started, resolve: start } = Promise.withResolvers();
|
|
469
|
-
const { promise: closeRequested, resolve: requestClose } = Promise.withResolvers();
|
|
470
|
-
const { selectValue } = createStatementRunners(database);
|
|
471
|
-
const running = database.transaction(async () => {
|
|
472
|
-
start();
|
|
473
|
-
await closeRequested;
|
|
474
|
-
});
|
|
475
|
-
await started;
|
|
476
|
-
const waiting = selectValue("SELECT 'waiting'");
|
|
477
|
-
const closing = database.close();
|
|
478
|
-
requestClose();
|
|
479
|
-
await running;
|
|
480
|
-
assert.equal(await waiting, "waiting");
|
|
481
|
-
await closing;
|
|
482
|
-
});
|
|
483
|
-
it("waits for an in-flight pool query before closing", async () => {
|
|
484
|
-
const database = createClosableDatabase();
|
|
485
|
-
const { selectValue } = createStatementRunners(database);
|
|
486
|
-
await selectValue("SELECT 1");
|
|
487
|
-
const settled = [];
|
|
488
|
-
const query = selectValue("SELECT 'slept' FROM pg_sleep(0.2)");
|
|
489
|
-
query.then(() => settled.push("query"), () => {});
|
|
490
|
-
await database.close();
|
|
491
|
-
settled.push("database");
|
|
492
|
-
assert.equal(await query, "slept");
|
|
493
|
-
assert.deepEqual(settled, ["query", "database"]);
|
|
494
|
-
});
|
|
495
|
-
it("rejects a statement, a script, and a connection requested right after it starts with ConnectionError", async () => {
|
|
496
|
-
const driver = createDriver({ max: 1 });
|
|
497
|
-
const held = await driver.acquire();
|
|
498
|
-
const closing = driver.close();
|
|
499
|
-
const acquiring = driver.acquire();
|
|
500
|
-
const rejections = [
|
|
501
|
-
assert.rejects(driver.execute(createStatement("SELECT 1")), ConnectionError),
|
|
502
|
-
assert.rejects(driver.executeScript("SELECT 1"), ConnectionError),
|
|
503
|
-
assert.rejects(acquiring, ConnectionError)
|
|
504
|
-
];
|
|
505
|
-
acquiring.then((connection) => connection.release()).catch(() => {});
|
|
506
|
-
await held.release();
|
|
507
|
-
try {
|
|
508
|
-
await Promise.all(rejections);
|
|
509
|
-
} finally {
|
|
510
|
-
await closing;
|
|
511
|
-
}
|
|
512
|
-
});
|
|
513
|
-
});
|
|
514
|
-
describe("in test transactions", () => {
|
|
515
|
-
const database = createDatabase({
|
|
516
|
-
driver: createDriver({ max: 2 }),
|
|
517
|
-
test: true
|
|
518
|
-
});
|
|
519
|
-
after(() => database.close());
|
|
520
|
-
const { testTransaction } = createTestHelpers(database, {
|
|
521
|
-
before,
|
|
522
|
-
after
|
|
523
|
-
});
|
|
524
|
-
const { runStatement, selectValue } = createStatementRunners(database);
|
|
525
|
-
it("discards only the rows of a rolled-back nested transaction", async () => {
|
|
526
|
-
const tag = defineTable("pg_nested_test", { id: integer });
|
|
527
|
-
const db = database.bind(defineSchema({ tag }));
|
|
528
|
-
const rows = await testTransaction(async () => {
|
|
529
|
-
await runStatement("CREATE TABLE pg_nested_test (id integer)");
|
|
530
|
-
await runStatement("INSERT INTO pg_nested_test VALUES (1)");
|
|
531
|
-
const failure = /* @__PURE__ */ new Error("failure");
|
|
532
|
-
const failed = database.transaction(async () => {
|
|
533
|
-
await runStatement("INSERT INTO pg_nested_test VALUES (2)");
|
|
534
|
-
throw failure;
|
|
535
|
-
});
|
|
536
|
-
await assert.rejects(failed, (error) => error === failure);
|
|
537
|
-
await database.transaction(() => runStatement("INSERT INTO pg_nested_test VALUES (3)"));
|
|
538
|
-
return await db.tag.select("id");
|
|
539
|
-
});
|
|
540
|
-
assert.deepEqual(rows.map(({ id }) => id).toSorted(), [1, 3]);
|
|
541
|
-
});
|
|
542
|
-
it("keeps the transaction usable after a failed statement in a nested transaction", async () => {
|
|
543
|
-
const value = await testTransaction(async () => {
|
|
544
|
-
await assert.rejects(database.transaction(() => runStatement("SELECT 1/0")), /division by zero/);
|
|
545
|
-
return await selectValue("SELECT 'usable'");
|
|
546
|
-
});
|
|
547
|
-
assert.equal(value, "usable");
|
|
548
|
-
});
|
|
549
|
-
it("keeps the changes of open fixtures for test transactions until each fixture closes", async () => {
|
|
550
|
-
const entry = defineTable("pg_fixture_test", { label: text });
|
|
551
|
-
const db = database.bind(defineSchema({ entry }));
|
|
552
|
-
const selectLabels = () => testTransaction(async () => {
|
|
553
|
-
return (await db.entry.select("label")).map(({ label }) => label).toSorted();
|
|
554
|
-
});
|
|
555
|
-
{
|
|
556
|
-
await using _run = await database.coordinator.createTestFixture(async () => {
|
|
557
|
-
await runStatement("CREATE TABLE pg_fixture_test (label text)");
|
|
558
|
-
await runStatement("INSERT INTO pg_fixture_test VALUES ('run')");
|
|
559
|
-
});
|
|
560
|
-
{
|
|
561
|
-
await using file = await database.coordinator.createTestFixture(async () => {
|
|
562
|
-
await runStatement("INSERT INTO pg_fixture_test VALUES ('file')");
|
|
563
|
-
return await db.entry.where({ label: "file" }).select("label");
|
|
564
|
-
});
|
|
565
|
-
assert.deepEqual(file.data, [{ label: "file" }]);
|
|
566
|
-
const inTest = await testTransaction(async () => {
|
|
567
|
-
await runStatement("INSERT INTO pg_fixture_test VALUES ('test')");
|
|
568
|
-
return (await db.entry.select("label")).map(({ label }) => label).toSorted();
|
|
569
|
-
});
|
|
570
|
-
assert.deepEqual(inTest, [
|
|
571
|
-
"file",
|
|
572
|
-
"run",
|
|
573
|
-
"test"
|
|
574
|
-
]);
|
|
575
|
-
assert.deepEqual(await selectLabels(), ["file", "run"]);
|
|
576
|
-
}
|
|
577
|
-
assert.deepEqual(await selectLabels(), ["run"]);
|
|
578
|
-
}
|
|
579
|
-
assert.equal(await testTransaction(() => selectValue("SELECT to_regclass('pg_fixture_test')")), null);
|
|
580
|
-
});
|
|
581
|
-
it("closes a database with an open fixture", async () => {
|
|
582
|
-
const closed = createDatabase({
|
|
583
|
-
driver: createDriver({ max: 1 }),
|
|
584
|
-
test: true
|
|
585
|
-
});
|
|
586
|
-
const fixture = await closed.coordinator.createTestFixture(() => closed.execute(createStatement("SELECT 1")));
|
|
587
|
-
await closed.close();
|
|
588
|
-
assert.throws(() => fixture.data, /Test fixture is closed/);
|
|
589
|
-
});
|
|
590
|
-
it("round-trips the built-in types", async () => {
|
|
591
|
-
const sample = defineTable("pg_type_test", {
|
|
592
|
-
bigint,
|
|
593
|
-
numeric: numeric.with(20, 6),
|
|
594
|
-
price: numeric.with(10, 2).transform({ decode: Number }),
|
|
595
|
-
double: doublePrecision,
|
|
596
|
-
flag: boolean,
|
|
597
|
-
id: uuid,
|
|
598
|
-
bytes: bytea,
|
|
599
|
-
at: timestamptz,
|
|
600
|
-
local: timestamp,
|
|
601
|
-
day: date,
|
|
602
|
-
clock: time,
|
|
603
|
-
span: interval,
|
|
604
|
-
doc: jsonb.of(),
|
|
605
|
-
labels: array(text.nullable()),
|
|
606
|
-
address: inet,
|
|
607
|
-
status: enumOf("pg_status_test", ["draft", "published"])
|
|
608
|
-
});
|
|
609
|
-
const db = database.bind(defineSchema({ sample }));
|
|
610
|
-
const values = {
|
|
611
|
-
bigint: 9007199254740993n,
|
|
612
|
-
numeric: "12345678901234.123456",
|
|
613
|
-
price: 12.5,
|
|
614
|
-
double: .1,
|
|
615
|
-
flag: true,
|
|
616
|
-
id: "0193d6b6-7c43-7000-8000-000000000001",
|
|
617
|
-
bytes: new Uint8Array([0, 255]),
|
|
618
|
-
at: "2026-01-15T07:00:00.123456Z",
|
|
619
|
-
local: "2026-01-15T10:00:00.000000",
|
|
620
|
-
day: "2026-01-15",
|
|
621
|
-
clock: "10:00:00.500000",
|
|
622
|
-
span: "1 day 02:00:00",
|
|
623
|
-
doc: { tags: ["a", "b"] },
|
|
624
|
-
labels: [
|
|
625
|
-
"a",
|
|
626
|
-
"b c",
|
|
627
|
-
null,
|
|
628
|
-
"NULL"
|
|
629
|
-
],
|
|
630
|
-
address: "192.168.0.1",
|
|
631
|
-
status: "published"
|
|
632
|
-
};
|
|
633
|
-
const rows = await testTransaction(async () => {
|
|
634
|
-
await runStatement("SET LOCAL TIME ZONE 'Asia/Bangkok'");
|
|
635
|
-
await runStatement("CREATE TYPE pg_status_test AS ENUM ('draft', 'published')");
|
|
636
|
-
await runStatement(`
|
|
637
|
-
CREATE TABLE pg_type_test (
|
|
638
|
-
bigint bigint, numeric numeric(20, 6), price numeric(10, 2), double double precision, flag boolean, id uuid,
|
|
639
|
-
bytes bytea, at timestamptz, local timestamp, day date, clock time, span interval,
|
|
640
|
-
doc jsonb, labels text[], address inet, status pg_status_test
|
|
641
|
-
)
|
|
642
|
-
`);
|
|
643
|
-
await runStatement(`
|
|
644
|
-
INSERT INTO pg_type_test VALUES (
|
|
645
|
-
9007199254740993, 12345678901234.123456, 12.50, 0.1, true, '0193d6b6-7c43-7000-8000-000000000001',
|
|
646
|
-
'\\x00ff', '2026-01-15 14:00:00.123456+07', '2026-01-15 10:00', '2026-01-15', '10:00:00.5',
|
|
647
|
-
'1 day 2 hours', '{"tags": ["a", "b"]}', '{a,"b c",NULL,"NULL"}', '192.168.0.1', 'published'
|
|
648
|
-
)
|
|
649
|
-
`);
|
|
650
|
-
return {
|
|
651
|
-
selected: await db.sample.select("*"),
|
|
652
|
-
matched: await db.sample.where(values).select("id")
|
|
653
|
-
};
|
|
654
|
-
});
|
|
655
|
-
assert.deepEqual(rows.selected, [values]);
|
|
656
|
-
assert.deepEqual(rows.matched, [{ id: values.id }]);
|
|
657
|
-
});
|
|
658
|
-
it("compares parameters of param with the operators of their types", async () => {
|
|
659
|
-
const probe = defineTable("pg_param_test", { id: integer });
|
|
660
|
-
const db = database.bind(defineSchema({ probe }));
|
|
661
|
-
const rows = await testTransaction(async () => {
|
|
662
|
-
await runStatement("CREATE TABLE pg_param_test (id integer)");
|
|
663
|
-
await runStatement("INSERT INTO pg_param_test VALUES (1)");
|
|
664
|
-
return await db.probe.where(() => and(param(integer, 2).lt(10), param(numeric.with(3, 1), "1.04").ne("1.0"), param(char, "ab").eq("ab"))).select("id");
|
|
665
|
-
});
|
|
666
|
-
assert.deepEqual(rows, [{ id: 1 }]);
|
|
667
|
-
});
|
|
668
|
-
it("round-trips date and time values of years outside 1 to 9999", async () => {
|
|
669
|
-
const moment = defineTable("pg_moment_test", {
|
|
670
|
-
id: integer,
|
|
671
|
-
at: timestamptz,
|
|
672
|
-
local: timestamp,
|
|
673
|
-
day: date
|
|
674
|
-
});
|
|
675
|
-
const db = database.bind(defineSchema({ moment }));
|
|
676
|
-
const values = [
|
|
677
|
-
{
|
|
678
|
-
id: 1,
|
|
679
|
-
at: "0000-01-01T00:00:00.000000Z",
|
|
680
|
-
local: "-000001-06-15T10:00:00.000000",
|
|
681
|
-
day: "-004712-01-01"
|
|
682
|
-
},
|
|
683
|
-
{
|
|
684
|
-
id: 2,
|
|
685
|
-
at: "+294276-12-31T23:59:59.999999Z",
|
|
686
|
-
local: "+010000-01-01T00:00:00.000000",
|
|
687
|
-
day: "+010000-01-01"
|
|
688
|
-
},
|
|
689
|
-
{
|
|
690
|
-
id: 3,
|
|
691
|
-
at: "9999-12-31T23:59:59.000000Z",
|
|
692
|
-
local: "9999-12-31T23:59:59.000000",
|
|
693
|
-
day: "+5874897-12-31"
|
|
694
|
-
}
|
|
695
|
-
];
|
|
696
|
-
const rows = await testTransaction(async () => {
|
|
697
|
-
await runStatement("SET LOCAL TIME ZONE 'Asia/Bangkok'");
|
|
698
|
-
await runStatement("CREATE TABLE pg_moment_test (id integer, at timestamptz, local timestamp, day date)");
|
|
699
|
-
await runStatement(`
|
|
700
|
-
INSERT INTO pg_moment_test VALUES
|
|
701
|
-
(1, '0001-01-01 00:00:00+00 BC', '0002-06-15 10:00 BC', '4713-01-01 BC'),
|
|
702
|
-
(2, '294276-12-31 23:59:59.999999+00', '10000-01-01', '10000-01-01'),
|
|
703
|
-
(3, '9999-12-31 23:59:59+00', '9999-12-31 23:59:59', '5874897-12-31')
|
|
704
|
-
`);
|
|
705
|
-
return {
|
|
706
|
-
selected: await db.moment.select("*"),
|
|
707
|
-
matched: await Promise.all(values.map((value) => db.moment.where(value).select("id")))
|
|
708
|
-
};
|
|
709
|
-
});
|
|
710
|
-
assert.deepEqual(rows.selected, values);
|
|
711
|
-
assert.deepEqual(rows.matched, [
|
|
712
|
-
[{ id: 1 }],
|
|
713
|
-
[{ id: 2 }],
|
|
714
|
-
[{ id: 3 }]
|
|
715
|
-
]);
|
|
716
|
-
});
|
|
717
|
-
});
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
export { createTestHelpers, describeDriverContract };
|
|
1
|
+
export * from "kern-db/testing";
|
|
2
|
+
export {};
|