orez 0.4.30 → 0.4.31
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/dist/config.js +3 -3
- package/dist/fk-cascade.d.ts +102 -0
- package/dist/fk-cascade.d.ts.map +1 -0
- package/dist/fk-cascade.js +233 -0
- package/dist/fk-cascade.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +63 -37
- package/dist/index.js.map +1 -1
- package/dist/pg-proxy-do-backend.d.ts +1 -0
- package/dist/pg-proxy-do-backend.d.ts.map +1 -1
- package/dist/pg-proxy-do-backend.js +114 -0
- package/dist/pg-proxy-do-backend.js.map +1 -1
- package/dist/pglite-manager.d.ts +12 -9
- package/dist/pglite-manager.d.ts.map +1 -1
- package/dist/pglite-manager.js +61 -31
- package/dist/pglite-manager.js.map +1 -1
- package/dist/recovery.d.ts +7 -6
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +5 -4
- package/dist/recovery.js.map +1 -1
- package/dist/worker/cf-patches.d.ts +4 -2
- package/dist/worker/cf-patches.d.ts.map +1 -1
- package/dist/worker/cf-patches.js +15 -2
- package/dist/worker/cf-patches.js.map +1 -1
- package/dist/worker/shims/postgres.d.ts.map +1 -1
- package/dist/worker/shims/postgres.js +112 -0
- package/dist/worker/shims/postgres.js.map +1 -1
- package/dist/worker/zero-cache-embed.d.ts.map +1 -1
- package/dist/worker/zero-cache-embed.js +4 -0
- package/dist/worker/zero-cache-embed.js.map +1 -1
- package/dist/zero-changelog-cleanup-patch.d.ts +18 -0
- package/dist/zero-changelog-cleanup-patch.d.ts.map +1 -0
- package/dist/zero-changelog-cleanup-patch.js +63 -0
- package/dist/zero-changelog-cleanup-patch.js.map +1 -0
- package/package.json +2 -2
package/dist/config.js
CHANGED
|
@@ -6,9 +6,9 @@ export function defineConfig(config) {
|
|
|
6
6
|
export function getConfig(overrides = {}) {
|
|
7
7
|
return {
|
|
8
8
|
dataDir: overrides.dataDir || '.orez',
|
|
9
|
-
pgPort: overrides.pgPort
|
|
10
|
-
zeroPort: overrides.zeroPort
|
|
11
|
-
adminPort: overrides.adminPort
|
|
9
|
+
pgPort: overrides.pgPort ?? 6434,
|
|
10
|
+
zeroPort: overrides.zeroPort ?? 5849,
|
|
11
|
+
adminPort: overrides.adminPort ?? 0,
|
|
12
12
|
pgUser: overrides.pgUser || 'user',
|
|
13
13
|
pgPassword: overrides.pgPassword || 'password',
|
|
14
14
|
migrationsDir: overrides.migrationsDir || '',
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fk-cascade — pg→sqlite compat for ON DELETE actions.
|
|
3
|
+
*
|
|
4
|
+
* orez's backends drop every FOREIGN KEY constraint when translating PG DDL to
|
|
5
|
+
* their store (the PGlite shim regex-strips them; the DO backend filters the
|
|
6
|
+
* CONSTR_FOREIGN AST nodes). that leaves the store with no FK enforcement and,
|
|
7
|
+
* crucially, no `ON DELETE CASCADE` / `ON DELETE SET NULL` — so a plain
|
|
8
|
+
* `DELETE FROM parent` orphans every child row instead of cascading.
|
|
9
|
+
*
|
|
10
|
+
* native engine cascade is not an option here: change capture is trigger-based
|
|
11
|
+
* (`_zero_change_trigger` / `_zero_track_change`), and SQLite does NOT fire
|
|
12
|
+
* triggers for native foreign-key cascade actions unless `recursive_triggers`
|
|
13
|
+
* is on. an engine-level cascade would delete child rows invisibly, so zero
|
|
14
|
+
* clients would never learn the rows are gone — strictly worse than orphaning.
|
|
15
|
+
*
|
|
16
|
+
* instead we restore faithful PG semantics by EXPANSION: capture the FK edges
|
|
17
|
+
* the backends were about to drop, and rewrite each `DELETE FROM parent` into
|
|
18
|
+
* an ordered set of explicit, set-based child statements (leaves-first) plus
|
|
19
|
+
* the original parent delete. every emitted statement is a real DELETE/UPDATE,
|
|
20
|
+
* so it goes through the same change-tracking trigger and replicates to zero
|
|
21
|
+
* clients exactly like any other write. one implementation, both backends.
|
|
22
|
+
*
|
|
23
|
+
* this module is pure: it operates on already-parsed libpg_query CREATE TABLE
|
|
24
|
+
* AST nodes (for edge capture) and plain SQL strings (for expansion). the
|
|
25
|
+
* caller owns naming — it passes a `resolveTable` that produces the canonical
|
|
26
|
+
* SQL identifier for a table (schema-qualified PG name). that identifier doubles
|
|
27
|
+
* as the registry key, so capture and expansion stay consistent. the expansion
|
|
28
|
+
* emits PG SQL under those names; the DO backend re-runs each child through its
|
|
29
|
+
* normal rewrite (flatten + change-track), while PGlite runs them directly.
|
|
30
|
+
*/
|
|
31
|
+
/** the ON DELETE actions we can faithfully expand. restrict/no-action/set-default are left alone (no enforcement, matching today). */
|
|
32
|
+
export type FkDeleteAction = 'cascade' | 'set-null';
|
|
33
|
+
export interface FkChild {
|
|
34
|
+
/** canonical SQL identifier of the child (referencing) table; also its registry key. */
|
|
35
|
+
table: string;
|
|
36
|
+
/** child FK column identifiers (already quoted), in FK order. */
|
|
37
|
+
columns: string[];
|
|
38
|
+
/** referenced parent column identifiers (already quoted), aligned with `columns`. */
|
|
39
|
+
refColumns: string[];
|
|
40
|
+
onDelete: FkDeleteAction;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* parent-table → children that reference it. built once from DDL, consulted on
|
|
44
|
+
* every DELETE. keyed by the canonical table identifier `resolveTable` produces.
|
|
45
|
+
*/
|
|
46
|
+
export declare class FkCascadeRegistry {
|
|
47
|
+
private byParent;
|
|
48
|
+
add(parentKey: string, child: FkChild): void;
|
|
49
|
+
childrenOf(parentKey: string): readonly FkChild[];
|
|
50
|
+
/** every (parentKey, child) edge — for persisting the registry to durable storage. */
|
|
51
|
+
entries(): IterableIterator<readonly [string, FkChild]>;
|
|
52
|
+
/** true once any cascade/set-null edge has been captured — lets backends skip the DELETE parse on FK-free schemas. */
|
|
53
|
+
get hasEdges(): boolean;
|
|
54
|
+
clear(): void;
|
|
55
|
+
}
|
|
56
|
+
export interface TableRef {
|
|
57
|
+
schemaname?: string;
|
|
58
|
+
relname: string;
|
|
59
|
+
}
|
|
60
|
+
export type ResolveTable = (ref: TableRef) => string;
|
|
61
|
+
export declare function quoteIdent(name: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* capture every cascade/set-null FK edge declared by a CREATE TABLE statement
|
|
64
|
+
* into `registry`. handles both inline column FKs (`col … REFERENCES parent(c)`)
|
|
65
|
+
* and table-level FKs (`FOREIGN KEY (col) REFERENCES parent(c)`). call this at
|
|
66
|
+
* the exact site each backend drops the constraint, BEFORE it is discarded.
|
|
67
|
+
*/
|
|
68
|
+
export declare function recordCreateTableForeignKeys(createStmt: {
|
|
69
|
+
relation?: TableRef;
|
|
70
|
+
tableElts?: unknown[];
|
|
71
|
+
} | undefined, registry: FkCascadeRegistry, resolveTable: ResolveTable): number;
|
|
72
|
+
/**
|
|
73
|
+
* capture cascade/set-null FK edges from an `ALTER TABLE … ADD CONSTRAINT …
|
|
74
|
+
* FOREIGN KEY …` statement (drizzle's standard FK form — it emits FKs as a
|
|
75
|
+
* separate ALTER, not inline in CREATE TABLE). call at the site the backend
|
|
76
|
+
* drops the constraint. returns the number of edges recorded.
|
|
77
|
+
*/
|
|
78
|
+
export declare function recordAlterTableForeignKeys(alterStmt: {
|
|
79
|
+
relation?: TableRef;
|
|
80
|
+
cmds?: unknown[];
|
|
81
|
+
} | undefined, registry: FkCascadeRegistry, resolveTable: ResolveTable): number;
|
|
82
|
+
export interface ExpandOptions {
|
|
83
|
+
/** guard against pathological / cyclic graphs. */
|
|
84
|
+
maxDepth?: number;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* expand a `DELETE FROM target [WHERE wherePredicate]` into the ordered list of
|
|
88
|
+
* statements that reproduces PG's ON DELETE semantics. children are emitted
|
|
89
|
+
* leaves-first (grandchildren before children) so every delete runs before the
|
|
90
|
+
* rows it depends on disappear; the returned list does NOT include the original
|
|
91
|
+
* parent delete — the caller still runs that, after these.
|
|
92
|
+
*
|
|
93
|
+
* each statement embeds the full original predicate via nested subqueries, so
|
|
94
|
+
* for a parameterized DELETE the SAME bound params apply unchanged to every
|
|
95
|
+
* emitted statement.
|
|
96
|
+
*
|
|
97
|
+
* cascade → `DELETE FROM child WHERE childCols IN (SELECT … )`
|
|
98
|
+
* set-null → `UPDATE child SET childCols = NULL WHERE childCols IN (SELECT … )`
|
|
99
|
+
* (SET NULL does not delete the child, so its subtree is not recursed)
|
|
100
|
+
*/
|
|
101
|
+
export declare function expandDelete(target: string, wherePredicate: string | null, registry: FkCascadeRegistry, options?: ExpandOptions): string[];
|
|
102
|
+
//# sourceMappingURL=fk-cascade.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fk-cascade.d.ts","sourceRoot":"","sources":["../src/fk-cascade.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,sIAAsI;AACtI,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,UAAU,CAAA;AAEnD,MAAM,WAAW,OAAO;IACtB,wFAAwF;IACxF,KAAK,EAAE,MAAM,CAAA;IACb,iEAAiE;IACjE,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,qFAAqF;IACrF,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,QAAQ,EAAE,cAAc,CAAA;CACzB;AAED;;;GAGG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAA+B;IAE/C,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAY5C,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,OAAO,EAAE;IAIjD,sFAAsF;IACrF,OAAO,IAAI,gBAAgB,CAAC,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAMxD,sHAAsH;IACtH,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,KAAK,IAAI,IAAI;CAGd;AAcD,MAAM,WAAW,QAAQ;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,QAAQ,KAAK,MAAM,CAAA;AASpD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAaD;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE;IAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,EAAE,CAAA;CAAE,GAAG,SAAS,EACtE,QAAQ,EAAE,iBAAiB,EAC3B,YAAY,EAAE,YAAY,GACzB,MAAM,CAyCR;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,SAAS,EAAE;IAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAA;CAAE,GAAG,SAAS,EAChE,QAAQ,EAAE,iBAAiB,EAC3B,YAAY,EAAE,YAAY,GACzB,MAAM,CAwBR;AAqDD,MAAM,WAAW,aAAa;IAC5B,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GAAG,IAAI,EAC7B,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,GAAE,aAAkB,GAC1B,MAAM,EAAE,CA6BV"}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fk-cascade — pg→sqlite compat for ON DELETE actions.
|
|
3
|
+
*
|
|
4
|
+
* orez's backends drop every FOREIGN KEY constraint when translating PG DDL to
|
|
5
|
+
* their store (the PGlite shim regex-strips them; the DO backend filters the
|
|
6
|
+
* CONSTR_FOREIGN AST nodes). that leaves the store with no FK enforcement and,
|
|
7
|
+
* crucially, no `ON DELETE CASCADE` / `ON DELETE SET NULL` — so a plain
|
|
8
|
+
* `DELETE FROM parent` orphans every child row instead of cascading.
|
|
9
|
+
*
|
|
10
|
+
* native engine cascade is not an option here: change capture is trigger-based
|
|
11
|
+
* (`_zero_change_trigger` / `_zero_track_change`), and SQLite does NOT fire
|
|
12
|
+
* triggers for native foreign-key cascade actions unless `recursive_triggers`
|
|
13
|
+
* is on. an engine-level cascade would delete child rows invisibly, so zero
|
|
14
|
+
* clients would never learn the rows are gone — strictly worse than orphaning.
|
|
15
|
+
*
|
|
16
|
+
* instead we restore faithful PG semantics by EXPANSION: capture the FK edges
|
|
17
|
+
* the backends were about to drop, and rewrite each `DELETE FROM parent` into
|
|
18
|
+
* an ordered set of explicit, set-based child statements (leaves-first) plus
|
|
19
|
+
* the original parent delete. every emitted statement is a real DELETE/UPDATE,
|
|
20
|
+
* so it goes through the same change-tracking trigger and replicates to zero
|
|
21
|
+
* clients exactly like any other write. one implementation, both backends.
|
|
22
|
+
*
|
|
23
|
+
* this module is pure: it operates on already-parsed libpg_query CREATE TABLE
|
|
24
|
+
* AST nodes (for edge capture) and plain SQL strings (for expansion). the
|
|
25
|
+
* caller owns naming — it passes a `resolveTable` that produces the canonical
|
|
26
|
+
* SQL identifier for a table (schema-qualified PG name). that identifier doubles
|
|
27
|
+
* as the registry key, so capture and expansion stay consistent. the expansion
|
|
28
|
+
* emits PG SQL under those names; the DO backend re-runs each child through its
|
|
29
|
+
* normal rewrite (flatten + change-track), while PGlite runs them directly.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* parent-table → children that reference it. built once from DDL, consulted on
|
|
33
|
+
* every DELETE. keyed by the canonical table identifier `resolveTable` produces.
|
|
34
|
+
*/
|
|
35
|
+
export class FkCascadeRegistry {
|
|
36
|
+
byParent = new Map();
|
|
37
|
+
add(parentKey, child) {
|
|
38
|
+
const list = this.byParent.get(parentKey);
|
|
39
|
+
if (!list) {
|
|
40
|
+
this.byParent.set(parentKey, [child]);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// dedupe: the same DDL can be re-applied (CREATE TABLE IF NOT EXISTS, a boot
|
|
44
|
+
// load followed by a re-sent migration) — a duplicate edge doubles the cascade.
|
|
45
|
+
if (list.some((existing) => sameEdge(existing, child)))
|
|
46
|
+
return;
|
|
47
|
+
list.push(child);
|
|
48
|
+
}
|
|
49
|
+
childrenOf(parentKey) {
|
|
50
|
+
return this.byParent.get(parentKey) ?? EMPTY;
|
|
51
|
+
}
|
|
52
|
+
/** every (parentKey, child) edge — for persisting the registry to durable storage. */
|
|
53
|
+
*entries() {
|
|
54
|
+
for (const [parentKey, children] of this.byParent) {
|
|
55
|
+
for (const child of children)
|
|
56
|
+
yield [parentKey, child];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** true once any cascade/set-null edge has been captured — lets backends skip the DELETE parse on FK-free schemas. */
|
|
60
|
+
get hasEdges() {
|
|
61
|
+
return this.byParent.size > 0;
|
|
62
|
+
}
|
|
63
|
+
clear() {
|
|
64
|
+
this.byParent.clear();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const EMPTY = Object.freeze([]);
|
|
68
|
+
function sameEdge(a, b) {
|
|
69
|
+
return (a.table === b.table &&
|
|
70
|
+
a.onDelete === b.onDelete &&
|
|
71
|
+
a.columns.length === b.columns.length &&
|
|
72
|
+
a.columns.every((c, i) => c === b.columns[i]) &&
|
|
73
|
+
a.refColumns.every((c, i) => c === b.refColumns[i]));
|
|
74
|
+
}
|
|
75
|
+
/** libpg_query fk_del_action codes → the action we expand (others: no enforcement, skip). */
|
|
76
|
+
const DELETE_ACTION = {
|
|
77
|
+
c: 'cascade', // CASCADE
|
|
78
|
+
n: 'set-null', // SET NULL
|
|
79
|
+
// a: NO ACTION, r: RESTRICT, d: SET DEFAULT — no faithful expansion, leave unenforced
|
|
80
|
+
};
|
|
81
|
+
export function quoteIdent(name) {
|
|
82
|
+
return '"' + name.replace(/"/g, '""') + '"';
|
|
83
|
+
}
|
|
84
|
+
function stringValues(nodes) {
|
|
85
|
+
if (!Array.isArray(nodes))
|
|
86
|
+
return [];
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const node of nodes) {
|
|
89
|
+
const sval = node?.String;
|
|
90
|
+
const value = sval?.sval ?? sval?.str;
|
|
91
|
+
if (typeof value === 'string')
|
|
92
|
+
out.push(value);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* capture every cascade/set-null FK edge declared by a CREATE TABLE statement
|
|
98
|
+
* into `registry`. handles both inline column FKs (`col … REFERENCES parent(c)`)
|
|
99
|
+
* and table-level FKs (`FOREIGN KEY (col) REFERENCES parent(c)`). call this at
|
|
100
|
+
* the exact site each backend drops the constraint, BEFORE it is discarded.
|
|
101
|
+
*/
|
|
102
|
+
export function recordCreateTableForeignKeys(createStmt, registry, resolveTable) {
|
|
103
|
+
const relation = createStmt?.relation;
|
|
104
|
+
if (!relation?.relname)
|
|
105
|
+
return 0;
|
|
106
|
+
const childTable = resolveTable(relation);
|
|
107
|
+
let recorded = 0;
|
|
108
|
+
for (const elt of createStmt?.tableElts ?? []) {
|
|
109
|
+
const node = elt;
|
|
110
|
+
// table-level: FOREIGN KEY (a, b) REFERENCES parent (x, y)
|
|
111
|
+
if (node.Constraint?.contype === 'CONSTR_FOREIGN') {
|
|
112
|
+
if (addForeignKey(registry, childTable, node.Constraint, stringValues(node.Constraint.fk_attrs), resolveTable)) {
|
|
113
|
+
recorded++;
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// inline column: col … REFERENCES parent (x)
|
|
118
|
+
const col = node.ColumnDef;
|
|
119
|
+
if (col?.colname && Array.isArray(col.constraints)) {
|
|
120
|
+
for (const c of col.constraints) {
|
|
121
|
+
const constraint = c.Constraint;
|
|
122
|
+
if (constraint?.contype === 'CONSTR_FOREIGN') {
|
|
123
|
+
if (addForeignKey(registry, childTable, constraint, [col.colname], resolveTable)) {
|
|
124
|
+
recorded++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return recorded;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* capture cascade/set-null FK edges from an `ALTER TABLE … ADD CONSTRAINT …
|
|
134
|
+
* FOREIGN KEY …` statement (drizzle's standard FK form — it emits FKs as a
|
|
135
|
+
* separate ALTER, not inline in CREATE TABLE). call at the site the backend
|
|
136
|
+
* drops the constraint. returns the number of edges recorded.
|
|
137
|
+
*/
|
|
138
|
+
export function recordAlterTableForeignKeys(alterStmt, registry, resolveTable) {
|
|
139
|
+
const relation = alterStmt?.relation;
|
|
140
|
+
if (!relation?.relname)
|
|
141
|
+
return 0;
|
|
142
|
+
const childTable = resolveTable(relation);
|
|
143
|
+
let recorded = 0;
|
|
144
|
+
for (const cmd of alterStmt?.cmds ?? []) {
|
|
145
|
+
const command = cmd
|
|
146
|
+
?.AlterTableCmd;
|
|
147
|
+
if (command?.subtype !== 'AT_AddConstraint')
|
|
148
|
+
continue;
|
|
149
|
+
const constraint = command.def?.Constraint;
|
|
150
|
+
if (constraint?.contype !== 'CONSTR_FOREIGN')
|
|
151
|
+
continue;
|
|
152
|
+
if (addForeignKey(registry, childTable, constraint, stringValues(constraint.fk_attrs), resolveTable)) {
|
|
153
|
+
recorded++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return recorded;
|
|
157
|
+
}
|
|
158
|
+
function addForeignKey(registry, childTable, constraint, childColumns, resolveTable) {
|
|
159
|
+
const onDelete = DELETE_ACTION[constraint.fk_del_action ?? 'a'];
|
|
160
|
+
if (!onDelete)
|
|
161
|
+
return false; // restrict / no-action / set-default: nothing to expand
|
|
162
|
+
const parent = constraint.pktable;
|
|
163
|
+
if (!parent?.relname)
|
|
164
|
+
return false;
|
|
165
|
+
const refColumns = stringValues(constraint.pk_attrs);
|
|
166
|
+
// need an explicit referenced column to build the IN-subquery; drizzle always
|
|
167
|
+
// emits one. without it we can't faithfully expand, so skip rather than guess.
|
|
168
|
+
if (childColumns.length === 0 || refColumns.length !== childColumns.length)
|
|
169
|
+
return false;
|
|
170
|
+
registry.add(resolveTable(parent), {
|
|
171
|
+
table: childTable,
|
|
172
|
+
columns: childColumns.map(quoteIdent),
|
|
173
|
+
refColumns: refColumns.map(quoteIdent),
|
|
174
|
+
onDelete,
|
|
175
|
+
});
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
function columnTuple(columns) {
|
|
179
|
+
return columns.length === 1 ? columns[0] : `(${columns.join(', ')})`;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* build the set-based predicate selecting child rows whose FK points at the
|
|
183
|
+
* parent rows matched by `parentPredicate`:
|
|
184
|
+
* childCols IN (SELECT refCols FROM parentTable WHERE parentPredicate)
|
|
185
|
+
*/
|
|
186
|
+
function childMatchPredicate(child, parentTable, parentPredicate) {
|
|
187
|
+
const where = parentPredicate ? ` WHERE ${parentPredicate}` : '';
|
|
188
|
+
const select = `SELECT ${child.refColumns.join(', ')} FROM ${parentTable}${where}`;
|
|
189
|
+
return `${columnTuple(child.columns)} IN (${select})`;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* expand a `DELETE FROM target [WHERE wherePredicate]` into the ordered list of
|
|
193
|
+
* statements that reproduces PG's ON DELETE semantics. children are emitted
|
|
194
|
+
* leaves-first (grandchildren before children) so every delete runs before the
|
|
195
|
+
* rows it depends on disappear; the returned list does NOT include the original
|
|
196
|
+
* parent delete — the caller still runs that, after these.
|
|
197
|
+
*
|
|
198
|
+
* each statement embeds the full original predicate via nested subqueries, so
|
|
199
|
+
* for a parameterized DELETE the SAME bound params apply unchanged to every
|
|
200
|
+
* emitted statement.
|
|
201
|
+
*
|
|
202
|
+
* cascade → `DELETE FROM child WHERE childCols IN (SELECT … )`
|
|
203
|
+
* set-null → `UPDATE child SET childCols = NULL WHERE childCols IN (SELECT … )`
|
|
204
|
+
* (SET NULL does not delete the child, so its subtree is not recursed)
|
|
205
|
+
*/
|
|
206
|
+
export function expandDelete(target, wherePredicate, registry, options = {}) {
|
|
207
|
+
if (!registry.hasEdges)
|
|
208
|
+
return [];
|
|
209
|
+
const maxDepth = options.maxDepth ?? 32;
|
|
210
|
+
const out = [];
|
|
211
|
+
const visit = (table, predicate, depth, path) => {
|
|
212
|
+
if (depth > maxDepth)
|
|
213
|
+
return;
|
|
214
|
+
for (const child of registry.childrenOf(table)) {
|
|
215
|
+
if (path.has(child.table))
|
|
216
|
+
continue; // cycle guard — self-ref / mutual FK
|
|
217
|
+
const match = childMatchPredicate(child, table, predicate);
|
|
218
|
+
if (child.onDelete === 'cascade') {
|
|
219
|
+
// delete grandchildren first, keyed off the rows this child is about to lose
|
|
220
|
+
visit(child.table, match, depth + 1, new Set(path).add(child.table));
|
|
221
|
+
out.push(`DELETE FROM ${child.table} WHERE ${match}`);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
// set-null: null the link, leave the child row (and its subtree) intact
|
|
225
|
+
const assignments = child.columns.map((c) => `${c} = NULL`).join(', ');
|
|
226
|
+
out.push(`UPDATE ${child.table} SET ${assignments} WHERE ${match}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
visit(target, wherePredicate, 0, new Set([target]));
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=fk-cascade.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fk-cascade.js","sourceRoot":"","sources":["../src/fk-cascade.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAeH,OAAO,EAGL,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAeH,OAAO,EAGL,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAsDpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AA0DjD,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAC1E,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC7E,OAAO,EAAE,+BAA+B,EAAE,MAAM,0BAA0B,CAAA;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAA;AAoEvE,wBAAsB,aAAa,CAAC,SAAS,GAAE,OAAO,CAAC,cAAc,CAAM;;;;;;;;;;;;;;GA0+B1E"}
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import { getConfig, getConnectionString } from './config.js';
|
|
|
17
17
|
import { log, port, setLogLevel, setLogStore } from './log.js';
|
|
18
18
|
import { DoBackend } from './pg-proxy-do-backend.js';
|
|
19
19
|
import { startPgProxy } from './pg-proxy.js';
|
|
20
|
-
import { createPGliteInstances, createPGliteWorkerInstances, createSinglePGliteInstance, createSinglePGliteWorkerInstance, createPGliteWorker, runMigrations, startPeriodicCheckpoint, startPeriodicVacuum, } from './pglite-manager.js';
|
|
20
|
+
import { createPGliteInstances, createPGliteWorkerInstances, createSinglePGliteInstance, createSinglePGliteWorkerInstance, createPGliteWorker, runMigrations, startPeriodicCheckpoint, startPeriodicVacuum, vacuumPGliteChurnTables, } from './pglite-manager.js';
|
|
21
21
|
import { findPort } from './port.js';
|
|
22
22
|
import { orezTitle } from './process-title.js';
|
|
23
23
|
import { ensurePublicationHasTables, syncManagedPublications } from './publications.js';
|
|
@@ -25,6 +25,7 @@ import { classifyZeroCrashRecovery, classifyZeroStartupRecovery, hasRecoverableZ
|
|
|
25
25
|
import { installChangeTracking } from './replication/change-tracker.js';
|
|
26
26
|
import { resetReplicationState } from './replication/handler.js';
|
|
27
27
|
import { applySqliteMode, cleanupShim, formatNativeBootstrapInstructions, hasMissingNativeBinarySignature, inspectNativeSqliteBinary, resolveSqliteMode, resolveSqliteModeConfig, } from './sqlite-mode/index.js';
|
|
28
|
+
import { enableZeroChangeLogCleanupRetry } from './zero-changelog-cleanup-patch.js';
|
|
28
29
|
import { enableZeroReplicaCheckpoint } from './zero-checkpoint-patch.js';
|
|
29
30
|
import { probeZeroCacheHttp } from './zero-health.js';
|
|
30
31
|
import { disableZeroLitestreamRestore } from './zero-litestream-patch.js';
|
|
@@ -240,6 +241,19 @@ export async function startZeroLite(overrides = {}) {
|
|
|
240
241
|
let instances, db, stopCheckpoint, stopVacuum = () => { };
|
|
241
242
|
let migrationsApplied = 0;
|
|
242
243
|
let isDoBackend = false;
|
|
244
|
+
let pgliteVacuumMs = 0;
|
|
245
|
+
let delayedVacuumTimer;
|
|
246
|
+
const queuePgliteVacuum = (delayMs = 15_000) => {
|
|
247
|
+
if (isDoBackend || pgliteVacuumMs <= 0)
|
|
248
|
+
return;
|
|
249
|
+
if (delayedVacuumTimer)
|
|
250
|
+
clearTimeout(delayedVacuumTimer);
|
|
251
|
+
delayedVacuumTimer = setTimeout(() => {
|
|
252
|
+
delayedVacuumTimer = undefined;
|
|
253
|
+
void vacuumPGliteChurnTables(instances);
|
|
254
|
+
}, delayMs);
|
|
255
|
+
delayedVacuumTimer.unref?.();
|
|
256
|
+
};
|
|
243
257
|
if (config.doBackendUrl) {
|
|
244
258
|
isDoBackend = true;
|
|
245
259
|
log.orez(`using DO backend: ${config.doBackendUrl}`);
|
|
@@ -275,12 +289,6 @@ export async function startZeroLite(overrides = {}) {
|
|
|
275
289
|
config.checkpointIntervalMs > 0
|
|
276
290
|
? startPeriodicCheckpoint(instances, config.checkpointIntervalMs)
|
|
277
291
|
: () => { };
|
|
278
|
-
// periodic VACUUM of the change-tracking churn tables — PGlite has no
|
|
279
|
-
// effective autovacuum, so without this the change buffer bloats until the
|
|
280
|
-
// change-streamer scan times out and Zero stops sending live updates. Interval
|
|
281
|
-
// via OREZ_VACUUM_MS (default 10min, 0 disables).
|
|
282
|
-
const vacuumMs = Number(process.env.OREZ_VACUUM_MS ?? 10 * 60 * 1000);
|
|
283
|
-
stopVacuum = vacuumMs > 0 ? startPeriodicVacuum(instances, vacuumMs) : () => { };
|
|
284
292
|
// config-based publications
|
|
285
293
|
if (config.zeroPublications && !process.env.ZERO_APP_PUBLICATIONS) {
|
|
286
294
|
process.env.ZERO_APP_PUBLICATIONS = config.zeroPublications;
|
|
@@ -289,6 +297,16 @@ export async function startZeroLite(overrides = {}) {
|
|
|
289
297
|
migrationsApplied = await runMigrations(db, config);
|
|
290
298
|
log.debug.orez('installing change tracking');
|
|
291
299
|
await installChangeTracking(db);
|
|
300
|
+
// periodic VACUUM of the change-tracking churn tables — PGlite has no
|
|
301
|
+
// effective autovacuum, so without this the change buffers bloat until the
|
|
302
|
+
// change-streamer scan times out and Zero stops sending live updates. Start it
|
|
303
|
+
// only after Orez's own tracking tables exist; zero-cache's CDB changeLog is
|
|
304
|
+
// vacuumed once more after zero-cache has started.
|
|
305
|
+
pgliteVacuumMs = Number(process.env.OREZ_VACUUM_MS ?? 60 * 1000);
|
|
306
|
+
if (pgliteVacuumMs > 0) {
|
|
307
|
+
await vacuumPGliteChurnTables(instances);
|
|
308
|
+
stopVacuum = startPeriodicVacuum(instances, pgliteVacuumMs);
|
|
309
|
+
}
|
|
292
310
|
}
|
|
293
311
|
// shared: publications config
|
|
294
312
|
if (config.zeroPublications && !process.env.ZERO_APP_PUBLICATIONS) {
|
|
@@ -325,6 +343,9 @@ export async function startZeroLite(overrides = {}) {
|
|
|
325
343
|
await ensurePublicationHasTables(db, managedPub.names);
|
|
326
344
|
log.debug.orez('re-installing change tracking after on-db-ready');
|
|
327
345
|
await installChangeTracking(db);
|
|
346
|
+
if (!isDoBackend && pgliteVacuumMs > 0) {
|
|
347
|
+
await vacuumPGliteChurnTables(instances);
|
|
348
|
+
}
|
|
328
349
|
}
|
|
329
350
|
if (isDoBackend) {
|
|
330
351
|
await installChangeTracking(db);
|
|
@@ -343,26 +364,22 @@ export async function startZeroLite(overrides = {}) {
|
|
|
343
364
|
instances.postgresReplicas = await createReadReplicas(db, config.readReplicas, config);
|
|
344
365
|
}
|
|
345
366
|
// clean up stale lock files from previous crash. if lock files were present,
|
|
346
|
-
// the previous shutdown was unclean and the replica
|
|
347
|
-
//
|
|
367
|
+
// the previous shutdown was unclean and the replica file may not be safe to
|
|
368
|
+
// reuse, but wiping CVR/CDB here evicts every persisted client with
|
|
369
|
+
// ClientNotFound. rebuild only the replica; true CDC/CVR corruption is handled
|
|
370
|
+
// by the crash classifiers below.
|
|
348
371
|
const hadStaleLocks = cleanupStaleLockFiles(config);
|
|
349
372
|
if (hadStaleLocks) {
|
|
350
|
-
log.debug.orez('unclean shutdown detected,
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
instances,
|
|
354
|
-
zeroCacheProcess: null,
|
|
355
|
-
});
|
|
373
|
+
log.debug.orez('unclean shutdown detected, rebuilding replica (CVR preserved)');
|
|
374
|
+
cleanupStaleReplica(config);
|
|
375
|
+
resetReplicationState();
|
|
356
376
|
}
|
|
357
377
|
if (!config.skipZeroCache) {
|
|
358
378
|
const replicaResetReason = getZeroReplicaStartupResetReason(getReplicaDir(config));
|
|
359
379
|
if (replicaResetReason) {
|
|
360
|
-
log.orez(`detected invalid zero replica (${replicaResetReason}),
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
instances,
|
|
364
|
-
zeroCacheProcess: null,
|
|
365
|
-
});
|
|
380
|
+
log.orez(`detected invalid zero replica (${replicaResetReason}), rebuilding replica (CVR preserved)`);
|
|
381
|
+
cleanupStaleReplica(config);
|
|
382
|
+
resetReplicationState();
|
|
366
383
|
}
|
|
367
384
|
}
|
|
368
385
|
// when admin is enabled, zero-cache runs on internal port with http proxy in front
|
|
@@ -394,6 +411,10 @@ export async function startZeroLite(overrides = {}) {
|
|
|
394
411
|
const details = tail?.length ? tail.slice(-20).join('\n') : '';
|
|
395
412
|
throw new Error(`zero-cache crashed during startup stability check\n${details}`);
|
|
396
413
|
}
|
|
414
|
+
if (!isDoBackend && pgliteVacuumMs > 0) {
|
|
415
|
+
await vacuumPGliteChurnTables(instances);
|
|
416
|
+
queuePgliteVacuum();
|
|
417
|
+
}
|
|
397
418
|
};
|
|
398
419
|
// zero-cache can die during startup for a few distinct reasons; the policy
|
|
399
420
|
// for each lives in classifyZeroStartupRecovery (pure + unit-tested) and
|
|
@@ -410,7 +431,7 @@ export async function startZeroLite(overrides = {}) {
|
|
|
410
431
|
plainRestarts: 0,
|
|
411
432
|
maxRestarts: 3,
|
|
412
433
|
didRecoverState: false,
|
|
413
|
-
|
|
434
|
+
didCacheReset: false,
|
|
414
435
|
canWasmFallback: sqliteMode === 'native' && !config.disableWasmSqlite,
|
|
415
436
|
didWasmFallback: false,
|
|
416
437
|
nativeBinaryMissing: false,
|
|
@@ -456,10 +477,11 @@ export async function startZeroLite(overrides = {}) {
|
|
|
456
477
|
sqliteModeConfig = resolveSqliteModeConfig(false, true); // force wasm
|
|
457
478
|
continue;
|
|
458
479
|
}
|
|
459
|
-
if (action === '
|
|
460
|
-
startupRetry.
|
|
461
|
-
log.orez('zero-cache still crashing after restarts —
|
|
462
|
-
|
|
480
|
+
if (action === 'cache-reset') {
|
|
481
|
+
startupRetry.didCacheReset = true;
|
|
482
|
+
log.orez('zero-cache still crashing after restarts — rebuilding replica and retrying once more (CVR preserved)...');
|
|
483
|
+
cleanupStaleReplica(config);
|
|
484
|
+
resetReplicationState();
|
|
463
485
|
continue;
|
|
464
486
|
}
|
|
465
487
|
// action === 'restart': transient / unexpected crash, plain relaunch.
|
|
@@ -471,7 +493,7 @@ export async function startZeroLite(overrides = {}) {
|
|
|
471
493
|
// visible in logs rather than silently swallowed.
|
|
472
494
|
if (startupRetry.plainRestarts > 0 ||
|
|
473
495
|
startupRetry.didRecoverState ||
|
|
474
|
-
startupRetry.
|
|
496
|
+
startupRetry.didCacheReset ||
|
|
475
497
|
startupRetry.didWasmFallback) {
|
|
476
498
|
log.orez('zero-cache started after recovery');
|
|
477
499
|
}
|
|
@@ -820,16 +842,14 @@ export async function startZeroLite(overrides = {}) {
|
|
|
820
842
|
installCrashWatcher();
|
|
821
843
|
})
|
|
822
844
|
.catch((err) => {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
log.orez(`zero-cache restart recovery failed (${err?.message || err}) — falling back to full reset`);
|
|
826
|
-
resetZeroState('full')
|
|
845
|
+
log.orez(`zero-cache restart recovery failed (${err?.message || err}) — rebuilding replica (CVR preserved)`);
|
|
846
|
+
resetZeroState('cache-only')
|
|
827
847
|
.then(() => {
|
|
828
|
-
log.orez('zero-cache
|
|
848
|
+
log.orez('zero-cache replica rebuild recovery successful');
|
|
829
849
|
installCrashWatcher();
|
|
830
850
|
})
|
|
831
851
|
.catch((resetErr) => {
|
|
832
|
-
log.orez(`zero-cache
|
|
852
|
+
log.orez(`zero-cache replica rebuild recovery failed: ${resetErr?.message || resetErr}`);
|
|
833
853
|
});
|
|
834
854
|
});
|
|
835
855
|
});
|
|
@@ -894,14 +914,14 @@ export async function startZeroLite(overrides = {}) {
|
|
|
894
914
|
installCrashWatcher();
|
|
895
915
|
}
|
|
896
916
|
catch (err) {
|
|
897
|
-
log.orez(`zero-cache HTTP liveness restart failed (${err?.message || err}) —
|
|
917
|
+
log.orez(`zero-cache HTTP liveness restart failed (${err?.message || err}) — rebuilding replica (CVR preserved)`);
|
|
898
918
|
try {
|
|
899
|
-
await resetZeroState('
|
|
900
|
-
log.orez('zero-cache HTTP liveness
|
|
919
|
+
await resetZeroState('cache-only');
|
|
920
|
+
log.orez('zero-cache HTTP liveness replica rebuild recovery successful');
|
|
901
921
|
installCrashWatcher();
|
|
902
922
|
}
|
|
903
923
|
catch (resetErr) {
|
|
904
|
-
log.orez(`zero-cache HTTP liveness
|
|
924
|
+
log.orez(`zero-cache HTTP liveness replica rebuild recovery failed: ${resetErr?.message || resetErr}`);
|
|
905
925
|
}
|
|
906
926
|
}
|
|
907
927
|
finally {
|
|
@@ -923,6 +943,8 @@ export async function startZeroLite(overrides = {}) {
|
|
|
923
943
|
shuttingDown = true;
|
|
924
944
|
if (zeroHttpHealthTimer)
|
|
925
945
|
clearInterval(zeroHttpHealthTimer);
|
|
946
|
+
if (delayedVacuumTimer)
|
|
947
|
+
clearTimeout(delayedVacuumTimer);
|
|
926
948
|
stopCheckpoint();
|
|
927
949
|
stopVacuum();
|
|
928
950
|
httpProxyServer?.close();
|
|
@@ -1067,6 +1089,10 @@ async function startZeroCache(config, logStore, sqliteMode = resolveSqliteMode(c
|
|
|
1067
1089
|
// orez owns the replica on disk and has no litestream backup; stop zero 1.5's
|
|
1068
1090
|
// change-streamer from erroring + resyncing on every restart (see the patch).
|
|
1069
1091
|
disableZeroLitestreamRestore();
|
|
1092
|
+
// zero-cache's changeLog cleanup can strand pending cleanup watermarks when
|
|
1093
|
+
// it runs before any subscriber is connected. Keep the cleanup timer alive so
|
|
1094
|
+
// local/ephemeral Orez instances do not accumulate a huge CDB catchup log.
|
|
1095
|
+
enableZeroChangeLogCleanupRetry();
|
|
1070
1096
|
// litestream also checkpointed the replica WAL in stock zero-cache; with it gone,
|
|
1071
1097
|
// nothing reclaims the wal2 (PASSIVE autocheckpoint can't pass the view-syncer's
|
|
1072
1098
|
// held readers), so it grows unbounded and reads slow down. Inject a periodic
|