pgsql-test 5.16.1 → 5.17.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 +29 -0
- package/connect.js +5 -2
- package/esm/connect.js +5 -2
- package/esm/test-client.js +42 -0
- package/package.json +9 -9
- package/test-client.d.ts +15 -0
- package/test-client.js +42 -0
package/README.md
CHANGED
|
@@ -131,6 +131,7 @@ The `PgTestClient` returned by `getConnections()` wraps a `pg.Client` and provid
|
|
|
131
131
|
* `beforeEach()` – Begins a transaction and sets a savepoint (called at the start of each test)
|
|
132
132
|
* `afterEach()` – Rolls back to the savepoint and commits the outer transaction (cleans up test state)
|
|
133
133
|
* `setContext({ key: value })` – Sets PostgreSQL config variables (like `role`) to simulate RLS contexts
|
|
134
|
+
* `checkConstraints()` – Runs the commit-time checks for any pending deferred constraints now, without committing (see [Deferred constraints](#deferred-constraints-under-rollback-isolation))
|
|
134
135
|
* `any`, `one`, `oneOrNone`, `many`, `manyOrNone`, `none`, `result` – Typed query helpers for specific result expectations
|
|
135
136
|
|
|
136
137
|
These methods make it easier to build expressive and isolated integration tests with strong typing and error handling.
|
|
@@ -601,6 +602,34 @@ This table documents the available options for the `getConnections` function. Th
|
|
|
601
602
|
| `db.template` | `string` | `undefined` | Template database used for faster test DB creation |
|
|
602
603
|
| `db.rootDb` | `string` | `'postgres'` | Root database used for administrative operations (e.g., creating databases) |
|
|
603
604
|
| `db.prefix` | `string` | `'db-'` | Prefix used when generating test database names |
|
|
605
|
+
| `db.deferredConstraints` | `'off' \| 'check' \| 'immediate'` | `'off'` | How `DEFERRABLE INITIALLY DEFERRED` constraints are handled under rollback isolation (env: `DB_DEFERRED_CONSTRAINTS`). See below. |
|
|
606
|
+
|
|
607
|
+
### Deferred constraints under rollback isolation
|
|
608
|
+
|
|
609
|
+
`beforeEach()`/`afterEach()` isolate tests by rolling back instead of committing. Anything Postgres only does **at COMMIT** therefore never happens inside a test. The one that bites is `DEFERRABLE INITIALLY DEFERRED` constraints (FK / UNIQUE / EXCLUDE / `CONSTRAINT TRIGGER`): a test can leave a dangling deferred foreign key and still pass, because the check that would have failed the commit is discarded with the rollback. This is inherent to every rollback-based test harness, not specific to pgsql-test.
|
|
610
|
+
|
|
611
|
+
`db.deferredConstraints` controls what to do about it:
|
|
612
|
+
|
|
613
|
+
| Mode | What happens | Trade-off |
|
|
614
|
+
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
615
|
+
| `'off'` | Nothing (default, previous behaviour). | Deferred violations pass silently. |
|
|
616
|
+
| `'check'` | `afterEach()` runs `SET CONSTRAINTS ALL IMMEDIATE` as the last statement before rolling back. Postgres defines this as running exactly the checks COMMIT would have run, so deferral still works *inside* the test and a pending violation fails the test. | The failure surfaces in `afterEach`, not on the offending line (the message names the constraint and table). If the test body already raised a Postgres error, the check is skipped so the original error is not masked. |
|
|
617
|
+
| `'immediate'` | `beforeEach()` runs `SET CONSTRAINTS ALL IMMEDIATE`, so every deferred constraint is checked per statement and a violation fails on the exact line. | Changes semantics: code that legitimately relies on deferral (insert child before parent, swap two values under a deferred UNIQUE) fails in tests but works in production. |
|
|
618
|
+
|
|
619
|
+
```ts
|
|
620
|
+
const { db, teardown } = await getConnections({ db: { deferredConstraints: 'check' } });
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
A test that wants to *assert* that a deferred constraint is enforced can call `checkConstraints()` itself; once it has thrown, the pending events are consumed and `afterEach()` is clean:
|
|
624
|
+
|
|
625
|
+
```ts
|
|
626
|
+
it('rejects orphan children at commit', async () => {
|
|
627
|
+
await db.query(`INSERT INTO children (parent_id) VALUES (999)`);
|
|
628
|
+
await expect(db.checkConstraints()).rejects.toThrow(/children_parent_id_fkey/);
|
|
629
|
+
});
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
Other commit-only effects — `NOTIFY` delivery and visibility to other sessions — are not covered by any mode; use `publish()` (which really commits) when a test needs them.
|
|
604
633
|
|
|
605
634
|
### `pg` Options (PgConfig)
|
|
606
635
|
|
package/connect.js
CHANGED
|
@@ -53,7 +53,9 @@ const getConnections = async (cn = {}, seedAdapters = [seed_1.seed.pgpm()]) => {
|
|
|
53
53
|
}
|
|
54
54
|
await admin.grantConnect(connOpts.connections.app.user, config.database);
|
|
55
55
|
manager = manager_1.PgTestConnector.getInstance(config);
|
|
56
|
-
const pg = manager.getClient(config
|
|
56
|
+
const pg = manager.getClient(config, {
|
|
57
|
+
deferredConstraints: connOpts.deferredConstraints
|
|
58
|
+
});
|
|
57
59
|
let teardownPromise = null;
|
|
58
60
|
let teardownOpts = {};
|
|
59
61
|
const teardown = async (opts = {}) => {
|
|
@@ -97,7 +99,8 @@ const getConnections = async (cn = {}, seedAdapters = [seed_1.seed.pgpm()]) => {
|
|
|
97
99
|
};
|
|
98
100
|
const db = manager.getClient(dbConfig, {
|
|
99
101
|
auth: connOpts.auth,
|
|
100
|
-
roles: connOpts.roles
|
|
102
|
+
roles: connOpts.roles,
|
|
103
|
+
deferredConstraints: connOpts.deferredConstraints
|
|
101
104
|
});
|
|
102
105
|
db.setContext({ role: (0, pgsql_client_1.getDefaultRole)(connOpts) });
|
|
103
106
|
return { pg, db, teardown, manager, admin };
|
package/esm/connect.js
CHANGED
|
@@ -49,7 +49,9 @@ export const getConnections = async (cn = {}, seedAdapters = [seed.pgpm()]) => {
|
|
|
49
49
|
}
|
|
50
50
|
await admin.grantConnect(connOpts.connections.app.user, config.database);
|
|
51
51
|
manager = PgTestConnector.getInstance(config);
|
|
52
|
-
const pg = manager.getClient(config
|
|
52
|
+
const pg = manager.getClient(config, {
|
|
53
|
+
deferredConstraints: connOpts.deferredConstraints
|
|
54
|
+
});
|
|
53
55
|
let teardownPromise = null;
|
|
54
56
|
let teardownOpts = {};
|
|
55
57
|
const teardown = async (opts = {}) => {
|
|
@@ -93,7 +95,8 @@ export const getConnections = async (cn = {}, seedAdapters = [seed.pgpm()]) => {
|
|
|
93
95
|
};
|
|
94
96
|
const db = manager.getClient(dbConfig, {
|
|
95
97
|
auth: connOpts.auth,
|
|
96
|
-
roles: connOpts.roles
|
|
98
|
+
roles: connOpts.roles,
|
|
99
|
+
deferredConstraints: connOpts.deferredConstraints
|
|
97
100
|
});
|
|
98
101
|
db.setContext({ role: getDefaultRole(connOpts) });
|
|
99
102
|
return { pg, db, teardown, manager, admin };
|
package/esm/test-client.js
CHANGED
|
@@ -4,6 +4,7 @@ import { loadCsvMap } from 'pgsql-seed';
|
|
|
4
4
|
import { loadSqlFiles } from 'pgsql-seed';
|
|
5
5
|
import { deployPgpm } from 'pgsql-seed';
|
|
6
6
|
import { formatPgError } from './utils';
|
|
7
|
+
const IN_FAILED_SQL_TRANSACTION = '25P02';
|
|
7
8
|
export class PgTestClient extends PgClient {
|
|
8
9
|
testOpts;
|
|
9
10
|
constructor(config, opts = {}) {
|
|
@@ -34,13 +35,51 @@ export class PgTestClient extends PgClient {
|
|
|
34
35
|
throw err;
|
|
35
36
|
}
|
|
36
37
|
}
|
|
38
|
+
get deferredConstraintsMode() {
|
|
39
|
+
return this.testOpts.deferredConstraints ?? 'off';
|
|
40
|
+
}
|
|
37
41
|
async beforeEach() {
|
|
38
42
|
await this.begin();
|
|
39
43
|
await this.savepoint();
|
|
44
|
+
if (this.deferredConstraintsMode === 'immediate') {
|
|
45
|
+
await this.setConstraintsImmediate();
|
|
46
|
+
}
|
|
40
47
|
}
|
|
41
48
|
async afterEach() {
|
|
49
|
+
let violation;
|
|
50
|
+
if (this.deferredConstraintsMode === 'check') {
|
|
51
|
+
try {
|
|
52
|
+
await this.checkConstraints();
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
if (err?.code !== IN_FAILED_SQL_TRANSACTION)
|
|
56
|
+
violation = err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
42
59
|
await this.rollback();
|
|
43
60
|
await this.commit();
|
|
61
|
+
if (violation)
|
|
62
|
+
throw violation;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Run the commit-time checks for every pending deferred constraint now, without committing.
|
|
66
|
+
* Postgres checks all outstanding deferred constraint events when a constraint switches from
|
|
67
|
+
* DEFERRED to IMMEDIATE, so this fails exactly where a real COMMIT would have failed.
|
|
68
|
+
* Once it passes (or throws), the pending events are consumed.
|
|
69
|
+
*/
|
|
70
|
+
async checkConstraints() {
|
|
71
|
+
try {
|
|
72
|
+
await this.setConstraintsImmediate();
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err?.code !== IN_FAILED_SQL_TRANSACTION) {
|
|
76
|
+
err.message = `[pgsql-test] deferred constraint violated at end of test (a real COMMIT would have failed here):\n${err.message}`;
|
|
77
|
+
}
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async setConstraintsImmediate() {
|
|
82
|
+
await this.query('SET CONSTRAINTS ALL IMMEDIATE');
|
|
44
83
|
}
|
|
45
84
|
/**
|
|
46
85
|
* Commit current transaction to make data visible to other connections, then start fresh transaction.
|
|
@@ -50,6 +89,9 @@ export class PgTestClient extends PgClient {
|
|
|
50
89
|
await this.commit(); // make data visible to other sessions
|
|
51
90
|
await this.begin(); // fresh tx
|
|
52
91
|
await this.savepoint(); // keep rollback harness
|
|
92
|
+
if (this.deferredConstraintsMode === 'immediate') {
|
|
93
|
+
await this.setConstraintsImmediate();
|
|
94
|
+
}
|
|
53
95
|
await this.ctxQuery(); // reapply all setContext()
|
|
54
96
|
}
|
|
55
97
|
async loadJson(data) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pgsql-test",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.17.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "pgsql-test offers isolated, role-aware, and rollback-friendly PostgreSQL environments for integration tests — giving developers realistic test coverage without external state pollution",
|
|
6
6
|
"main": "index.js",
|
|
@@ -55,21 +55,21 @@
|
|
|
55
55
|
"test:watch": "jest --watch"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@pgpmjs/core": "7.32.
|
|
58
|
+
"@pgpmjs/core": "7.32.2",
|
|
59
59
|
"@types/pg": "^8.20.4",
|
|
60
60
|
"@types/pg-copy-streams": "^1.2.5",
|
|
61
61
|
"makage": "^0.8.0"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@pgpmjs/env": "^2.
|
|
64
|
+
"@pgpmjs/env": "^2.46.0",
|
|
65
65
|
"@pgpmjs/logger": "^2.26.0",
|
|
66
|
-
"@pgpmjs/server-utils": "^3.29.
|
|
67
|
-
"@pgpmjs/types": "^2.
|
|
66
|
+
"@pgpmjs/server-utils": "^3.29.3",
|
|
67
|
+
"@pgpmjs/types": "^2.56.0",
|
|
68
68
|
"pg": "^8.21.0",
|
|
69
|
-
"pg-cache": "^3.29.
|
|
69
|
+
"pg-cache": "^3.29.3",
|
|
70
70
|
"pg-env": "^1.32.0",
|
|
71
|
-
"pgsql-client": "^4.16.
|
|
72
|
-
"pgsql-seed": "^3.15.
|
|
71
|
+
"pgsql-client": "^4.16.3",
|
|
72
|
+
"pgsql-seed": "^3.15.3"
|
|
73
73
|
},
|
|
74
|
-
"gitHead": "
|
|
74
|
+
"gitHead": "4c68f85c7d9b3c72082f8305dffc0b3dfca80941"
|
|
75
75
|
}
|
package/test-client.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DeferredConstraintsMode } from '@pgpmjs/types';
|
|
1
2
|
import { QueryResult } from 'pg';
|
|
2
3
|
import { PgConfig } from 'pg-env';
|
|
3
4
|
import { PgClient, PgClientOpts } from 'pgsql-client';
|
|
@@ -10,6 +11,11 @@ export type PgTestClientOpts = PgClientOpts & {
|
|
|
10
11
|
* Can be disabled by setting enhancedErrors: false.
|
|
11
12
|
*/
|
|
12
13
|
enhancedErrors?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* How DEFERRABLE INITIALLY DEFERRED constraints are handled under rollback isolation.
|
|
16
|
+
* Defaults to 'off'. See {@link DeferredConstraintsMode}.
|
|
17
|
+
*/
|
|
18
|
+
deferredConstraints?: DeferredConstraintsMode;
|
|
13
19
|
};
|
|
14
20
|
export declare class PgTestClient extends PgClient {
|
|
15
21
|
protected testOpts: PgTestClientOpts;
|
|
@@ -24,8 +30,17 @@ export declare class PgTestClient extends PgClient {
|
|
|
24
30
|
* When enhancedErrors is enabled, errors will include detail, hint, where, position, etc.
|
|
25
31
|
*/
|
|
26
32
|
query<T = any>(query: string, values?: any[]): Promise<QueryResult<T>>;
|
|
33
|
+
private get deferredConstraintsMode();
|
|
27
34
|
beforeEach(): Promise<void>;
|
|
28
35
|
afterEach(): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Run the commit-time checks for every pending deferred constraint now, without committing.
|
|
38
|
+
* Postgres checks all outstanding deferred constraint events when a constraint switches from
|
|
39
|
+
* DEFERRED to IMMEDIATE, so this fails exactly where a real COMMIT would have failed.
|
|
40
|
+
* Once it passes (or throws), the pending events are consumed.
|
|
41
|
+
*/
|
|
42
|
+
checkConstraints(): Promise<void>;
|
|
43
|
+
private setConstraintsImmediate;
|
|
29
44
|
/**
|
|
30
45
|
* Commit current transaction to make data visible to other connections, then start fresh transaction.
|
|
31
46
|
* Maintains test isolation by creating a savepoint and reapplying session context.
|
package/test-client.js
CHANGED
|
@@ -7,6 +7,7 @@ const pgsql_seed_2 = require("pgsql-seed");
|
|
|
7
7
|
const pgsql_seed_3 = require("pgsql-seed");
|
|
8
8
|
const pgsql_seed_4 = require("pgsql-seed");
|
|
9
9
|
const utils_1 = require("./utils");
|
|
10
|
+
const IN_FAILED_SQL_TRANSACTION = '25P02';
|
|
10
11
|
class PgTestClient extends pgsql_client_1.PgClient {
|
|
11
12
|
testOpts;
|
|
12
13
|
constructor(config, opts = {}) {
|
|
@@ -37,13 +38,51 @@ class PgTestClient extends pgsql_client_1.PgClient {
|
|
|
37
38
|
throw err;
|
|
38
39
|
}
|
|
39
40
|
}
|
|
41
|
+
get deferredConstraintsMode() {
|
|
42
|
+
return this.testOpts.deferredConstraints ?? 'off';
|
|
43
|
+
}
|
|
40
44
|
async beforeEach() {
|
|
41
45
|
await this.begin();
|
|
42
46
|
await this.savepoint();
|
|
47
|
+
if (this.deferredConstraintsMode === 'immediate') {
|
|
48
|
+
await this.setConstraintsImmediate();
|
|
49
|
+
}
|
|
43
50
|
}
|
|
44
51
|
async afterEach() {
|
|
52
|
+
let violation;
|
|
53
|
+
if (this.deferredConstraintsMode === 'check') {
|
|
54
|
+
try {
|
|
55
|
+
await this.checkConstraints();
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (err?.code !== IN_FAILED_SQL_TRANSACTION)
|
|
59
|
+
violation = err;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
45
62
|
await this.rollback();
|
|
46
63
|
await this.commit();
|
|
64
|
+
if (violation)
|
|
65
|
+
throw violation;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Run the commit-time checks for every pending deferred constraint now, without committing.
|
|
69
|
+
* Postgres checks all outstanding deferred constraint events when a constraint switches from
|
|
70
|
+
* DEFERRED to IMMEDIATE, so this fails exactly where a real COMMIT would have failed.
|
|
71
|
+
* Once it passes (or throws), the pending events are consumed.
|
|
72
|
+
*/
|
|
73
|
+
async checkConstraints() {
|
|
74
|
+
try {
|
|
75
|
+
await this.setConstraintsImmediate();
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
if (err?.code !== IN_FAILED_SQL_TRANSACTION) {
|
|
79
|
+
err.message = `[pgsql-test] deferred constraint violated at end of test (a real COMMIT would have failed here):\n${err.message}`;
|
|
80
|
+
}
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async setConstraintsImmediate() {
|
|
85
|
+
await this.query('SET CONSTRAINTS ALL IMMEDIATE');
|
|
47
86
|
}
|
|
48
87
|
/**
|
|
49
88
|
* Commit current transaction to make data visible to other connections, then start fresh transaction.
|
|
@@ -53,6 +92,9 @@ class PgTestClient extends pgsql_client_1.PgClient {
|
|
|
53
92
|
await this.commit(); // make data visible to other sessions
|
|
54
93
|
await this.begin(); // fresh tx
|
|
55
94
|
await this.savepoint(); // keep rollback harness
|
|
95
|
+
if (this.deferredConstraintsMode === 'immediate') {
|
|
96
|
+
await this.setConstraintsImmediate();
|
|
97
|
+
}
|
|
56
98
|
await this.ctxQuery(); // reapply all setContext()
|
|
57
99
|
}
|
|
58
100
|
async loadJson(data) {
|