@pgpmjs/transform 0.19.0 → 0.21.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/bundle-driver.js +11 -11
- package/categorize.d.ts +1 -1
- package/categorize.js +2 -2
- package/constraint-names.d.ts +36 -0
- package/constraint-names.js +75 -0
- package/esm/bundle-driver.js +1 -1
- package/esm/categorize.js +1 -1
- package/esm/constraint-names.js +71 -0
- package/esm/fixture-closure.js +1 -1
- package/esm/granularity-driver.js +86 -22
- package/esm/index.js +4 -2
- package/esm/module-emit.js +71 -1
- package/esm/partition-driver.js +3 -2
- package/esm/program.js +1 -1
- package/esm/regen.js +1 -1
- package/esm/restructure.js +6 -1
- package/esm/semantic-diff-driver.js +99 -7
- package/esm/sub-object.js +80 -0
- package/fixture-closure.d.ts +1 -1
- package/fixture-closure.js +2 -2
- package/granularity-driver.d.ts +59 -1
- package/granularity-driver.js +88 -22
- package/index.d.ts +7 -4
- package/index.js +10 -1
- package/module-emit.d.ts +26 -0
- package/module-emit.js +72 -1
- package/package.json +8 -7
- package/partition-driver.d.ts +3 -2
- package/partition-driver.js +3 -2
- package/program.d.ts +1 -1
- package/program.js +2 -2
- package/regen.d.ts +1 -1
- package/regen.js +2 -2
- package/restructure.d.ts +7 -0
- package/restructure.js +6 -1
- package/semantic-diff-driver.d.ts +3 -1
- package/semantic-diff-driver.js +101 -9
- package/sub-object.d.ts +31 -0
- package/sub-object.js +84 -0
package/module-emit.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.checkOverwrite = exports.writeModule = exports.writeControlFile = void 0;
|
|
36
|
+
exports.checkOverwrite = exports.appendModule = exports.writeModule = exports.writeControlFile = void 0;
|
|
37
37
|
/**
|
|
38
38
|
* Module emission — the single seam every dials command renders through.
|
|
39
39
|
*
|
|
@@ -86,6 +86,77 @@ const writeModule = (outBase, model, extraRequires = []) => {
|
|
|
86
86
|
return dir;
|
|
87
87
|
};
|
|
88
88
|
exports.writeModule = writeModule;
|
|
89
|
+
/**
|
|
90
|
+
* Append plan-ordered {@link PgpmRow}s into an *existing* module rather than
|
|
91
|
+
* writing a fresh package. Existing changes, their scripts, and the `.control`
|
|
92
|
+
* file are left untouched; only the new changes are written (deploy/revert/
|
|
93
|
+
* verify) and appended to `pgpm.plan` after the current changes.
|
|
94
|
+
*
|
|
95
|
+
* Rows whose change name already exists are skipped (never overwritten).
|
|
96
|
+
* A new change's dependency bracket is filtered to names that resolve within
|
|
97
|
+
* the plan (existing changes, other appended changes, or `pkg:`-external
|
|
98
|
+
* references); a dangling internal dependency is dropped with a warning
|
|
99
|
+
* (plan order still sequences it after the current changes).
|
|
100
|
+
*/
|
|
101
|
+
const appendModule = (moduleDir, rows, options = {}) => {
|
|
102
|
+
const planPath = path.join(moduleDir, 'pgpm.plan');
|
|
103
|
+
if (!fs.existsSync(planPath)) {
|
|
104
|
+
throw new Error(`No pgpm.plan found at ${planPath}; append mode expects an existing pgpm module directory.`);
|
|
105
|
+
}
|
|
106
|
+
const parsed = (0, ast_1.parsePlanFile)(planPath);
|
|
107
|
+
if (!parsed.data) {
|
|
108
|
+
throw new Error(`Failed to parse ${planPath}: ${parsed.errors
|
|
109
|
+
.map(e => `line ${e.line}: ${e.message}`)
|
|
110
|
+
.join('; ')}`);
|
|
111
|
+
}
|
|
112
|
+
const plan = parsed.data;
|
|
113
|
+
const existingNames = new Set(plan.changes.map(c => c.name));
|
|
114
|
+
const incomingNames = new Set(rows.map(r => r.deploy));
|
|
115
|
+
const appended = new Set();
|
|
116
|
+
const added = [];
|
|
117
|
+
const skipped = [];
|
|
118
|
+
const warnings = [];
|
|
119
|
+
const newRows = [];
|
|
120
|
+
const { fullName, email } = (0, ast_1.parseAuthor)(options.author || 'constructive');
|
|
121
|
+
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
122
|
+
for (const row of rows) {
|
|
123
|
+
if (existingNames.has(row.deploy) || appended.has(row.deploy)) {
|
|
124
|
+
skipped.push(row.deploy);
|
|
125
|
+
warnings.push(`change ${row.deploy} already exists in ${plan.package}; left untouched`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const deps = (row.deps ?? []).filter(dep => {
|
|
129
|
+
if (dep.includes(':'))
|
|
130
|
+
return true; // cross-package (pkg:change) external
|
|
131
|
+
if (existingNames.has(dep) || incomingNames.has(dep))
|
|
132
|
+
return true;
|
|
133
|
+
warnings.push(`change ${row.deploy}: dropped dependency ${dep} (not present in ${plan.package})`);
|
|
134
|
+
return false;
|
|
135
|
+
});
|
|
136
|
+
plan.changes.push({
|
|
137
|
+
name: row.deploy,
|
|
138
|
+
dependencies: deps,
|
|
139
|
+
timestamp,
|
|
140
|
+
planner: fullName,
|
|
141
|
+
email: email || `${fullName}@constructive.io`,
|
|
142
|
+
comment: `add ${row.name ?? row.deploy}`
|
|
143
|
+
});
|
|
144
|
+
newRows.push({ ...row, deps });
|
|
145
|
+
appended.add(row.deploy);
|
|
146
|
+
added.push(row.deploy);
|
|
147
|
+
}
|
|
148
|
+
if (newRows.length) {
|
|
149
|
+
const opts = {
|
|
150
|
+
outdir: path.dirname(moduleDir),
|
|
151
|
+
name: path.basename(moduleDir),
|
|
152
|
+
replacer: (str) => str
|
|
153
|
+
};
|
|
154
|
+
(0, ast_1.writePgpmFiles)(newRows, opts);
|
|
155
|
+
(0, ast_1.writePlanFile)(planPath, plan);
|
|
156
|
+
}
|
|
157
|
+
return { dir: moduleDir, added, skipped, warnings };
|
|
158
|
+
};
|
|
159
|
+
exports.appendModule = appendModule;
|
|
89
160
|
/**
|
|
90
161
|
* Guard against clobbering: writing into the source directory requires
|
|
91
162
|
* `--write`, as does overwriting any existing package directory.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgpmjs/transform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "Change-aware SQL transformation for PGPM — superset of @pgsql/transform plus change categorization, fixture closure, and bundle transpile/apply drivers",
|
|
6
6
|
"main": "index.js",
|
|
@@ -39,15 +39,16 @@
|
|
|
39
39
|
"plpgsql"
|
|
40
40
|
],
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@pgpmjs/bundle": "^0.
|
|
42
|
+
"@pgpmjs/bundle": "^0.14.0",
|
|
43
43
|
"makage": "^0.3.0"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@pgpmjs/ast": "^0.
|
|
47
|
-
"@pgpmjs/naming-spec": "^0.
|
|
48
|
-
"@pgsql/scripts": "^18.3.
|
|
49
|
-
"@pgsql/
|
|
46
|
+
"@pgpmjs/ast": "^0.11.0",
|
|
47
|
+
"@pgpmjs/naming-spec": "^0.3.0",
|
|
48
|
+
"@pgsql/scripts": "^18.3.2",
|
|
49
|
+
"@pgsql/semantics": "^18.1.0",
|
|
50
|
+
"@pgsql/transform": "^18.15.0",
|
|
50
51
|
"plpgsql-parser": "^18.5.1"
|
|
51
52
|
},
|
|
52
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "d508453a32a2f1d70ff6916871046615226e6f33"
|
|
53
54
|
}
|
package/partition-driver.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Partition dial: project one deploy surface into a set of pgpm packages.
|
|
3
3
|
*
|
|
4
|
-
* The object graph (statements classified by `@pgsql/
|
|
4
|
+
* The object graph (statements classified by `@pgsql/semantics`, grouped
|
|
5
5
|
* into identity-keyed units) is the source of truth; which package a unit
|
|
6
6
|
* lives in is a *derived projection* of a declarative partition config.
|
|
7
7
|
* Units can be assigned by schema, by object kind, by explicit cherry-pick
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
* pgpm.plan + deploy trees, feed a bundle, ...).
|
|
22
22
|
*/
|
|
23
23
|
import { PathStyle } from '@pgpmjs/naming-spec';
|
|
24
|
-
import {
|
|
24
|
+
import { StatementKind } from '@pgsql/semantics';
|
|
25
|
+
import { ObjectIdentity, ObjectIdentityKind } from '@pgsql/transform';
|
|
25
26
|
/** One deployable unit: an object (by identity) and the statements that build it. */
|
|
26
27
|
export interface PartitionUnit {
|
|
27
28
|
/** The object's identity, or `null` for split riders and the residual unit. */
|
package/partition-driver.js
CHANGED
|
@@ -5,7 +5,7 @@ exports.partitionUnits = partitionUnits;
|
|
|
5
5
|
/**
|
|
6
6
|
* Partition dial: project one deploy surface into a set of pgpm packages.
|
|
7
7
|
*
|
|
8
|
-
* The object graph (statements classified by `@pgsql/
|
|
8
|
+
* The object graph (statements classified by `@pgsql/semantics`, grouped
|
|
9
9
|
* into identity-keyed units) is the source of truth; which package a unit
|
|
10
10
|
* lives in is a *derived projection* of a declarative partition config.
|
|
11
11
|
* Units can be assigned by schema, by object kind, by explicit cherry-pick
|
|
@@ -25,6 +25,7 @@ exports.partitionUnits = partitionUnits;
|
|
|
25
25
|
* pgpm.plan + deploy trees, feed a bundle, ...).
|
|
26
26
|
*/
|
|
27
27
|
const naming_spec_1 = require("@pgpmjs/naming-spec");
|
|
28
|
+
const semantics_1 = require("@pgsql/semantics");
|
|
28
29
|
const transform_1 = require("@pgsql/transform");
|
|
29
30
|
/** A partition whose packages require each other in a cycle — unshippable. */
|
|
30
31
|
class PartitionCycleError extends Error {
|
|
@@ -84,7 +85,7 @@ exports.RESIDUAL_UNIT_PATH = 'misc/statements';
|
|
|
84
85
|
* otherwise) that depend on their host unit.
|
|
85
86
|
*/
|
|
86
87
|
function buildUnitGraph(sql, style, splitRiders) {
|
|
87
|
-
const facts = (0,
|
|
88
|
+
const facts = (0, semantics_1.classifyStatements)(sql);
|
|
88
89
|
const graph = (0, transform_1.buildStatementGraph)(facts);
|
|
89
90
|
const unitOf = new Array(facts.length).fill(-1);
|
|
90
91
|
const units = [];
|
package/program.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* untouched statements). A program with no dirty statements emits its source
|
|
10
10
|
* byte-identically, so content-addressed hashes are preserved.
|
|
11
11
|
*/
|
|
12
|
-
import type { StatementFacts } from '@pgsql/
|
|
12
|
+
import type { StatementFacts } from '@pgsql/semantics';
|
|
13
13
|
/** A statement's location in the source script (byte offsets). */
|
|
14
14
|
export type SqlStatementSpan = StatementFacts['span'];
|
|
15
15
|
/** One statement of a parsed SQL script. */
|
package/program.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.parseSqlProgram = parseSqlProgram;
|
|
4
4
|
exports.emitSqlProgram = emitSqlProgram;
|
|
5
|
-
const
|
|
5
|
+
const semantics_1 = require("@pgsql/semantics");
|
|
6
6
|
const plpgsql_parser_1 = require("plpgsql-parser");
|
|
7
7
|
/**
|
|
8
8
|
* Parse a SQL script into a statement program. Requires `loadModule()` from
|
|
@@ -11,7 +11,7 @@ const plpgsql_parser_1 = require("plpgsql-parser");
|
|
|
11
11
|
function parseSqlProgram(source) {
|
|
12
12
|
const parseResult = (0, plpgsql_parser_1.parseSql)(source);
|
|
13
13
|
const stmts = parseResult?.stmts ?? [];
|
|
14
|
-
const facts = (0,
|
|
14
|
+
const facts = (0, semantics_1.classifyStatements)(source);
|
|
15
15
|
const statements = [];
|
|
16
16
|
for (let i = 0; i < stmts.length; i++) {
|
|
17
17
|
const rawStmt = stmts[i];
|
package/regen.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Requires `loadModule()` from `plpgsql-parser` to have completed.
|
|
13
13
|
*/
|
|
14
14
|
import type { GeneratedScript } from '@pgsql/scripts';
|
|
15
|
-
import type { StatementFacts } from '@pgsql/
|
|
15
|
+
import type { StatementFacts } from '@pgsql/semantics';
|
|
16
16
|
export type { GeneratedScript } from '@pgsql/scripts';
|
|
17
17
|
/** Generated revert and verify scripts for one change's deploy SQL. */
|
|
18
18
|
export interface RegeneratedScripts {
|
package/regen.js
CHANGED
|
@@ -4,13 +4,13 @@ exports.classifyScript = classifyScript;
|
|
|
4
4
|
exports.isStubScript = isStubScript;
|
|
5
5
|
exports.regenerateScripts = regenerateScripts;
|
|
6
6
|
const scripts_1 = require("@pgsql/scripts");
|
|
7
|
-
const
|
|
7
|
+
const semantics_1 = require("@pgsql/semantics");
|
|
8
8
|
/**
|
|
9
9
|
* Classify a pgpm script's substantive statements: every top-level statement
|
|
10
10
|
* except transaction control (`BEGIN`/`COMMIT`/`ROLLBACK` wrappers).
|
|
11
11
|
*/
|
|
12
12
|
function classifyScript(sql) {
|
|
13
|
-
return (0,
|
|
13
|
+
return (0, semantics_1.classifyStatements)(sql).filter(f => f.nodeTag !== 'TransactionStmt');
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* Whether a script is an empty stub: nothing but comments, whitespace, and
|
package/restructure.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { PgpmRow } from '@pgpmjs/ast';
|
|
9
9
|
import { PathStyle } from '@pgpmjs/naming-spec';
|
|
10
10
|
import { Granularity } from '@pgsql/transform';
|
|
11
|
+
import { ChangeGranularity } from './granularity-driver';
|
|
11
12
|
export type ExportGranularity = Granularity;
|
|
12
13
|
export declare const EXPORT_GRANULARITIES: readonly ExportGranularity[];
|
|
13
14
|
export declare const isExportGranularity: (value: unknown) => value is ExportGranularity;
|
|
@@ -18,6 +19,12 @@ export interface RestructureExportRowsResult {
|
|
|
18
19
|
export interface RestructureExportRowsOptions {
|
|
19
20
|
/** Naming-spec rendering style for derived paths (default `directory`). */
|
|
20
21
|
naming?: PathStyle;
|
|
22
|
+
/**
|
|
23
|
+
* Change-level distribution (default `object`): with `alteration`, each
|
|
24
|
+
* `ADD COLUMN` / `ADD CONSTRAINT` becomes its own change. See
|
|
25
|
+
* {@link ChangeGranularity}.
|
|
26
|
+
*/
|
|
27
|
+
changeGranularity?: ChangeGranularity;
|
|
21
28
|
}
|
|
22
29
|
/**
|
|
23
30
|
* Restructure exported migration rows to the target granularity.
|
package/restructure.js
CHANGED
|
@@ -37,7 +37,12 @@ const restructureExportRows = async (rows, granularity, options = {}) => {
|
|
|
37
37
|
name: row.deploy,
|
|
38
38
|
dependencies: row.deps ?? [],
|
|
39
39
|
deploy: row.content
|
|
40
|
-
})), {
|
|
40
|
+
})), {
|
|
41
|
+
granularity,
|
|
42
|
+
changeGranularity: options.changeGranularity,
|
|
43
|
+
changeName,
|
|
44
|
+
subObjectName: identity => (0, naming_spec_1.pathFor)(identity, { style })
|
|
45
|
+
});
|
|
41
46
|
const counters = new Map();
|
|
42
47
|
const taken = new Set();
|
|
43
48
|
const restructured = changes.map(change => {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import { PathStyle } from '@pgpmjs/naming-spec';
|
|
30
30
|
import type { ObjectIdentity } from '@pgsql/transform';
|
|
31
31
|
import { Granularity } from '@pgsql/transform';
|
|
32
|
-
import { GranularityChange } from './granularity-driver';
|
|
32
|
+
import { ChangeGranularity, GranularityChange } from './granularity-driver';
|
|
33
33
|
/** How one object differs between the two sides. */
|
|
34
34
|
export type ObjectDelta = 'added' | 'removed' | 'modified';
|
|
35
35
|
export interface SemanticObjectDiff {
|
|
@@ -45,6 +45,8 @@ export interface SemanticObjectDiff {
|
|
|
45
45
|
export interface SemanticDiffOptions {
|
|
46
46
|
/** Granularity for the emitted CREATE/ALTER changes (default `object`). */
|
|
47
47
|
granularity?: Granularity;
|
|
48
|
+
/** Change-level distribution for emitted changes (default `object`). */
|
|
49
|
+
changeGranularity?: ChangeGranularity;
|
|
48
50
|
/** Naming-spec rendering style (default `directory`). */
|
|
49
51
|
style?: PathStyle;
|
|
50
52
|
}
|
package/semantic-diff-driver.js
CHANGED
|
@@ -32,8 +32,10 @@ exports.diffChangeSets = diffChangeSets;
|
|
|
32
32
|
*/
|
|
33
33
|
const naming_spec_1 = require("@pgpmjs/naming-spec");
|
|
34
34
|
const scripts_1 = require("@pgsql/scripts");
|
|
35
|
+
const semantics_1 = require("@pgsql/semantics");
|
|
35
36
|
const transform_1 = require("@pgsql/transform");
|
|
36
37
|
const plpgsql_parser_1 = require("plpgsql-parser");
|
|
38
|
+
const constraint_names_1 = require("./constraint-names");
|
|
37
39
|
const granularity_driver_1 = require("./granularity-driver");
|
|
38
40
|
const identityKey = (id) => `${id.kind}\u0000${id.schema ?? ''}\u0000${id.table ?? ''}\u0000${id.name}`;
|
|
39
41
|
/**
|
|
@@ -43,7 +45,7 @@ const identityKey = (id) => `${id.kind}\u0000${id.schema ?? ''}\u0000${id.table
|
|
|
43
45
|
* statements are reported via `warnings`.
|
|
44
46
|
*/
|
|
45
47
|
function readSide(sql, warnings, label) {
|
|
46
|
-
const facts = (0,
|
|
48
|
+
const facts = (0, semantics_1.classifyStatements)(sql);
|
|
47
49
|
const graph = (0, transform_1.buildStatementGraph)(facts);
|
|
48
50
|
const units = new Map();
|
|
49
51
|
const order = [];
|
|
@@ -103,7 +105,7 @@ function groupingIdentity(f) {
|
|
|
103
105
|
const identity = (0, transform_1.identityOf)(f);
|
|
104
106
|
if (!identity)
|
|
105
107
|
return null;
|
|
106
|
-
if ((f.kind === 'table' || f.kind === 'constraint' || f.kind === 'rls_enable') &&
|
|
108
|
+
if ((f.kind === 'table' || f.kind === 'constraint' || f.kind === 'fk_constraint' || f.kind === 'rls_enable') &&
|
|
107
109
|
identity.kind !== 'table') {
|
|
108
110
|
return { kind: 'table', schema: identity.schema, name: identity.table ?? identity.name };
|
|
109
111
|
}
|
|
@@ -116,21 +118,61 @@ function fingerprint(text) {
|
|
|
116
118
|
}
|
|
117
119
|
/** Multiset fingerprint of a whole unit. */
|
|
118
120
|
const unitFingerprint = (unit) => unit.texts.map(fingerprint).sort().join('\n');
|
|
121
|
+
/** JSON with recursively sorted object keys — an insertion-order-proof fingerprint. */
|
|
122
|
+
function stablePrint(node) {
|
|
123
|
+
if (Array.isArray(node))
|
|
124
|
+
return `[${node.map(stablePrint).join(',')}]`;
|
|
125
|
+
if (node && typeof node === 'object') {
|
|
126
|
+
const entries = Object.entries(node)
|
|
127
|
+
.filter(([, v]) => v !== undefined)
|
|
128
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
129
|
+
.map(([k, v]) => `${JSON.stringify(k)}:${stablePrint(v)}`);
|
|
130
|
+
return `{${entries.join(',')}}`;
|
|
131
|
+
}
|
|
132
|
+
return JSON.stringify(node);
|
|
133
|
+
}
|
|
134
|
+
/** Constraint kinds that can move between column, table-elt, and ALTER form. */
|
|
135
|
+
const RELOCATABLE = new Set(['CONSTR_PRIMARY', 'CONSTR_UNIQUE', 'CONSTR_FOREIGN', 'CONSTR_CHECK']);
|
|
119
136
|
/**
|
|
120
137
|
* Fold a table unit's statements (CREATE TABLE plus any ALTER TABLE
|
|
121
138
|
* ADD COLUMN / ADD CONSTRAINT) into an effective shape, so atomic and
|
|
122
139
|
* consolidated authorships of the same table compare equal.
|
|
140
|
+
*
|
|
141
|
+
* Constraint placement is representation, not semantics: `id bigint PRIMARY
|
|
142
|
+
* KEY`, a `PRIMARY KEY (id)` table elt, and `ALTER TABLE .. ADD CONSTRAINT
|
|
143
|
+
* t_pkey PRIMARY KEY (id)` all catalog identically. Relocatable constraints
|
|
144
|
+
* (PK/UNIQUE/FK/CHECK) are therefore lifted out of columns and ALTER
|
|
145
|
+
* commands into one canonical table-level set — keyed with the Postgres
|
|
146
|
+
* default name when unnamed, columns filled in from the owning column when
|
|
147
|
+
* column-attached — and PK columns gain the NOT NULL the catalog implies.
|
|
123
148
|
*/
|
|
124
149
|
function tableShape(unit) {
|
|
125
150
|
const shape = { relation: null, columns: new Map(), extras: new Map() };
|
|
151
|
+
const table = unit.identity.name;
|
|
152
|
+
const rawColumns = [];
|
|
153
|
+
const constraints = [];
|
|
154
|
+
const pkColumns = new Set();
|
|
126
155
|
const addColumn = (node) => {
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
156
|
+
const colname = node.colname ?? '';
|
|
157
|
+
const attached = node.constraints ?? [];
|
|
158
|
+
const residual = [];
|
|
159
|
+
for (const item of attached) {
|
|
160
|
+
const c = item.Constraint;
|
|
161
|
+
if (c && RELOCATABLE.has(c.contype ?? '')) {
|
|
162
|
+
constraints.push({ node: c, column: colname });
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
residual.push(item);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
rawColumns.push({ colname, def: { ...node, constraints: residual } });
|
|
169
|
+
};
|
|
170
|
+
const addConstraint = (node, column) => {
|
|
171
|
+
constraints.push({ node: node, column });
|
|
130
172
|
};
|
|
131
173
|
const addExtra = (node) => {
|
|
132
174
|
const clean = (0, transform_1.cleanTree)(node);
|
|
133
|
-
shape.extras.set(
|
|
175
|
+
shape.extras.set(stablePrint(clean), clean);
|
|
134
176
|
};
|
|
135
177
|
for (const text of unit.texts) {
|
|
136
178
|
for (const raw of (0, plpgsql_parser_1.parseSync)(text).sql.stmts) {
|
|
@@ -141,6 +183,9 @@ function tableShape(unit) {
|
|
|
141
183
|
for (const elt of elts) {
|
|
142
184
|
if (elt.ColumnDef)
|
|
143
185
|
addColumn(elt.ColumnDef);
|
|
186
|
+
else if (elt.Constraint && RELOCATABLE.has(elt.Constraint.contype ?? '')) {
|
|
187
|
+
addConstraint(elt.Constraint);
|
|
188
|
+
}
|
|
144
189
|
else
|
|
145
190
|
addExtra(elt);
|
|
146
191
|
}
|
|
@@ -156,6 +201,11 @@ function tableShape(unit) {
|
|
|
156
201
|
if (at.subtype === 'AT_AddColumn' && def?.ColumnDef) {
|
|
157
202
|
addColumn(def.ColumnDef);
|
|
158
203
|
}
|
|
204
|
+
else if (at.subtype === 'AT_AddConstraint' &&
|
|
205
|
+
def?.Constraint &&
|
|
206
|
+
RELOCATABLE.has(def.Constraint.contype ?? '')) {
|
|
207
|
+
addConstraint(def.Constraint);
|
|
208
|
+
}
|
|
159
209
|
else {
|
|
160
210
|
addExtra({ AlterTableCmd: at });
|
|
161
211
|
}
|
|
@@ -166,6 +216,44 @@ function tableShape(unit) {
|
|
|
166
216
|
}
|
|
167
217
|
}
|
|
168
218
|
}
|
|
219
|
+
for (const { node, column } of constraints) {
|
|
220
|
+
const canonical = { ...node };
|
|
221
|
+
canonical.conname = node.conname ?? (0, constraint_names_1.defaultConstraintName)(table, node, column) ?? undefined;
|
|
222
|
+
if (column && (node.keys ?? []).length === 0 && node.contype !== 'CONSTR_FOREIGN' && node.contype !== 'CONSTR_CHECK') {
|
|
223
|
+
canonical.keys = [{ String: { sval: column } }];
|
|
224
|
+
}
|
|
225
|
+
if (column && node.contype === 'CONSTR_FOREIGN' && (node.fk_attrs ?? []).length === 0) {
|
|
226
|
+
canonical.fk_attrs = [{ String: { sval: column } }];
|
|
227
|
+
}
|
|
228
|
+
if (node.contype === 'CONSTR_PRIMARY') {
|
|
229
|
+
const keyed = canonical.keys ?? [];
|
|
230
|
+
for (const k of keyed)
|
|
231
|
+
if (k.String?.sval)
|
|
232
|
+
pkColumns.add(k.String.sval);
|
|
233
|
+
}
|
|
234
|
+
addExtra({ Constraint: canonical });
|
|
235
|
+
}
|
|
236
|
+
for (const { colname, def } of rawColumns) {
|
|
237
|
+
const residual = (def.constraints ?? []).map(c => {
|
|
238
|
+
const constraint = c.Constraint;
|
|
239
|
+
// NOT NULL parses with parser-version-dependent extras (is_enforced,
|
|
240
|
+
// initially_valid); reduce to its semantic core so an authored NOT NULL
|
|
241
|
+
// equals the one a primary key implies.
|
|
242
|
+
if (constraint?.contype === 'CONSTR_NOTNULL') {
|
|
243
|
+
return { Constraint: { contype: 'CONSTR_NOTNULL', conname: constraint.conname } };
|
|
244
|
+
}
|
|
245
|
+
return c;
|
|
246
|
+
});
|
|
247
|
+
const hasNotNull = residual.some(c => c.Constraint?.contype === 'CONSTR_NOTNULL');
|
|
248
|
+
if (pkColumns.has(colname) && !hasNotNull) {
|
|
249
|
+
residual.push({ Constraint: { contype: 'CONSTR_NOTNULL' } });
|
|
250
|
+
}
|
|
251
|
+
const sorted = residual
|
|
252
|
+
.map(c => (0, transform_1.cleanTree)(c))
|
|
253
|
+
.sort((x, y) => stablePrint(x).localeCompare(stablePrint(y)));
|
|
254
|
+
const clean = (0, transform_1.cleanTree)({ ...def, constraints: sorted });
|
|
255
|
+
shape.columns.set(colname, { def: clean, print: stablePrint(clean) });
|
|
256
|
+
}
|
|
169
257
|
return shape;
|
|
170
258
|
}
|
|
171
259
|
const deparseStmt = (node) => `${plpgsql_parser_1.Deparser.deparse(node, { pretty: false })};`;
|
|
@@ -280,7 +368,7 @@ function diffSchemas(from, to, options = {}) {
|
|
|
280
368
|
* statement to surface its not-derivable comment and warning.
|
|
281
369
|
*/
|
|
282
370
|
const inverseOf = (deploy, context) => {
|
|
283
|
-
const facts = (0,
|
|
371
|
+
const facts = (0, semantics_1.classifyStatements)(deploy);
|
|
284
372
|
const graph = (0, transform_1.buildStatementGraph)(facts);
|
|
285
373
|
const pieces = [];
|
|
286
374
|
for (const i of [...graph.order].reverse()) {
|
|
@@ -303,7 +391,7 @@ function diffSchemas(from, to, options = {}) {
|
|
|
303
391
|
*/
|
|
304
392
|
const verifyOf = (deploy, context) => {
|
|
305
393
|
const pieces = [];
|
|
306
|
-
for (const fact of (0,
|
|
394
|
+
for (const fact of (0, semantics_1.classifyStatements)(deploy)) {
|
|
307
395
|
const nodes = (0, scripts_1.existenceCheck)(fact);
|
|
308
396
|
if (nodes === null) {
|
|
309
397
|
const { warnings: w } = (0, scripts_1.verifyFor)([fact]);
|
|
@@ -389,7 +477,11 @@ function diffSchemas(from, to, options = {}) {
|
|
|
389
477
|
}
|
|
390
478
|
let forwardChanges = [];
|
|
391
479
|
if (forwardSql.length > 0) {
|
|
392
|
-
const { changes, warnings: w } = (0, granularity_driver_1.restructureChanges)([{ name: 'delta', dependencies: [], deploy: forwardSql.join('\n\n') }], {
|
|
480
|
+
const { changes, warnings: w } = (0, granularity_driver_1.restructureChanges)([{ name: 'delta', dependencies: [], deploy: forwardSql.join('\n\n') }], {
|
|
481
|
+
granularity: options.granularity ?? 'object',
|
|
482
|
+
changeGranularity: options.changeGranularity,
|
|
483
|
+
subObjectName: identity => (0, naming_spec_1.pathFor)(identity, { style })
|
|
484
|
+
});
|
|
393
485
|
forwardChanges = changes.map(c => ({
|
|
394
486
|
...c,
|
|
395
487
|
revert: inverseOf(c.deploy, c.name),
|
package/sub-object.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-object identities: the column or constraint a single `ALTER TABLE`
|
|
3
|
+
* statement adds.
|
|
4
|
+
*
|
|
5
|
+
* `identityOf` summarizes an ALTER TABLE by the table it targets — the right
|
|
6
|
+
* grouping key for object-level changes, but lossy one level down: the added
|
|
7
|
+
* column's `colname` and the added constraint's `conname` stay behind in the
|
|
8
|
+
* raw parse node (`facts.stmt`). This module recovers them, giving the
|
|
9
|
+
* change-granularity pipeline a stable identity (and naming-spec path) for
|
|
10
|
+
* every alteration.
|
|
11
|
+
*/
|
|
12
|
+
import type { ObjectIdentity } from '@pgpmjs/naming-spec';
|
|
13
|
+
import type { StatementFacts } from '@pgsql/semantics';
|
|
14
|
+
/**
|
|
15
|
+
* The column or constraint a one-command `ALTER TABLE .. ADD COLUMN` /
|
|
16
|
+
* `ADD CONSTRAINT` statement adds, as a naming-spec identity
|
|
17
|
+
* (`kind: 'column' | 'constraint'`, `table` set). Unnamed constraints get
|
|
18
|
+
* their Postgres default name. Returns `null` for anything else — multi-
|
|
19
|
+
* command ALTERs, other subtypes, non-ALTER statements — so callers fall
|
|
20
|
+
* back to object-level grouping.
|
|
21
|
+
*/
|
|
22
|
+
export declare function subObjectIdentityOf(facts: StatementFacts): ObjectIdentity | null;
|
|
23
|
+
/**
|
|
24
|
+
* Rewrite a script so every unnamed `ALTER TABLE .. ADD <constraint>` carries
|
|
25
|
+
* its Postgres default name (`ADD PRIMARY KEY (id)` becomes `ADD CONSTRAINT
|
|
26
|
+
* {table}_pkey PRIMARY KEY (id)`) — the exact name the catalog would assign
|
|
27
|
+
* anyway. Naming them makes each constraint change independently revertible
|
|
28
|
+
* (`DROP CONSTRAINT <name>`) and verifiable. Statements that need no rewrite
|
|
29
|
+
* keep their original text.
|
|
30
|
+
*/
|
|
31
|
+
export declare function nameUnnamedConstraints(sql: string): string;
|
package/sub-object.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.subObjectIdentityOf = subObjectIdentityOf;
|
|
4
|
+
exports.nameUnnamedConstraints = nameUnnamedConstraints;
|
|
5
|
+
const plpgsql_parser_1 = require("plpgsql-parser");
|
|
6
|
+
const constraint_names_1 = require("./constraint-names");
|
|
7
|
+
/** The single AlterTableCmd of a one-command ALTER TABLE, or null. */
|
|
8
|
+
function singleCmd(stmt) {
|
|
9
|
+
const cmds = stmt.cmds ?? [];
|
|
10
|
+
if (cmds.length !== 1)
|
|
11
|
+
return null;
|
|
12
|
+
return cmds[0].AlterTableCmd ?? null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The column or constraint a one-command `ALTER TABLE .. ADD COLUMN` /
|
|
16
|
+
* `ADD CONSTRAINT` statement adds, as a naming-spec identity
|
|
17
|
+
* (`kind: 'column' | 'constraint'`, `table` set). Unnamed constraints get
|
|
18
|
+
* their Postgres default name. Returns `null` for anything else — multi-
|
|
19
|
+
* command ALTERs, other subtypes, non-ALTER statements — so callers fall
|
|
20
|
+
* back to object-level grouping.
|
|
21
|
+
*/
|
|
22
|
+
function subObjectIdentityOf(facts) {
|
|
23
|
+
const stmt = facts.stmt?.AlterTableStmt;
|
|
24
|
+
if (!stmt?.relation?.relname)
|
|
25
|
+
return null;
|
|
26
|
+
const cmd = singleCmd(stmt);
|
|
27
|
+
if (!cmd)
|
|
28
|
+
return null;
|
|
29
|
+
const schema = stmt.relation.schemaname ?? null;
|
|
30
|
+
const table = stmt.relation.relname;
|
|
31
|
+
if (cmd.subtype === 'AT_AddColumn' && cmd.def?.ColumnDef?.colname) {
|
|
32
|
+
return { kind: 'column', schema, name: cmd.def.ColumnDef.colname, table };
|
|
33
|
+
}
|
|
34
|
+
if (cmd.subtype === 'AT_AddConstraint' && cmd.def?.Constraint) {
|
|
35
|
+
const constraint = cmd.def.Constraint;
|
|
36
|
+
const name = constraint.conname ?? (0, constraint_names_1.defaultConstraintName)(table, constraint);
|
|
37
|
+
if (!name)
|
|
38
|
+
return null;
|
|
39
|
+
return { kind: 'constraint', schema, name, table };
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Rewrite a script so every unnamed `ALTER TABLE .. ADD <constraint>` carries
|
|
45
|
+
* its Postgres default name (`ADD PRIMARY KEY (id)` becomes `ADD CONSTRAINT
|
|
46
|
+
* {table}_pkey PRIMARY KEY (id)`) — the exact name the catalog would assign
|
|
47
|
+
* anyway. Naming them makes each constraint change independently revertible
|
|
48
|
+
* (`DROP CONSTRAINT <name>`) and verifiable. Statements that need no rewrite
|
|
49
|
+
* keep their original text.
|
|
50
|
+
*/
|
|
51
|
+
function nameUnnamedConstraints(sql) {
|
|
52
|
+
const parsed = (0, plpgsql_parser_1.parseSync)(sql);
|
|
53
|
+
const pieces = [];
|
|
54
|
+
for (const raw of parsed.sql.stmts) {
|
|
55
|
+
const node = raw.stmt;
|
|
56
|
+
const stmt = node.AlterTableStmt;
|
|
57
|
+
const table = stmt?.relation?.relname;
|
|
58
|
+
let rewritten = false;
|
|
59
|
+
if (stmt && table) {
|
|
60
|
+
for (const cmd of stmt.cmds ?? []) {
|
|
61
|
+
const at = cmd.AlterTableCmd;
|
|
62
|
+
const constraint = at?.subtype === 'AT_AddConstraint' ? at.def?.Constraint : undefined;
|
|
63
|
+
if (constraint && !constraint.conname) {
|
|
64
|
+
const name = (0, constraint_names_1.defaultConstraintName)(table, constraint);
|
|
65
|
+
if (name) {
|
|
66
|
+
constraint.conname = name;
|
|
67
|
+
rewritten = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (rewritten) {
|
|
73
|
+
pieces.push(`${plpgsql_parser_1.Deparser.deparse(node, { pretty: true })};`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const start = raw.stmt_location ?? 0;
|
|
77
|
+
const len = raw.stmt_len;
|
|
78
|
+
const text = len === undefined ? sql.slice(start) : sql.slice(start, start + len);
|
|
79
|
+
const trimmed = text.trim();
|
|
80
|
+
pieces.push(trimmed.endsWith(';') ? trimmed : `${trimmed};`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return pieces.join('\n\n');
|
|
84
|
+
}
|