@softure-ai/db 0.1.11 → 0.1.13
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 +23 -0
- package/README.md +56 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/migrations/drizzle.d.ts +15 -3
- package/dist/migrations/drizzle.d.ts.map +1 -1
- package/dist/migrations/drizzle.js +27 -6
- package/dist/migrations/drizzle.js.map +1 -1
- package/dist/migrations/migrator.d.ts.map +1 -1
- package/dist/migrations/migrator.js +12 -2
- package/dist/migrations/migrator.js.map +1 -1
- package/dist/migrations/problems.d.ts +3 -0
- package/dist/migrations/problems.d.ts.map +1 -1
- package/dist/migrations/problems.js +4 -0
- package/dist/migrations/problems.js.map +1 -1
- package/dist/testing/global-setup.d.ts +2 -0
- package/dist/testing/global-setup.d.ts.map +1 -0
- package/dist/testing/global-setup.js +21 -0
- package/dist/testing/global-setup.js.map +1 -0
- package/dist/testing/reuse.d.ts +15 -0
- package/dist/testing/reuse.d.ts.map +1 -0
- package/dist/testing/reuse.js +155 -0
- package/dist/testing/reuse.js.map +1 -0
- package/dist/testing/template-store.d.ts +23 -0
- package/dist/testing/template-store.d.ts.map +1 -0
- package/dist/testing/template-store.js +103 -0
- package/dist/testing/template-store.js.map +1 -0
- package/dist/testing.d.ts +15 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +42 -11
- package/dist/testing.js.map +1 -1
- package/package.json +6 -1
- package/src/index.ts +1 -1
- package/src/migrations/drizzle.ts +45 -6
- package/src/migrations/migrator.ts +13 -2
- package/src/migrations/problems.ts +7 -0
- package/src/testing/global-setup.ts +21 -0
- package/src/testing/reuse.ts +205 -0
- package/src/testing/template-store.ts +116 -0
- package/src/testing.ts +57 -11
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const pools = new Map();
|
|
2
|
+
const USER_NAMESPACE = "n.nspname not in ('pg_catalog', 'information_schema') and n.nspname not like 'pg\\_toast%' and n.nspname not like 'pg\\_temp\\_%'";
|
|
3
|
+
// What a reset cannot undo. Indexes, constraints and triggers are listed with their table, enum labels with their type.
|
|
4
|
+
const FINGERPRINT_SQL = `
|
|
5
|
+
select coalesce(string_agg(entry, E'\\n' order by entry), '') as fingerprint from (
|
|
6
|
+
select format('rel %s %I.%I %s', c.relkind, n.nspname, c.relname,
|
|
7
|
+
(select string_agg(format('%I:%s', a.attname, format_type(a.atttypid, a.atttypmod)), ',' order by a.attnum)
|
|
8
|
+
from pg_attribute a where a.attrelid = c.oid and a.attnum > 0 and not a.attisdropped)) as entry
|
|
9
|
+
from pg_class c join pg_namespace n on n.oid = c.relnamespace where ${USER_NAMESPACE}
|
|
10
|
+
union all
|
|
11
|
+
select format('fn %I.%I(%s)', n.nspname, p.proname, pg_get_function_identity_arguments(p.oid))
|
|
12
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace where ${USER_NAMESPACE}
|
|
13
|
+
union all
|
|
14
|
+
select format('con %s %s %s', co.conrelid::regclass, co.conname, co.contype)
|
|
15
|
+
from pg_constraint co join pg_namespace n on n.oid = co.connamespace where ${USER_NAMESPACE}
|
|
16
|
+
union all
|
|
17
|
+
select format('trg %s %s %s', t.tgrelid::regclass, t.tgname, t.tgenabled) from pg_trigger t where not t.tgisinternal
|
|
18
|
+
union all
|
|
19
|
+
select format('enum %s %s', e.enumtypid::regtype, e.enumlabel)
|
|
20
|
+
from pg_enum e join pg_type ty on ty.oid = e.enumtypid join pg_namespace n on n.oid = ty.typnamespace where ${USER_NAMESPACE}
|
|
21
|
+
) entries`;
|
|
22
|
+
const TABLES_SQL = `
|
|
23
|
+
select format('%I.%I', n.nspname, c.relname) as name, c.relkind::text as kind, c.relispopulated as populated,
|
|
24
|
+
(select string_agg(format('%I', a.attname), ', ' order by a.attnum) from pg_attribute a
|
|
25
|
+
where a.attrelid = c.oid and a.attnum > 0 and not a.attisdropped and a.attgenerated = '') as columns
|
|
26
|
+
from pg_class c join pg_namespace n on n.oid = c.relnamespace
|
|
27
|
+
where c.relkind in ('r', 'p', 'm') and ${USER_NAMESPACE}
|
|
28
|
+
order by 1`;
|
|
29
|
+
const SEQUENCES_SQL = `
|
|
30
|
+
select format('%I.%I', schemaname, sequencename) as name, coalesce(last_value, start_value)::text as value, last_value is not null as called
|
|
31
|
+
from pg_sequences where schemaname not in ('pg_catalog', 'information_schema')`;
|
|
32
|
+
const RESTORE_SEQUENCES_SQL = "select setval(s.name::regclass, s.value, s.called) from json_to_recordset($1::json) as s(name text, value bigint, called boolean)";
|
|
33
|
+
/**
|
|
34
|
+
* A database from the pool of `key`, reset to the template's state, or a fresh copy from `open` when none is idle.
|
|
35
|
+
* Its `close` puts it back; a second `close` does nothing.
|
|
36
|
+
*/
|
|
37
|
+
export async function acquirePooledDatabase(key, open) {
|
|
38
|
+
const pool = getPool(key);
|
|
39
|
+
for (let idle = pool.idle.pop(); idle !== undefined; idle = pool.idle.pop()) {
|
|
40
|
+
if (pool.state !== undefined && (await tryRestore(idle.client, await pool.state))) {
|
|
41
|
+
return lend(pool, idle);
|
|
42
|
+
}
|
|
43
|
+
await idle.close();
|
|
44
|
+
}
|
|
45
|
+
const fresh = await open();
|
|
46
|
+
try {
|
|
47
|
+
pool.state ??= captureTemplateState(fresh.client);
|
|
48
|
+
await pool.state;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
pool.state = undefined;
|
|
52
|
+
await fresh.close();
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
return lend(pool, fresh);
|
|
56
|
+
}
|
|
57
|
+
/** Closes every idle database of every pool; for `afterAll`. A database still held is closed when it comes back. */
|
|
58
|
+
export async function closePooledDatabases() {
|
|
59
|
+
const retired = [...pools.values()];
|
|
60
|
+
pools.clear();
|
|
61
|
+
for (const pool of retired)
|
|
62
|
+
pool.isRetired = true;
|
|
63
|
+
const idle = retired.flatMap((pool) => pool.idle.splice(0));
|
|
64
|
+
await Promise.all(idle.map((database) => database.close()));
|
|
65
|
+
}
|
|
66
|
+
function getPool(key) {
|
|
67
|
+
let pool = pools.get(key);
|
|
68
|
+
if (pool === undefined) {
|
|
69
|
+
pool = { idle: [], state: undefined, isRetired: false };
|
|
70
|
+
pools.set(key, pool);
|
|
71
|
+
}
|
|
72
|
+
return pool;
|
|
73
|
+
}
|
|
74
|
+
function lend(pool, database) {
|
|
75
|
+
let isReturned = false;
|
|
76
|
+
return {
|
|
77
|
+
db: database.db,
|
|
78
|
+
client: database.client,
|
|
79
|
+
close: () => {
|
|
80
|
+
if (isReturned) {
|
|
81
|
+
return Promise.resolve();
|
|
82
|
+
}
|
|
83
|
+
isReturned = true;
|
|
84
|
+
if (pool.isRetired) {
|
|
85
|
+
return database.close();
|
|
86
|
+
}
|
|
87
|
+
pool.idle.push(database);
|
|
88
|
+
return Promise.resolve();
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function captureTemplateState(client) {
|
|
93
|
+
const fingerprint = await readFingerprint(client);
|
|
94
|
+
const relations = await client.query(TABLES_SQL);
|
|
95
|
+
const seeds = [];
|
|
96
|
+
for (const relation of relations.rows) {
|
|
97
|
+
if (relation.kind !== "r" || relation.columns === null) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const hasRows = await client.query(`select exists (select 1 from ${relation.name}) as has_rows`);
|
|
101
|
+
if (hasRows.rows[0]?.has_rows === true) {
|
|
102
|
+
const copied = await client.query(`copy ${relation.name} (${relation.columns}) to '/dev/blob'`);
|
|
103
|
+
if (copied.blob === undefined) {
|
|
104
|
+
throw new Error(`createTestDatabase: copying the template rows of ${relation.name} returned no data`);
|
|
105
|
+
}
|
|
106
|
+
seeds.push({ table: relation.name, columns: relation.columns, rows: copied.blob });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const sequences = await client.query(SEQUENCES_SQL);
|
|
110
|
+
return {
|
|
111
|
+
fingerprint,
|
|
112
|
+
tables: relations.rows.filter((relation) => relation.kind !== "m").map((relation) => relation.name),
|
|
113
|
+
seeds,
|
|
114
|
+
sequences: sequences.rows,
|
|
115
|
+
materializedViews: relations.rows.filter((relation) => relation.kind === "m" && relation.populated).map((relation) => relation.name),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function readFingerprint(client) {
|
|
119
|
+
const result = await client.query(FINGERPRINT_SQL);
|
|
120
|
+
return result.rows[0]?.fingerprint ?? "";
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Restores the template's rows and session state. `false` when the database cannot be reset (its schema changed, or
|
|
124
|
+
* the test left it in a state a reset fails on, such as an open transaction): the caller replaces it.
|
|
125
|
+
*/
|
|
126
|
+
async function tryRestore(client, state) {
|
|
127
|
+
try {
|
|
128
|
+
await client.exec("DISCARD ALL");
|
|
129
|
+
if ((await readFingerprint(client)) !== state.fingerprint) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
await client.transaction(async (tx) => {
|
|
133
|
+
// Rows go back as they were, so foreign keys and triggers stay out of the way.
|
|
134
|
+
await tx.exec("SET LOCAL session_replication_role = replica");
|
|
135
|
+
if (state.tables.length > 0) {
|
|
136
|
+
await tx.exec(`TRUNCATE ${state.tables.join(", ")} CASCADE`);
|
|
137
|
+
}
|
|
138
|
+
for (const seed of state.seeds) {
|
|
139
|
+
await tx.query(`copy ${seed.table} (${seed.columns}) from '/dev/blob'`, [], { blob: seed.rows });
|
|
140
|
+
}
|
|
141
|
+
if (state.sequences.length > 0) {
|
|
142
|
+
await tx.query(RESTORE_SEQUENCES_SQL, [JSON.stringify(state.sequences)]);
|
|
143
|
+
}
|
|
144
|
+
for (const view of state.materializedViews) {
|
|
145
|
+
await tx.exec(`REFRESH MATERIALIZED VIEW ${view}`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Deliberately not reported: a database a test broke is replaced by a fresh copy, which is what the test expects.
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=reuse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reuse.js","sourceRoot":"","sources":["../../src/testing/reuse.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AnySoftureModule } from "@softure-ai/core";
|
|
2
|
+
import type { AppMigrations } from "../migrations/migrator.js";
|
|
3
|
+
/** The variable naming the run's template folder. Set by the global setup; an app may set it itself. */
|
|
4
|
+
export declare const TEMPLATE_DIR_VARIABLE = "SOFTURE_DB_TEST_TEMPLATES";
|
|
5
|
+
export interface TemplateSource {
|
|
6
|
+
readonly modules: readonly AnySoftureModule[];
|
|
7
|
+
readonly app: AppMigrations;
|
|
8
|
+
readonly templateKey: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
export declare function getTemplateDir(): string | null;
|
|
11
|
+
/**
|
|
12
|
+
* The name of a template on disk, or `null` when it cannot be identified across processes: an app hook other than
|
|
13
|
+
* `drizzleAppMigrations` without a `templateKey`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function computeTemplateName(source: TemplateSource): Promise<string | null>;
|
|
16
|
+
/** The stored template, or `null` when this run has not written it yet. */
|
|
17
|
+
export declare function readStoredTemplate(dir: string, name: string): Promise<Blob | null>;
|
|
18
|
+
/**
|
|
19
|
+
* Writes the template under a temporary name and renames it, so a worker reading at the same time sees the whole
|
|
20
|
+
* file or none. Two workers building the same template both write it; the second rename wins with identical bytes.
|
|
21
|
+
*/
|
|
22
|
+
export declare function storeTemplate(dir: string, name: string, template: Blob): Promise<void>;
|
|
23
|
+
//# sourceMappingURL=template-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-store.d.ts","sourceRoot":"","sources":["../../src/testing/template-store.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,OAAO,KAAK,EAAoB,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAEjF,wGAAwG;AACxG,eAAO,MAAM,qBAAqB,8BAA8B,CAAC;AAKjE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC9C,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1C;AAED,wBAAgB,cAAc,IAAI,MAAM,GAAG,IAAI,CAG9C;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBxF;AAED,2EAA2E;AAC3E,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CASxF;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAS5F"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Migrated test templates on disk, shared by every test file and worker of one run (issue #641). The global setup
|
|
2
|
+
// (`@softure-ai/db/testing/global-setup`) gives the run a folder through an environment variable; without it nothing
|
|
3
|
+
// is written and each file migrates its own template, as before. A template is named by a hash of what it is built
|
|
4
|
+
// from (the module and drizzle files, the hooks' `templateKey`, the baseline), never by object identity, which means
|
|
5
|
+
// nothing in another process.
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
import { readdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
8
|
+
import { join, relative } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { getDrizzleFolder, getDrizzleLedger } from "../migrations/drizzle.js";
|
|
11
|
+
/** The variable naming the run's template folder. Set by the global setup; an app may set it itself. */
|
|
12
|
+
export const TEMPLATE_DIR_VARIABLE = "SOFTURE_DB_TEST_TEMPLATES";
|
|
13
|
+
// Bumped when the stored format or the key changes, so a folder an app keeps across runs is not misread.
|
|
14
|
+
const STORE_VERSION = "1";
|
|
15
|
+
export function getTemplateDir() {
|
|
16
|
+
const dir = process.env[TEMPLATE_DIR_VARIABLE];
|
|
17
|
+
return dir === undefined || dir === "" ? null : dir;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The name of a template on disk, or `null` when it cannot be identified across processes: an app hook other than
|
|
21
|
+
* `drizzleAppMigrations` without a `templateKey`.
|
|
22
|
+
*/
|
|
23
|
+
export async function computeTemplateName(source) {
|
|
24
|
+
const parts = [`store:${STORE_VERSION}`];
|
|
25
|
+
for (const module of source.modules) {
|
|
26
|
+
const dir = module.migrations === null || module.migrations === undefined ? "-" : await hashFolder(fileURLToPath(module.migrations.dir));
|
|
27
|
+
parts.push(`module:${module.id}:${dir}`);
|
|
28
|
+
}
|
|
29
|
+
for (const [phase, hook] of [["before", source.app.before], ["after", source.app.after]]) {
|
|
30
|
+
const identity = await identifyHook(hook, source.templateKey);
|
|
31
|
+
if (identity === null) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
parts.push(`${phase}:${identity}`);
|
|
35
|
+
}
|
|
36
|
+
parts.push(`templateKey:${source.templateKey ?? "-"}`);
|
|
37
|
+
const baseline = Object.entries(source.app.baseline ?? {}).sort(([left], [right]) => left.localeCompare(right));
|
|
38
|
+
parts.push(`baseline:${baseline.map(([id, through]) => `${id}=${through}`).join(",")}`);
|
|
39
|
+
return createHash("sha256").update(parts.join("\n")).digest("hex");
|
|
40
|
+
}
|
|
41
|
+
/** The stored template, or `null` when this run has not written it yet. */
|
|
42
|
+
export async function readStoredTemplate(dir, name) {
|
|
43
|
+
try {
|
|
44
|
+
return new Blob([await readFile(join(dir, `${name}.tar`))]);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (isMissingFile(error)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`createTestDatabase: reading the stored template ${name} in ${dir} failed`, { cause: error });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Writes the template under a temporary name and renames it, so a worker reading at the same time sees the whole
|
|
55
|
+
* file or none. Two workers building the same template both write it; the second rename wins with identical bytes.
|
|
56
|
+
*/
|
|
57
|
+
export async function storeTemplate(dir, name, template) {
|
|
58
|
+
const path = join(dir, `${name}.tar`);
|
|
59
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
60
|
+
try {
|
|
61
|
+
await writeFile(temporary, new Uint8Array(await template.arrayBuffer()));
|
|
62
|
+
await rename(temporary, path);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw new Error(`createTestDatabase: storing the template ${name} in ${dir} failed`, { cause: error });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function identifyHook(hook, templateKey) {
|
|
69
|
+
if (hook === undefined) {
|
|
70
|
+
return "-";
|
|
71
|
+
}
|
|
72
|
+
const folder = getDrizzleFolder(hook);
|
|
73
|
+
if (folder !== undefined) {
|
|
74
|
+
return `drizzle:${getDrizzleLedger(hook) ?? "-"}:${await hashFolder(folder)}`;
|
|
75
|
+
}
|
|
76
|
+
// A custom hook is a function: only the app can say which migrations it runs.
|
|
77
|
+
return templateKey === undefined ? null : "custom";
|
|
78
|
+
}
|
|
79
|
+
// Every file under the folder, by relative path and content. A missing folder hashes as such: `migrate` reports it.
|
|
80
|
+
async function hashFolder(folder) {
|
|
81
|
+
const hash = createHash("sha256");
|
|
82
|
+
let entries;
|
|
83
|
+
try {
|
|
84
|
+
entries = await readdir(folder, { recursive: true, withFileTypes: true });
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (isMissingFile(error)) {
|
|
88
|
+
return "missing";
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`createTestDatabase: reading the migrations folder ${folder} failed`, { cause: error });
|
|
91
|
+
}
|
|
92
|
+
const files = entries.filter((entry) => entry.isFile()).map((entry) => join(entry.parentPath, entry.name)).sort();
|
|
93
|
+
for (const file of files) {
|
|
94
|
+
hash.update(`${relative(folder, file)}\0`);
|
|
95
|
+
hash.update(await readFile(file));
|
|
96
|
+
hash.update("\0");
|
|
97
|
+
}
|
|
98
|
+
return hash.digest("hex");
|
|
99
|
+
}
|
|
100
|
+
function isMissingFile(error) {
|
|
101
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=template-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-store.js","sourceRoot":"","sources":["../../src/testing/template-store.ts"],"names":[],"mappings":"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"}
|
package/dist/testing.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { AnySoftureModule } from "@softure-ai/core";
|
|
|
3
3
|
import { type PgliteClientDatabase } from "./client.js";
|
|
4
4
|
import { type AppMigrations } from "./migrations/migrator.js";
|
|
5
5
|
export { DEFAULT_DATABASE_OPENERS, expectEntriesOpenDatabase, findEntriesReachingDatabase, type EntryGuardOptions, type EntryReach, } from "./entry-guard.js";
|
|
6
|
+
export { TEMPLATE_DIR_VARIABLE } from "./testing/template-store.js";
|
|
6
7
|
export interface TestDatabase {
|
|
7
8
|
readonly db: PgliteClientDatabase;
|
|
8
9
|
readonly client: PGlite;
|
|
@@ -14,6 +15,18 @@ export interface CreateTestDatabaseOptions {
|
|
|
14
15
|
* cached per hook object: define the hooks once, at module level, not inline per call.
|
|
15
16
|
*/
|
|
16
17
|
readonly app?: AppMigrations;
|
|
18
|
+
/**
|
|
19
|
+
* Names what the app hooks migrate, for the run-wide template folder of the global setup. Needed only for a hook
|
|
20
|
+
* other than `drizzleAppMigrations` (whose files are read); without it such a template is kept per test file.
|
|
21
|
+
* Change it when those migrations change, e.g. include a version.
|
|
22
|
+
*/
|
|
23
|
+
readonly templateKey?: string;
|
|
24
|
+
/**
|
|
25
|
+
* `true`: a database a test closed is reset to the template and handed out again (a few milliseconds instead of a
|
|
26
|
+
* new copy). Rows, sequences and session state are reset; a test that changes the schema gets a fresh copy next
|
|
27
|
+
* time. Two open databases never share an instance.
|
|
28
|
+
*/
|
|
29
|
+
readonly reuse?: boolean;
|
|
17
30
|
}
|
|
18
31
|
/**
|
|
19
32
|
* A fresh, migrated, in-memory database per call. List the module under test and the modules it
|
|
@@ -21,4 +34,6 @@ export interface CreateTestDatabaseOptions {
|
|
|
21
34
|
* module package or the app's migrations.
|
|
22
35
|
*/
|
|
23
36
|
export declare function createTestDatabase(modules?: readonly AnySoftureModule[], options?: CreateTestDatabaseOptions): Promise<TestDatabase>;
|
|
37
|
+
/** Closes the idle databases `reuse` keeps, so a test file can free them in `afterAll`. */
|
|
38
|
+
export declare function closeReusedTestDatabases(): Promise<void>;
|
|
24
39
|
//# sourceMappingURL=testing.d.ts.map
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAsB,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAkC,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAK9F,OAAO,EACL,wBAAwB,EACxB,yBAAyB,EACzB,2BAA2B,EAC3B,KAAK,iBAAiB,EACtB,KAAK,UAAU,GAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAEpE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,oBAAoB,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,aAAa,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAMD;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,SAAS,gBAAgB,EAAO,EAAE,OAAO,GAAE,yBAA8B,GAAG,OAAO,CAAC,YAAY,CAAC,CASlJ;AAED,2FAA2F;AAC3F,wBAAgB,wBAAwB,IAAI,OAAO,CAAC,IAAI,CAAC,CAExD"}
|
package/dist/testing.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
// Test databases for module unit tests (docs/02-module-standard.md §10): a real Postgres (PGlite)
|
|
2
|
-
// with the real migrations of the given modules. Migrating costs
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// with the real migrations of the given modules. Migrating costs seconds, so each module set is
|
|
3
|
+
// migrated once into a template, and every call starts from a copy of the template's data folder
|
|
4
|
+
// (FIRE_TRACKER `src/db/test-db.ts`). The template is kept per test file, and per run when the
|
|
5
|
+
// global setup gives the run a folder (testing/template-store.ts). `reuse` hands back a used
|
|
6
|
+
// database reset to the template instead of a new copy (testing/reuse.ts). An app with its own
|
|
7
|
+
// migrations passes the same `app` hooks (and baseline) as to `migrate`, so module tables can
|
|
8
|
+
// reference app tables and a module the app's history already creates is adopted in the template.
|
|
7
9
|
import { PGlite } from "@electric-sql/pglite";
|
|
8
10
|
import { createPgliteHandle } from "./client.js";
|
|
9
11
|
import { migrate } from "./migrations/migrator.js";
|
|
10
12
|
import { describeProblem } from "./migrations/problems.js";
|
|
13
|
+
import { acquirePooledDatabase, closePooledDatabases } from "./testing/reuse.js";
|
|
14
|
+
import { computeTemplateName, getTemplateDir, readStoredTemplate, storeTemplate } from "./testing/template-store.js";
|
|
11
15
|
export { DEFAULT_DATABASE_OPENERS, expectEntriesOpenDatabase, findEntriesReachingDatabase, } from "./entry-guard.js";
|
|
16
|
+
export { TEMPLATE_DIR_VARIABLE } from "./testing/template-store.js";
|
|
12
17
|
const templates = new Map();
|
|
13
18
|
const hookIds = new WeakMap();
|
|
14
19
|
let nextHookId = 1;
|
|
@@ -18,17 +23,28 @@ let nextHookId = 1;
|
|
|
18
23
|
* module package or the app's migrations.
|
|
19
24
|
*/
|
|
20
25
|
export async function createTestDatabase(modules = [], options = {}) {
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
26
|
+
const app = options.app ?? {};
|
|
27
|
+
const key = computeTemplateKey(modules, app, options.templateKey);
|
|
28
|
+
const openCopy = async () => {
|
|
29
|
+
const client = new PGlite({ loadDataDir: await getTemplate(key, modules, app, options.templateKey) });
|
|
30
|
+
const handle = await createPgliteHandle(client);
|
|
31
|
+
return { db: handle.db, client, close: handle.close };
|
|
32
|
+
};
|
|
33
|
+
return options.reuse === true ? acquirePooledDatabase(key, openCopy) : openCopy();
|
|
24
34
|
}
|
|
25
|
-
|
|
35
|
+
/** Closes the idle databases `reuse` keeps, so a test file can free them in `afterAll`. */
|
|
36
|
+
export function closeReusedTestDatabases() {
|
|
37
|
+
return closePooledDatabases();
|
|
38
|
+
}
|
|
39
|
+
function computeTemplateKey(modules, app, templateKey) {
|
|
26
40
|
const moduleKey = modules.map((module) => `${module.id}@${module.migrations?.dir.href ?? "-"}`).join("|");
|
|
27
41
|
const baselineKey = Object.entries(app.baseline ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([id, through]) => `${id}:${through}`).join(",");
|
|
28
|
-
|
|
42
|
+
return `${moduleKey}#before:${identifyHook(app.before)}#after:${identifyHook(app.after)}#baseline:${baselineKey}#key:${templateKey ?? "-"}`;
|
|
43
|
+
}
|
|
44
|
+
function getTemplate(key, modules, app, templateKey) {
|
|
29
45
|
let template = templates.get(key);
|
|
30
46
|
if (template === undefined) {
|
|
31
|
-
template =
|
|
47
|
+
template = loadOrBuildTemplate(modules, app, templateKey);
|
|
32
48
|
templates.set(key, template);
|
|
33
49
|
// A failed build is not cached: the next call reports the problem again.
|
|
34
50
|
template.catch(() => templates.delete(key));
|
|
@@ -47,6 +63,21 @@ function identifyHook(hook) {
|
|
|
47
63
|
}
|
|
48
64
|
return String(id);
|
|
49
65
|
}
|
|
66
|
+
// From the run's folder when another file already built it; otherwise built here and stored for the others.
|
|
67
|
+
async function loadOrBuildTemplate(modules, app, templateKey) {
|
|
68
|
+
const dir = getTemplateDir();
|
|
69
|
+
const name = dir === null ? null : await computeTemplateName({ modules, app, templateKey });
|
|
70
|
+
if (dir === null || name === null) {
|
|
71
|
+
return buildTemplate(modules, app);
|
|
72
|
+
}
|
|
73
|
+
const stored = await readStoredTemplate(dir, name);
|
|
74
|
+
if (stored !== null) {
|
|
75
|
+
return stored;
|
|
76
|
+
}
|
|
77
|
+
const built = await buildTemplate(modules, app);
|
|
78
|
+
await storeTemplate(dir, name, built);
|
|
79
|
+
return built;
|
|
80
|
+
}
|
|
50
81
|
async function buildTemplate(modules, app) {
|
|
51
82
|
const handle = await createPgliteHandle(new PGlite());
|
|
52
83
|
try {
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softure-ai/db",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "SOFTURE AI database layer: pg/PGlite client, per-module migrator with plan and adopt, PGlite test databases.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
"types": "./dist/testing.d.ts",
|
|
39
39
|
"default": "./dist/testing.js"
|
|
40
40
|
},
|
|
41
|
+
"./testing/global-setup": {
|
|
42
|
+
"@softure-ai/source": "./src/testing/global-setup.ts",
|
|
43
|
+
"types": "./dist/testing/global-setup.d.ts",
|
|
44
|
+
"default": "./dist/testing/global-setup.js"
|
|
45
|
+
},
|
|
41
46
|
"./cli": {
|
|
42
47
|
"@softure-ai/source": "./src/cli/index.ts",
|
|
43
48
|
"types": "./dist/cli/index.d.ts",
|
package/src/index.ts
CHANGED
|
@@ -40,7 +40,7 @@ export {
|
|
|
40
40
|
type MigrationReport,
|
|
41
41
|
type MigrationStep,
|
|
42
42
|
} from "./migrations/migrator.js";
|
|
43
|
-
export { drizzleAppMigrations } from "./migrations/drizzle.js";
|
|
43
|
+
export { drizzleAppMigrations, type DrizzleAppMigrationsOptions } from "./migrations/drizzle.js";
|
|
44
44
|
export { adoptModule, type AdoptionReport, type AdoptOptions } from "./migrations/adopt.js";
|
|
45
45
|
export { exportMigrations } from "./migrations/export.js";
|
|
46
46
|
export {
|
|
@@ -1,22 +1,61 @@
|
|
|
1
1
|
// The app's own drizzle-kit migrations as an app migration hook: drizzle's migrator for the handle's driver,
|
|
2
2
|
// loaded only when the hook runs, so an app ships just its own driver.
|
|
3
|
+
import { resolve } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import type { AppMigrationHook } from "./migrator.js";
|
|
5
6
|
|
|
7
|
+
export interface DrizzleAppMigrationsOptions {
|
|
8
|
+
/** drizzle's ledger table for this folder. Default: `__drizzle_migrations`. */
|
|
9
|
+
readonly migrationsTable?: string;
|
|
10
|
+
/** The schema of that table. Default: `drizzle`. */
|
|
11
|
+
readonly migrationsSchema?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DEFAULT_TABLE = "__drizzle_migrations";
|
|
15
|
+
const DEFAULT_SCHEMA = "drizzle";
|
|
16
|
+
|
|
17
|
+
interface DrizzleHookSource {
|
|
18
|
+
/** `schema.table`, so `migrate` can refuse a `before` and an `after` sharing one (issue #534). */
|
|
19
|
+
readonly ledger: string;
|
|
20
|
+
/** The absolute folder, so a test template can be identified by its files across processes (issue #641). */
|
|
21
|
+
readonly folder: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const sources = new WeakMap<AppMigrationHook, DrizzleHookSource>();
|
|
25
|
+
|
|
6
26
|
/**
|
|
7
27
|
* A hook for `app.before` (or `app.after`) that applies the drizzle-kit folder (`meta/_journal.json` and its SQL
|
|
8
|
-
* files) with drizzle's migrator. Drizzle skips what its ledger (`drizzle.__drizzle_migrations`
|
|
9
|
-
* the hook is safe on every `migrate` run.
|
|
28
|
+
* files) with drizzle's migrator. Drizzle skips what its ledger (`drizzle.__drizzle_migrations` unless `options`
|
|
29
|
+
* names another) already holds, so the hook is safe on every `migrate` run. Drizzle reads only the newest row of the
|
|
30
|
+
* ledger, so a second folder needs a ledger of its own. `folder` is a path (relative to the working directory) or a
|
|
31
|
+
* file URL.
|
|
10
32
|
*/
|
|
11
|
-
export function drizzleAppMigrations(folder: string | URL): AppMigrationHook {
|
|
33
|
+
export function drizzleAppMigrations(folder: string | URL, options: DrizzleAppMigrationsOptions = {}): AppMigrationHook {
|
|
12
34
|
const migrationsFolder = typeof folder === "string" ? folder : fileURLToPath(folder);
|
|
13
|
-
|
|
35
|
+
const config = {
|
|
36
|
+
migrationsFolder,
|
|
37
|
+
migrationsTable: options.migrationsTable ?? DEFAULT_TABLE,
|
|
38
|
+
migrationsSchema: options.migrationsSchema ?? DEFAULT_SCHEMA,
|
|
39
|
+
};
|
|
40
|
+
const hook: AppMigrationHook = async (handle) => {
|
|
14
41
|
if (handle.kind === "postgres") {
|
|
15
42
|
const { migrate } = await import("drizzle-orm/node-postgres/migrator");
|
|
16
|
-
await migrate(handle.db,
|
|
43
|
+
await migrate(handle.db, config);
|
|
17
44
|
return;
|
|
18
45
|
}
|
|
19
46
|
const { migrate } = await import("drizzle-orm/pglite/migrator");
|
|
20
|
-
await migrate(handle.db,
|
|
47
|
+
await migrate(handle.db, config);
|
|
21
48
|
};
|
|
49
|
+
sources.set(hook, { ledger: `${config.migrationsSchema}.${config.migrationsTable}`, folder: resolve(migrationsFolder) });
|
|
50
|
+
return hook;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The ledger (`schema.table`) a `drizzleAppMigrations` hook writes to; `undefined` for any other hook. */
|
|
54
|
+
export function getDrizzleLedger(hook: AppMigrationHook): string | undefined {
|
|
55
|
+
return sources.get(hook)?.ledger;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The absolute folder a `drizzleAppMigrations` hook applies; `undefined` for any other hook. */
|
|
59
|
+
export function getDrizzleFolder(hook: AppMigrationHook): string | undefined {
|
|
60
|
+
return sources.get(hook)?.folder;
|
|
22
61
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// `app.baseline` names the module files the app's own history already creates (issue #152).
|
|
7
7
|
import { ok, sortModulesByDependencies, type AnySoftureModule } from "@softure-ai/core";
|
|
8
8
|
import type { DatabaseHandle } from "../client.js";
|
|
9
|
+
import { getDrizzleLedger } from "./drizzle.js";
|
|
9
10
|
import { applyFile, describeError, recordAdoption, withMigrationLock, type MigrationUnit } from "./apply.js";
|
|
10
11
|
import { readMigrationFiles } from "./files.js";
|
|
11
12
|
import { describeSchema, diffSchemas } from "./introspect.js";
|
|
@@ -98,7 +99,7 @@ export async function planMigrations(handle: DatabaseHandle, options: MigrateOpt
|
|
|
98
99
|
return units;
|
|
99
100
|
}
|
|
100
101
|
const baseline = options.app?.baseline ?? {};
|
|
101
|
-
const baselineProblems = checkBaseline(options.modules, units.value, baseline);
|
|
102
|
+
const baselineProblems = [...checkAppHooks(options.app ?? {}), ...checkBaseline(options.modules, units.value, baseline)];
|
|
102
103
|
if (baselineProblems.length > 0) {
|
|
103
104
|
return failWith(baselineProblems);
|
|
104
105
|
}
|
|
@@ -122,7 +123,7 @@ export async function migrate(handle: DatabaseHandle, options: MigrateOptions):
|
|
|
122
123
|
}
|
|
123
124
|
const app = options.app ?? {};
|
|
124
125
|
const baseline = app.baseline ?? {};
|
|
125
|
-
const baselineProblems = checkBaseline(options.modules, units.value, baseline);
|
|
126
|
+
const baselineProblems = [...checkAppHooks(app), ...checkBaseline(options.modules, units.value, baseline)];
|
|
126
127
|
if (baselineProblems.length > 0) {
|
|
127
128
|
return failWith(baselineProblems);
|
|
128
129
|
}
|
|
@@ -246,6 +247,16 @@ function checkBaseline(
|
|
|
246
247
|
});
|
|
247
248
|
}
|
|
248
249
|
|
|
250
|
+
/**
|
|
251
|
+
* `before` and `after` must not be drizzle hooks on one ledger: drizzle applies only files newer than the ledger's
|
|
252
|
+
* newest row, so every `after` file older than the newest `before` file would be skipped silently (issue #534).
|
|
253
|
+
*/
|
|
254
|
+
function checkAppHooks(app: AppMigrations): MigrationProblem[] {
|
|
255
|
+
const before = app.before === undefined ? undefined : getDrizzleLedger(app.before);
|
|
256
|
+
const after = app.after === undefined ? undefined : getDrizzleLedger(app.after);
|
|
257
|
+
return before !== undefined && before === after ? [{ code: "db.app_migrations_shared_ledger", ledger: before }] : [];
|
|
258
|
+
}
|
|
259
|
+
|
|
249
260
|
/** `through` must be a whole number from 1 to the unit's file count. */
|
|
250
261
|
export function checkThrough(unit: MigrationUnit, through: number): MigrationProblem[] {
|
|
251
262
|
const files = unit.files.length;
|
|
@@ -11,6 +11,7 @@ export type MigrationProblem =
|
|
|
11
11
|
| { readonly code: "db.migration_out_of_order"; readonly module: string; readonly version: number; readonly appliedVersion: number }
|
|
12
12
|
| { readonly code: "db.migration_failed"; readonly module: string; readonly version: number; readonly name: string; readonly reason: string }
|
|
13
13
|
| { readonly code: "db.app_migration_failed"; readonly phase: "before" | "after"; readonly reason: string }
|
|
14
|
+
| { readonly code: "db.app_migrations_shared_ledger"; readonly ledger: string }
|
|
14
15
|
| { readonly code: "db.reserved_module"; readonly module: string }
|
|
15
16
|
| { readonly code: "db.migrations_without_schema"; readonly module: string }
|
|
16
17
|
| { readonly code: "db.dependency_cycle"; readonly modules: readonly string[] }
|
|
@@ -60,6 +61,12 @@ export function describeProblem(problem: MigrationProblem): string {
|
|
|
60
61
|
return problem.phase === "before"
|
|
61
62
|
? `app: the before migrations failed: ${problem.reason}; no module migration ran`
|
|
62
63
|
: `app: the after migrations failed: ${problem.reason}; the module migrations before it stay applied`;
|
|
64
|
+
case "db.app_migrations_shared_ledger":
|
|
65
|
+
return (
|
|
66
|
+
`app: the before and after drizzle migrations share the ledger ${problem.ledger}, so drizzle would skip every ` +
|
|
67
|
+
"after file older than the newest before file; give the after folder its own ledger " +
|
|
68
|
+
'(drizzleAppMigrations(folder, { migrationsTable: "__drizzle_migrations_after" }))'
|
|
69
|
+
);
|
|
63
70
|
case "db.reserved_module":
|
|
64
71
|
return `${problem.module}: uses a reserved id or schema (the id "softure"; the schemas softure, public, information_schema, drizzle, pg_* and names over 63 bytes)`;
|
|
65
72
|
case "db.migrations_without_schema":
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// A Vitest `globalSetup` module (issue #641): `test: { globalSetup: ["@softure-ai/db/testing/global-setup"] }`.
|
|
2
|
+
// It runs once in the main process before any worker starts and gives the run a template folder; workers inherit the
|
|
3
|
+
// variable, so `createTestDatabase` migrates each module set once per run instead of once per test file. The folder
|
|
4
|
+
// is removed when the run ends. A folder the app named itself (the variable already set) is kept.
|
|
5
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { TEMPLATE_DIR_VARIABLE } from "./template-store.js";
|
|
9
|
+
|
|
10
|
+
export default async function setup(): Promise<() => Promise<void>> {
|
|
11
|
+
const given = process.env[TEMPLATE_DIR_VARIABLE];
|
|
12
|
+
if (given !== undefined && given !== "") {
|
|
13
|
+
return () => Promise.resolve();
|
|
14
|
+
}
|
|
15
|
+
const dir = await mkdtemp(join(tmpdir(), "softure-db-templates-"));
|
|
16
|
+
process.env[TEMPLATE_DIR_VARIABLE] = dir;
|
|
17
|
+
return async () => {
|
|
18
|
+
delete process.env[TEMPLATE_DIR_VARIABLE];
|
|
19
|
+
await rm(dir, { recursive: true, force: true });
|
|
20
|
+
};
|
|
21
|
+
}
|