@pgpmjs/transform 0.21.0 → 0.23.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/byte-slice.d.ts +23 -0
- package/byte-slice.js +31 -0
- package/esm/byte-slice.js +26 -0
- package/esm/granularity-driver.js +4 -2
- package/esm/index.js +1 -1
- package/esm/module-source.js +46 -10
- package/esm/partition-driver.js +3 -1
- package/esm/program.js +13 -5
- package/esm/semantic-diff-driver.js +38 -1
- package/esm/sub-object.js +3 -1
- package/granularity-driver.js +4 -2
- package/index.d.ts +2 -2
- package/index.js +2 -1
- package/module-source.d.ts +19 -1
- package/module-source.js +45 -9
- package/package.json +8 -8
- package/partition-driver.js +3 -1
- package/program.js +13 -5
- package/semantic-diff-driver.d.ts +35 -0
- package/semantic-diff-driver.js +39 -1
- package/sub-object.js +3 -1
package/byte-slice.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte-correct statement slicing.
|
|
3
|
+
*
|
|
4
|
+
* libpg-query reports statement offsets (`stmt_location` / `stmt_len`, and the
|
|
5
|
+
* `span` that `@pgsql/semantics` derives from them) as UTF-8 **byte** offsets,
|
|
6
|
+
* but JavaScript string indexing is UTF-16. The two agree only while a script
|
|
7
|
+
* is pure ASCII; a single non-ASCII byte (an em dash in a comment, an accented
|
|
8
|
+
* word in a seed row, an emoji) shifts every later byte offset past its JS
|
|
9
|
+
* index, so slicing the source string directly with `String.prototype.slice`
|
|
10
|
+
* mis-cuts every statement that follows it — e.g. `COMMENT ON ...` reappears as
|
|
11
|
+
* `MMENT ON ...` and fails to reparse.
|
|
12
|
+
*
|
|
13
|
+
* Slice on the UTF-8 bytes instead and decode back to text. Statement offsets
|
|
14
|
+
* always fall on character boundaries, so decoding a byte range never splits a
|
|
15
|
+
* multibyte character.
|
|
16
|
+
*/
|
|
17
|
+
export declare const sqlSourceBytes: (source: string) => Buffer;
|
|
18
|
+
/**
|
|
19
|
+
* Slice a statement's verbatim text out of the source bytes by its byte-offset
|
|
20
|
+
* span. `len === undefined` slices to the end of the source (libpg-query omits
|
|
21
|
+
* `stmt_len` for the final statement).
|
|
22
|
+
*/
|
|
23
|
+
export declare const sliceStatementBytes: (sourceBytes: Buffer, start: number, len?: number) => string;
|
package/byte-slice.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sliceStatementBytes = exports.sqlSourceBytes = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Byte-correct statement slicing.
|
|
6
|
+
*
|
|
7
|
+
* libpg-query reports statement offsets (`stmt_location` / `stmt_len`, and the
|
|
8
|
+
* `span` that `@pgsql/semantics` derives from them) as UTF-8 **byte** offsets,
|
|
9
|
+
* but JavaScript string indexing is UTF-16. The two agree only while a script
|
|
10
|
+
* is pure ASCII; a single non-ASCII byte (an em dash in a comment, an accented
|
|
11
|
+
* word in a seed row, an emoji) shifts every later byte offset past its JS
|
|
12
|
+
* index, so slicing the source string directly with `String.prototype.slice`
|
|
13
|
+
* mis-cuts every statement that follows it — e.g. `COMMENT ON ...` reappears as
|
|
14
|
+
* `MMENT ON ...` and fails to reparse.
|
|
15
|
+
*
|
|
16
|
+
* Slice on the UTF-8 bytes instead and decode back to text. Statement offsets
|
|
17
|
+
* always fall on character boundaries, so decoding a byte range never splits a
|
|
18
|
+
* multibyte character.
|
|
19
|
+
*/
|
|
20
|
+
const sqlSourceBytes = (source) => Buffer.from(source, 'utf8');
|
|
21
|
+
exports.sqlSourceBytes = sqlSourceBytes;
|
|
22
|
+
/**
|
|
23
|
+
* Slice a statement's verbatim text out of the source bytes by its byte-offset
|
|
24
|
+
* span. `len === undefined` slices to the end of the source (libpg-query omits
|
|
25
|
+
* `stmt_len` for the final statement).
|
|
26
|
+
*/
|
|
27
|
+
const sliceStatementBytes = (sourceBytes, start, len) => {
|
|
28
|
+
const end = len === undefined ? sourceBytes.length : start + len;
|
|
29
|
+
return sourceBytes.subarray(start, end).toString('utf8');
|
|
30
|
+
};
|
|
31
|
+
exports.sliceStatementBytes = sliceStatementBytes;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte-correct statement slicing.
|
|
3
|
+
*
|
|
4
|
+
* libpg-query reports statement offsets (`stmt_location` / `stmt_len`, and the
|
|
5
|
+
* `span` that `@pgsql/semantics` derives from them) as UTF-8 **byte** offsets,
|
|
6
|
+
* but JavaScript string indexing is UTF-16. The two agree only while a script
|
|
7
|
+
* is pure ASCII; a single non-ASCII byte (an em dash in a comment, an accented
|
|
8
|
+
* word in a seed row, an emoji) shifts every later byte offset past its JS
|
|
9
|
+
* index, so slicing the source string directly with `String.prototype.slice`
|
|
10
|
+
* mis-cuts every statement that follows it — e.g. `COMMENT ON ...` reappears as
|
|
11
|
+
* `MMENT ON ...` and fails to reparse.
|
|
12
|
+
*
|
|
13
|
+
* Slice on the UTF-8 bytes instead and decode back to text. Statement offsets
|
|
14
|
+
* always fall on character boundaries, so decoding a byte range never splits a
|
|
15
|
+
* multibyte character.
|
|
16
|
+
*/
|
|
17
|
+
export const sqlSourceBytes = (source) => Buffer.from(source, 'utf8');
|
|
18
|
+
/**
|
|
19
|
+
* Slice a statement's verbatim text out of the source bytes by its byte-offset
|
|
20
|
+
* span. `len === undefined` slices to the end of the source (libpg-query omits
|
|
21
|
+
* `stmt_len` for the final statement).
|
|
22
|
+
*/
|
|
23
|
+
export const sliceStatementBytes = (sourceBytes, start, len) => {
|
|
24
|
+
const end = len === undefined ? sourceBytes.length : start + len;
|
|
25
|
+
return sourceBytes.subarray(start, end).toString('utf8');
|
|
26
|
+
};
|
|
@@ -2,6 +2,7 @@ import { pathFor } from '@pgpmjs/naming-spec';
|
|
|
2
2
|
import { revertFor, verifyFor } from '@pgsql/scripts';
|
|
3
3
|
import { classifyStatements } from '@pgsql/semantics';
|
|
4
4
|
import { buildStatementGraph, identityOf, restructureSql } from '@pgsql/transform';
|
|
5
|
+
import { sliceStatementBytes, sqlSourceBytes } from './byte-slice';
|
|
5
6
|
import { nameUnnamedConstraints, subObjectIdentityOf } from './sub-object';
|
|
6
7
|
export const CHANGE_GRANULARITIES = ['alteration', 'object', 'single'];
|
|
7
8
|
export const isChangeGranularity = (value) => CHANGE_GRANULARITIES.includes(value);
|
|
@@ -53,12 +54,13 @@ export function restructureChanges(changes, options) {
|
|
|
53
54
|
// target (creates[0]), so a table's CREATE and its remaining ALTERs land
|
|
54
55
|
// in the same change regardless of statement kind.
|
|
55
56
|
const facts = classifyStatements(sql);
|
|
57
|
+
const sqlBytes = sqlSourceBytes(sql);
|
|
56
58
|
if (options.changeGranularity === 'single') {
|
|
57
59
|
const name = options.singleChangeName ?? 'module/init';
|
|
58
60
|
const inputNames = new Set(changes.map(c => c.name));
|
|
59
61
|
const dependencies = [...new Set(changes.flatMap(c => c.dependencies).filter(d => !inputNames.has(d)))].sort();
|
|
60
62
|
const statements = facts
|
|
61
|
-
.map(f =>
|
|
63
|
+
.map(f => sliceStatementBytes(sqlBytes, f.span.start, f.span.len).trim())
|
|
62
64
|
.filter(Boolean)
|
|
63
65
|
.map(text => (text.endsWith(';') ? text : `${text};`));
|
|
64
66
|
const revert = revertFor(facts);
|
|
@@ -137,7 +139,7 @@ export function restructureChanges(changes, options) {
|
|
|
137
139
|
const groupSql = groupNames.map(() => []);
|
|
138
140
|
const groupStatements = groupNames.map(() => []);
|
|
139
141
|
facts.forEach((f, i) => {
|
|
140
|
-
const text =
|
|
142
|
+
const text = sliceStatementBytes(sqlBytes, f.span.start, f.span.len).trim();
|
|
141
143
|
const g = groupOf[i];
|
|
142
144
|
if (g !== -1 && text) {
|
|
143
145
|
groupSql[g].push(text.endsWith(';') ? text : `${text};`);
|
package/esm/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export { PartitionCycleError, partitionUnits, RESIDUAL_UNIT_PATH, } from './part
|
|
|
11
11
|
export { emitSqlProgram, parseSqlProgram } from './program';
|
|
12
12
|
export { classifyScript, isStubScript, regenerateScripts, } from './regen';
|
|
13
13
|
export { EXPORT_GRANULARITIES, isExportGranularity, restructureExportRows } from './restructure';
|
|
14
|
-
export { diffChangeSets, diffSchemas, } from './semantic-diff-driver';
|
|
14
|
+
export { coverChanges, diffChangeSets, diffSchemas, } from './semantic-diff-driver';
|
|
15
15
|
export { nameUnnamedConstraints, subObjectIdentityOf } from './sub-object';
|
|
16
16
|
export * from '@pgsql/transform';
|
|
17
17
|
export { loadModule } from 'plpgsql-parser';
|
package/esm/module-source.js
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Module ingestion for the dials pipeline: load an on-disk pgpm module into
|
|
3
|
-
* the flattened, classified change list the transform/partition drivers
|
|
4
|
-
* consume. `pgpm transform` uses this today; a future `pgpm import` (dumps)
|
|
5
|
-
* and `pgpm diff` are expected to reuse the same seam.
|
|
6
|
-
*/
|
|
7
|
-
import { parsePgpmHeader, parsePlanFile, readScript } from '@pgpmjs/ast';
|
|
1
|
+
import { parsePgpmHeader, parsePlanFile, readScript, resolveReference } from '@pgpmjs/ast';
|
|
8
2
|
import * as path from 'path';
|
|
9
3
|
const TX_LINE = /^\s*(BEGIN|COMMIT|ROLLBACK)\s*;\s*$/i;
|
|
10
4
|
/**
|
|
@@ -17,22 +11,64 @@ export const stripTransactionWrapper = (sql) => sql
|
|
|
17
11
|
.filter(line => !TX_LINE.test(line))
|
|
18
12
|
.join('\n')
|
|
19
13
|
.trim();
|
|
14
|
+
const CROSS_PACKAGE_TAG = /^([^:]+):@(.+)$/;
|
|
15
|
+
/**
|
|
16
|
+
* Resolve a plan dependency token to a concrete change identity.
|
|
17
|
+
*
|
|
18
|
+
* Non-tag tokens (a bare change name, or a `pkg:change` cross-package name)
|
|
19
|
+
* pass through unchanged — they are already the identity the ledger and
|
|
20
|
+
* generated plans record. A tag token is resolved so a dependency written
|
|
21
|
+
* against a tag is never re-emitted verbatim into a generated plan or a
|
|
22
|
+
* ledger row:
|
|
23
|
+
* - a local tag (`@tag`) resolves to its change name via this plan;
|
|
24
|
+
* - a cross-package tag (`pkg:@tag`) resolves to the canonical `pkg:change`
|
|
25
|
+
* qualified name via `crossPackagePlans[pkg]` when available.
|
|
26
|
+
*
|
|
27
|
+
* Anything that cannot be resolved (unknown tag, or a cross-package tag with
|
|
28
|
+
* no plan in context) is kept verbatim — never dropped — with a warning.
|
|
29
|
+
*/
|
|
30
|
+
const resolveDependencyToken = (token, plan, warnings, changeName, crossPackagePlans) => {
|
|
31
|
+
if (!token.includes('@'))
|
|
32
|
+
return token;
|
|
33
|
+
const cross = token.match(CROSS_PACKAGE_TAG);
|
|
34
|
+
if (cross && cross[1] !== plan.package) {
|
|
35
|
+
const pkg = cross[1];
|
|
36
|
+
const otherPlan = crossPackagePlans?.get(pkg);
|
|
37
|
+
if (!otherPlan) {
|
|
38
|
+
warnings.push(`${changeName}: dependency tag "${token}" is cross-package; left unresolved (no plan for "${pkg}" in context)`);
|
|
39
|
+
return token;
|
|
40
|
+
}
|
|
41
|
+
const resolved = resolveReference(token, otherPlan, pkg);
|
|
42
|
+
if (resolved.error || !resolved.change) {
|
|
43
|
+
warnings.push(`${changeName}: could not resolve cross-package dependency tag "${token}" (${resolved.error ?? 'no matching change'}); left as-is`);
|
|
44
|
+
return token;
|
|
45
|
+
}
|
|
46
|
+
return `${pkg}:${resolved.change}`;
|
|
47
|
+
}
|
|
48
|
+
const resolved = resolveReference(token, plan, plan.package);
|
|
49
|
+
if (resolved.error || !resolved.change) {
|
|
50
|
+
warnings.push(`${changeName}: could not resolve dependency tag "${token}" (${resolved.error ?? 'no matching change'}); left as-is`);
|
|
51
|
+
return token;
|
|
52
|
+
}
|
|
53
|
+
return resolved.change;
|
|
54
|
+
};
|
|
20
55
|
/**
|
|
21
56
|
* Load a pgpm module from disk: parse pgpm.plan and read each change's
|
|
22
57
|
* deploy script, stripping pgpm headers and transaction wrappers so the
|
|
23
58
|
* result can be flattened straight into the dials pipeline
|
|
24
59
|
* (`restructureChanges` / `partitionUnits`).
|
|
25
60
|
*/
|
|
26
|
-
export const loadModuleSource = (moduleDir) => {
|
|
61
|
+
export const loadModuleSource = (moduleDir, options = {}) => {
|
|
27
62
|
const modulePath = path.resolve(moduleDir);
|
|
28
63
|
const planResult = parsePlanFile(path.join(modulePath, 'pgpm.plan'));
|
|
29
64
|
if (!planResult.data) {
|
|
30
65
|
const reasons = planResult.errors.map(e => e.message).join(', ');
|
|
31
66
|
throw new Error(`Failed to parse pgpm.plan in ${modulePath}: ${reasons}`);
|
|
32
67
|
}
|
|
68
|
+
const plan = planResult.data;
|
|
33
69
|
const warnings = [];
|
|
34
70
|
const changes = [];
|
|
35
|
-
for (const change of
|
|
71
|
+
for (const change of plan.changes) {
|
|
36
72
|
const raw = readScript(modulePath, 'deploy', change.name);
|
|
37
73
|
if (!raw) {
|
|
38
74
|
warnings.push(`${change.name}: no deploy script found, skipping`);
|
|
@@ -41,7 +77,7 @@ export const loadModuleSource = (moduleDir) => {
|
|
|
41
77
|
const { body } = parsePgpmHeader(raw);
|
|
42
78
|
changes.push({
|
|
43
79
|
name: change.name,
|
|
44
|
-
dependencies: change.dependencies ?? [],
|
|
80
|
+
dependencies: (change.dependencies ?? []).map(dep => resolveDependencyToken(dep, plan, warnings, change.name, options.crossPackagePlans)),
|
|
45
81
|
deploy: stripTransactionWrapper(body)
|
|
46
82
|
});
|
|
47
83
|
}
|
package/esm/partition-driver.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { pathFor } from '@pgpmjs/naming-spec';
|
|
24
24
|
import { classifyStatements } from '@pgsql/semantics';
|
|
25
25
|
import { buildStatementGraph, identityOf } from '@pgsql/transform';
|
|
26
|
+
import { sliceStatementBytes, sqlSourceBytes } from './byte-slice';
|
|
26
27
|
/** A partition whose packages require each other in a cycle — unshippable. */
|
|
27
28
|
export class PartitionCycleError extends Error {
|
|
28
29
|
packages;
|
|
@@ -81,6 +82,7 @@ export const RESIDUAL_UNIT_PATH = 'misc/statements';
|
|
|
81
82
|
*/
|
|
82
83
|
function buildUnitGraph(sql, style, splitRiders) {
|
|
83
84
|
const facts = classifyStatements(sql);
|
|
85
|
+
const sqlBytes = sqlSourceBytes(sql);
|
|
84
86
|
const graph = buildStatementGraph(facts);
|
|
85
87
|
const unitOf = new Array(facts.length).fill(-1);
|
|
86
88
|
const units = [];
|
|
@@ -163,7 +165,7 @@ function buildUnitGraph(sql, style, splitRiders) {
|
|
|
163
165
|
unitOf[i] = unitFor(null, f.kind);
|
|
164
166
|
});
|
|
165
167
|
facts.forEach((f, i) => {
|
|
166
|
-
const text =
|
|
168
|
+
const text = sliceStatementBytes(sqlBytes, f.span.start, f.span.len).trim();
|
|
167
169
|
if (!text)
|
|
168
170
|
return;
|
|
169
171
|
const unit = units[unitOf[i]];
|
package/esm/program.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { classifyStatements } from '@pgsql/semantics';
|
|
2
2
|
import { Deparser, parseSql } from 'plpgsql-parser';
|
|
3
|
+
import { sliceStatementBytes, sqlSourceBytes } from './byte-slice';
|
|
3
4
|
/**
|
|
4
5
|
* Parse a SQL script into a statement program. Requires `loadModule()` from
|
|
5
6
|
* `plpgsql-parser` to have completed.
|
|
@@ -8,6 +9,7 @@ export function parseSqlProgram(source) {
|
|
|
8
9
|
const parseResult = parseSql(source);
|
|
9
10
|
const stmts = parseResult?.stmts ?? [];
|
|
10
11
|
const facts = classifyStatements(source);
|
|
12
|
+
const sourceBytes = sqlSourceBytes(source);
|
|
11
13
|
const statements = [];
|
|
12
14
|
for (let i = 0; i < stmts.length; i++) {
|
|
13
15
|
const rawStmt = stmts[i];
|
|
@@ -18,7 +20,7 @@ export function parseSqlProgram(source) {
|
|
|
18
20
|
stmt: rawStmt.stmt,
|
|
19
21
|
facts: facts[i],
|
|
20
22
|
span: { start, len },
|
|
21
|
-
raw:
|
|
23
|
+
raw: sliceStatementBytes(sourceBytes, start, len),
|
|
22
24
|
dirty: false
|
|
23
25
|
});
|
|
24
26
|
}
|
|
@@ -34,16 +36,22 @@ export function emitSqlProgram(program) {
|
|
|
34
36
|
const { source, statements } = program;
|
|
35
37
|
if (!statements.some(s => s.dirty))
|
|
36
38
|
return source;
|
|
39
|
+
// Spans are UTF-8 byte offsets; slice statements and the verbatim gaps
|
|
40
|
+
// between them on the bytes so a multibyte character never mis-cuts the
|
|
41
|
+
// reassembly. Statement boundaries fall on character boundaries.
|
|
42
|
+
const sourceBytes = sqlSourceBytes(source);
|
|
37
43
|
const pieces = [];
|
|
38
44
|
let cursor = 0;
|
|
39
45
|
for (const statement of statements) {
|
|
40
46
|
const { start, len } = statement.span;
|
|
41
47
|
if (start > cursor)
|
|
42
|
-
pieces.push(
|
|
43
|
-
pieces.push(statement.dirty
|
|
48
|
+
pieces.push(sliceStatementBytes(sourceBytes, cursor, start - cursor));
|
|
49
|
+
pieces.push(statement.dirty
|
|
50
|
+
? Deparser.deparse(statement.stmt)
|
|
51
|
+
: sliceStatementBytes(sourceBytes, start, len));
|
|
44
52
|
cursor = start + len;
|
|
45
53
|
}
|
|
46
|
-
if (cursor <
|
|
47
|
-
pieces.push(
|
|
54
|
+
if (cursor < sourceBytes.length)
|
|
55
|
+
pieces.push(sliceStatementBytes(sourceBytes, cursor));
|
|
48
56
|
return pieces.join('');
|
|
49
57
|
}
|
|
@@ -31,6 +31,7 @@ import { existenceCheck, invertStatement, revertFor, verifyFor } from '@pgsql/sc
|
|
|
31
31
|
import { classifyStatements } from '@pgsql/semantics';
|
|
32
32
|
import { buildStatementGraph, cleanTree, identityOf } from '@pgsql/transform';
|
|
33
33
|
import { Deparser, parseSync } from 'plpgsql-parser';
|
|
34
|
+
import { sliceStatementBytes, sqlSourceBytes } from './byte-slice';
|
|
34
35
|
import { defaultConstraintName } from './constraint-names';
|
|
35
36
|
import { restructureChanges } from './granularity-driver';
|
|
36
37
|
const identityKey = (id) => `${id.kind}\u0000${id.schema ?? ''}\u0000${id.table ?? ''}\u0000${id.name}`;
|
|
@@ -47,8 +48,9 @@ function readSide(sql, warnings, label) {
|
|
|
47
48
|
const order = [];
|
|
48
49
|
const unitOf = new Array(facts.length).fill(null);
|
|
49
50
|
const byObject = new Map();
|
|
51
|
+
const sqlBytes = sqlSourceBytes(sql);
|
|
50
52
|
facts.forEach((f, i) => {
|
|
51
|
-
const text =
|
|
53
|
+
const text = sliceStatementBytes(sqlBytes, f.span.start, f.span.len).trim();
|
|
52
54
|
if (!text)
|
|
53
55
|
return;
|
|
54
56
|
let identity = groupingIdentity(f);
|
|
@@ -502,3 +504,38 @@ export function diffChangeSets(from, to, options = {}) {
|
|
|
502
504
|
const flatten = (changes) => changes.map(c => c.deploy.trim()).filter(Boolean).join('\n\n');
|
|
503
505
|
return diffSchemas(flatten(from), flatten(to), options);
|
|
504
506
|
}
|
|
507
|
+
/**
|
|
508
|
+
* Cover each `to`-side change against a semantic diff's per-object delta:
|
|
509
|
+
* which of the objects the change touches are already present and identical
|
|
510
|
+
* on the `from` side. A `satisfied` change deploys nothing new — its ledger
|
|
511
|
+
* entry can be backfilled instead of executing it; an `unsatisfied` change is
|
|
512
|
+
* the genuine delta. Identity keying is shared with the diff itself, so
|
|
513
|
+
* regrouped/renamed/reordered authorship of the same objects covers the same.
|
|
514
|
+
*
|
|
515
|
+
* `diff` must come from diffing the same `from` side against these changes
|
|
516
|
+
* (`diffChangeSets(fromChanges, toChanges)`).
|
|
517
|
+
*/
|
|
518
|
+
export function coverChanges(changes, diff, options = {}) {
|
|
519
|
+
const style = options.style ?? 'directory';
|
|
520
|
+
const deltaByKey = new Map();
|
|
521
|
+
for (const obj of diff.objects) {
|
|
522
|
+
deltaByKey.set(identityKey(obj.identity), obj.delta);
|
|
523
|
+
}
|
|
524
|
+
return changes.map(change => {
|
|
525
|
+
const sideWarnings = [];
|
|
526
|
+
const side = readSide(change.deploy, sideWarnings, change.name);
|
|
527
|
+
const objects = side.order.map(key => {
|
|
528
|
+
const unit = side.units.get(key);
|
|
529
|
+
return {
|
|
530
|
+
path: pathFor(unit.identity, { style }),
|
|
531
|
+
delta: deltaByKey.get(key) ?? 'unchanged'
|
|
532
|
+
};
|
|
533
|
+
});
|
|
534
|
+
const touched = objects.filter(o => o.delta !== 'unchanged').length;
|
|
535
|
+
const status = objects.length === 0 ? 'inert'
|
|
536
|
+
: touched === 0 ? 'satisfied'
|
|
537
|
+
: touched === objects.length ? 'unsatisfied'
|
|
538
|
+
: 'partial';
|
|
539
|
+
return { name: change.name, status, objects };
|
|
540
|
+
});
|
|
541
|
+
}
|
package/esm/sub-object.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Deparser, parseSync } from 'plpgsql-parser';
|
|
2
|
+
import { sliceStatementBytes, sqlSourceBytes } from './byte-slice';
|
|
2
3
|
import { defaultConstraintName } from './constraint-names';
|
|
3
4
|
/** The single AlterTableCmd of a one-command ALTER TABLE, or null. */
|
|
4
5
|
function singleCmd(stmt) {
|
|
@@ -46,6 +47,7 @@ export function subObjectIdentityOf(facts) {
|
|
|
46
47
|
*/
|
|
47
48
|
export function nameUnnamedConstraints(sql) {
|
|
48
49
|
const parsed = parseSync(sql);
|
|
50
|
+
const sqlBytes = sqlSourceBytes(sql);
|
|
49
51
|
const pieces = [];
|
|
50
52
|
for (const raw of parsed.sql.stmts) {
|
|
51
53
|
const node = raw.stmt;
|
|
@@ -71,7 +73,7 @@ export function nameUnnamedConstraints(sql) {
|
|
|
71
73
|
else {
|
|
72
74
|
const start = raw.stmt_location ?? 0;
|
|
73
75
|
const len = raw.stmt_len;
|
|
74
|
-
const text =
|
|
76
|
+
const text = sliceStatementBytes(sqlBytes, start, len);
|
|
75
77
|
const trimmed = text.trim();
|
|
76
78
|
pieces.push(trimmed.endsWith(';') ? trimmed : `${trimmed};`);
|
|
77
79
|
}
|
package/granularity-driver.js
CHANGED
|
@@ -7,6 +7,7 @@ const naming_spec_1 = require("@pgpmjs/naming-spec");
|
|
|
7
7
|
const scripts_1 = require("@pgsql/scripts");
|
|
8
8
|
const semantics_1 = require("@pgsql/semantics");
|
|
9
9
|
const transform_1 = require("@pgsql/transform");
|
|
10
|
+
const byte_slice_1 = require("./byte-slice");
|
|
10
11
|
const sub_object_1 = require("./sub-object");
|
|
11
12
|
exports.CHANGE_GRANULARITIES = ['alteration', 'object', 'single'];
|
|
12
13
|
const isChangeGranularity = (value) => exports.CHANGE_GRANULARITIES.includes(value);
|
|
@@ -59,12 +60,13 @@ function restructureChanges(changes, options) {
|
|
|
59
60
|
// target (creates[0]), so a table's CREATE and its remaining ALTERs land
|
|
60
61
|
// in the same change regardless of statement kind.
|
|
61
62
|
const facts = (0, semantics_1.classifyStatements)(sql);
|
|
63
|
+
const sqlBytes = (0, byte_slice_1.sqlSourceBytes)(sql);
|
|
62
64
|
if (options.changeGranularity === 'single') {
|
|
63
65
|
const name = options.singleChangeName ?? 'module/init';
|
|
64
66
|
const inputNames = new Set(changes.map(c => c.name));
|
|
65
67
|
const dependencies = [...new Set(changes.flatMap(c => c.dependencies).filter(d => !inputNames.has(d)))].sort();
|
|
66
68
|
const statements = facts
|
|
67
|
-
.map(f =>
|
|
69
|
+
.map(f => (0, byte_slice_1.sliceStatementBytes)(sqlBytes, f.span.start, f.span.len).trim())
|
|
68
70
|
.filter(Boolean)
|
|
69
71
|
.map(text => (text.endsWith(';') ? text : `${text};`));
|
|
70
72
|
const revert = (0, scripts_1.revertFor)(facts);
|
|
@@ -143,7 +145,7 @@ function restructureChanges(changes, options) {
|
|
|
143
145
|
const groupSql = groupNames.map(() => []);
|
|
144
146
|
const groupStatements = groupNames.map(() => []);
|
|
145
147
|
facts.forEach((f, i) => {
|
|
146
|
-
const text =
|
|
148
|
+
const text = (0, byte_slice_1.sliceStatementBytes)(sqlBytes, f.span.start, f.span.len).trim();
|
|
147
149
|
const g = groupOf[i];
|
|
148
150
|
if (g !== -1 && text) {
|
|
149
151
|
groupSql[g].push(text.endsWith(';') ? text : `${text};`);
|
package/index.d.ts
CHANGED
|
@@ -24,8 +24,8 @@ export type { GeneratedScript, RegeneratedScripts, } from './regen';
|
|
|
24
24
|
export { classifyScript, isStubScript, regenerateScripts, } from './regen';
|
|
25
25
|
export type { ExportGranularity, RestructureExportRowsOptions, RestructureExportRowsResult } from './restructure';
|
|
26
26
|
export { EXPORT_GRANULARITIES, isExportGranularity, restructureExportRows } from './restructure';
|
|
27
|
-
export type { DiffInputChange, ObjectDelta, SemanticDeltaChange, SemanticDiffOptions, SemanticDiffResult, SemanticObjectDiff, } from './semantic-diff-driver';
|
|
28
|
-
export { diffChangeSets, diffSchemas, } from './semantic-diff-driver';
|
|
27
|
+
export type { ChangeCoverage, ChangeObjectCoverage, ChangeObjectDelta, ChangeSatisfaction, DiffInputChange, ObjectDelta, SemanticDeltaChange, SemanticDiffOptions, SemanticDiffResult, SemanticObjectDiff, } from './semantic-diff-driver';
|
|
28
|
+
export { coverChanges, diffChangeSets, diffSchemas, } from './semantic-diff-driver';
|
|
29
29
|
export { nameUnnamedConstraints, subObjectIdentityOf } from './sub-object';
|
|
30
30
|
export * from '@pgsql/transform';
|
|
31
31
|
export { loadModule } from 'plpgsql-parser';
|
package/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.loadModule = exports.subObjectIdentityOf = exports.nameUnnamedConstraints = exports.diffSchemas = exports.diffChangeSets = exports.restructureExportRows = exports.isExportGranularity = exports.EXPORT_GRANULARITIES = exports.regenerateScripts = exports.isStubScript = exports.classifyScript = exports.parseSqlProgram = exports.emitSqlProgram = exports.RESIDUAL_UNIT_PATH = exports.partitionUnits = exports.PartitionCycleError = exports.partitionExportRows = exports.parsePartitionConfig = exports.stripTransactionWrapper = exports.loadModuleSource = exports.writeModule = exports.writeControlFile = exports.checkOverwrite = exports.appendModule = exports.restructureChanges = exports.isChangeGranularity = exports.defaultChangeName = exports.CHANGE_GRANULARITIES = exports.resolveFixtureClosure = exports.firstColumnRef = exports.defaultConstraintName = exports.TIER_PROFILE = exports.categorizeChange = exports.buildCategoryOf = exports.withoutColumnOrder = exports.snapshotCatalog = exports.diffCatalogSnapshots = exports.makeSchemaTranspiler = exports.makeNamespaceValidator = exports.buildSchemaRouter = exports.buildExtensionRouter = void 0;
|
|
17
|
+
exports.loadModule = exports.subObjectIdentityOf = exports.nameUnnamedConstraints = exports.diffSchemas = exports.diffChangeSets = exports.coverChanges = exports.restructureExportRows = exports.isExportGranularity = exports.EXPORT_GRANULARITIES = exports.regenerateScripts = exports.isStubScript = exports.classifyScript = exports.parseSqlProgram = exports.emitSqlProgram = exports.RESIDUAL_UNIT_PATH = exports.partitionUnits = exports.PartitionCycleError = exports.partitionExportRows = exports.parsePartitionConfig = exports.stripTransactionWrapper = exports.loadModuleSource = exports.writeModule = exports.writeControlFile = exports.checkOverwrite = exports.appendModule = exports.restructureChanges = exports.isChangeGranularity = exports.defaultChangeName = exports.CHANGE_GRANULARITIES = exports.resolveFixtureClosure = exports.firstColumnRef = exports.defaultConstraintName = exports.TIER_PROFILE = exports.categorizeChange = exports.buildCategoryOf = exports.withoutColumnOrder = exports.snapshotCatalog = exports.diffCatalogSnapshots = exports.makeSchemaTranspiler = exports.makeNamespaceValidator = exports.buildSchemaRouter = exports.buildExtensionRouter = void 0;
|
|
18
18
|
var bundle_driver_1 = require("./bundle-driver");
|
|
19
19
|
Object.defineProperty(exports, "buildExtensionRouter", { enumerable: true, get: function () { return bundle_driver_1.buildExtensionRouter; } });
|
|
20
20
|
Object.defineProperty(exports, "buildSchemaRouter", { enumerable: true, get: function () { return bundle_driver_1.buildSchemaRouter; } });
|
|
@@ -65,6 +65,7 @@ Object.defineProperty(exports, "EXPORT_GRANULARITIES", { enumerable: true, get:
|
|
|
65
65
|
Object.defineProperty(exports, "isExportGranularity", { enumerable: true, get: function () { return restructure_1.isExportGranularity; } });
|
|
66
66
|
Object.defineProperty(exports, "restructureExportRows", { enumerable: true, get: function () { return restructure_1.restructureExportRows; } });
|
|
67
67
|
var semantic_diff_driver_1 = require("./semantic-diff-driver");
|
|
68
|
+
Object.defineProperty(exports, "coverChanges", { enumerable: true, get: function () { return semantic_diff_driver_1.coverChanges; } });
|
|
68
69
|
Object.defineProperty(exports, "diffChangeSets", { enumerable: true, get: function () { return semantic_diff_driver_1.diffChangeSets; } });
|
|
69
70
|
Object.defineProperty(exports, "diffSchemas", { enumerable: true, get: function () { return semantic_diff_driver_1.diffSchemas; } });
|
|
70
71
|
var sub_object_1 = require("./sub-object");
|
package/module-source.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module ingestion for the dials pipeline: load an on-disk pgpm module into
|
|
3
|
+
* the flattened, classified change list the transform/partition drivers
|
|
4
|
+
* consume. `pgpm transform` uses this today; a future `pgpm import` (dumps)
|
|
5
|
+
* and `pgpm diff` are expected to reuse the same seam.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExtendedPlanFile } from '@pgpmjs/ast';
|
|
1
8
|
/** One change of a loaded module: headerless, unwrapped deploy SQL. */
|
|
2
9
|
export interface ModuleSourceChange {
|
|
3
10
|
/** Change name (plan token). */
|
|
@@ -24,10 +31,21 @@ export interface ModuleSource {
|
|
|
24
31
|
* transaction control.
|
|
25
32
|
*/
|
|
26
33
|
export declare const stripTransactionWrapper: (sql: string) => string;
|
|
34
|
+
/** Options controlling how {@link loadModuleSource} ingests a module. */
|
|
35
|
+
export interface LoadModuleSourceOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Parsed plans of the other packages in the same workspace, keyed by
|
|
38
|
+
* package name (`%project`). Lets a cross-package tag dependency
|
|
39
|
+
* (`pkg:@tag`) resolve against the plan that defines the tag. Without it,
|
|
40
|
+
* cross-package tags cannot be resolved from a single module's plan and are
|
|
41
|
+
* left verbatim with a warning.
|
|
42
|
+
*/
|
|
43
|
+
crossPackagePlans?: Map<string, ExtendedPlanFile>;
|
|
44
|
+
}
|
|
27
45
|
/**
|
|
28
46
|
* Load a pgpm module from disk: parse pgpm.plan and read each change's
|
|
29
47
|
* deploy script, stripping pgpm headers and transaction wrappers so the
|
|
30
48
|
* result can be flattened straight into the dials pipeline
|
|
31
49
|
* (`restructureChanges` / `partitionUnits`).
|
|
32
50
|
*/
|
|
33
|
-
export declare const loadModuleSource: (moduleDir: string) => ModuleSource;
|
|
51
|
+
export declare const loadModuleSource: (moduleDir: string, options?: LoadModuleSourceOptions) => ModuleSource;
|
package/module-source.js
CHANGED
|
@@ -34,12 +34,6 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.loadModuleSource = exports.stripTransactionWrapper = void 0;
|
|
37
|
-
/**
|
|
38
|
-
* Module ingestion for the dials pipeline: load an on-disk pgpm module into
|
|
39
|
-
* the flattened, classified change list the transform/partition drivers
|
|
40
|
-
* consume. `pgpm transform` uses this today; a future `pgpm import` (dumps)
|
|
41
|
-
* and `pgpm diff` are expected to reuse the same seam.
|
|
42
|
-
*/
|
|
43
37
|
const ast_1 = require("@pgpmjs/ast");
|
|
44
38
|
const path = __importStar(require("path"));
|
|
45
39
|
const TX_LINE = /^\s*(BEGIN|COMMIT|ROLLBACK)\s*;\s*$/i;
|
|
@@ -54,22 +48,64 @@ const stripTransactionWrapper = (sql) => sql
|
|
|
54
48
|
.join('\n')
|
|
55
49
|
.trim();
|
|
56
50
|
exports.stripTransactionWrapper = stripTransactionWrapper;
|
|
51
|
+
const CROSS_PACKAGE_TAG = /^([^:]+):@(.+)$/;
|
|
52
|
+
/**
|
|
53
|
+
* Resolve a plan dependency token to a concrete change identity.
|
|
54
|
+
*
|
|
55
|
+
* Non-tag tokens (a bare change name, or a `pkg:change` cross-package name)
|
|
56
|
+
* pass through unchanged — they are already the identity the ledger and
|
|
57
|
+
* generated plans record. A tag token is resolved so a dependency written
|
|
58
|
+
* against a tag is never re-emitted verbatim into a generated plan or a
|
|
59
|
+
* ledger row:
|
|
60
|
+
* - a local tag (`@tag`) resolves to its change name via this plan;
|
|
61
|
+
* - a cross-package tag (`pkg:@tag`) resolves to the canonical `pkg:change`
|
|
62
|
+
* qualified name via `crossPackagePlans[pkg]` when available.
|
|
63
|
+
*
|
|
64
|
+
* Anything that cannot be resolved (unknown tag, or a cross-package tag with
|
|
65
|
+
* no plan in context) is kept verbatim — never dropped — with a warning.
|
|
66
|
+
*/
|
|
67
|
+
const resolveDependencyToken = (token, plan, warnings, changeName, crossPackagePlans) => {
|
|
68
|
+
if (!token.includes('@'))
|
|
69
|
+
return token;
|
|
70
|
+
const cross = token.match(CROSS_PACKAGE_TAG);
|
|
71
|
+
if (cross && cross[1] !== plan.package) {
|
|
72
|
+
const pkg = cross[1];
|
|
73
|
+
const otherPlan = crossPackagePlans?.get(pkg);
|
|
74
|
+
if (!otherPlan) {
|
|
75
|
+
warnings.push(`${changeName}: dependency tag "${token}" is cross-package; left unresolved (no plan for "${pkg}" in context)`);
|
|
76
|
+
return token;
|
|
77
|
+
}
|
|
78
|
+
const resolved = (0, ast_1.resolveReference)(token, otherPlan, pkg);
|
|
79
|
+
if (resolved.error || !resolved.change) {
|
|
80
|
+
warnings.push(`${changeName}: could not resolve cross-package dependency tag "${token}" (${resolved.error ?? 'no matching change'}); left as-is`);
|
|
81
|
+
return token;
|
|
82
|
+
}
|
|
83
|
+
return `${pkg}:${resolved.change}`;
|
|
84
|
+
}
|
|
85
|
+
const resolved = (0, ast_1.resolveReference)(token, plan, plan.package);
|
|
86
|
+
if (resolved.error || !resolved.change) {
|
|
87
|
+
warnings.push(`${changeName}: could not resolve dependency tag "${token}" (${resolved.error ?? 'no matching change'}); left as-is`);
|
|
88
|
+
return token;
|
|
89
|
+
}
|
|
90
|
+
return resolved.change;
|
|
91
|
+
};
|
|
57
92
|
/**
|
|
58
93
|
* Load a pgpm module from disk: parse pgpm.plan and read each change's
|
|
59
94
|
* deploy script, stripping pgpm headers and transaction wrappers so the
|
|
60
95
|
* result can be flattened straight into the dials pipeline
|
|
61
96
|
* (`restructureChanges` / `partitionUnits`).
|
|
62
97
|
*/
|
|
63
|
-
const loadModuleSource = (moduleDir) => {
|
|
98
|
+
const loadModuleSource = (moduleDir, options = {}) => {
|
|
64
99
|
const modulePath = path.resolve(moduleDir);
|
|
65
100
|
const planResult = (0, ast_1.parsePlanFile)(path.join(modulePath, 'pgpm.plan'));
|
|
66
101
|
if (!planResult.data) {
|
|
67
102
|
const reasons = planResult.errors.map(e => e.message).join(', ');
|
|
68
103
|
throw new Error(`Failed to parse pgpm.plan in ${modulePath}: ${reasons}`);
|
|
69
104
|
}
|
|
105
|
+
const plan = planResult.data;
|
|
70
106
|
const warnings = [];
|
|
71
107
|
const changes = [];
|
|
72
|
-
for (const change of
|
|
108
|
+
for (const change of plan.changes) {
|
|
73
109
|
const raw = (0, ast_1.readScript)(modulePath, 'deploy', change.name);
|
|
74
110
|
if (!raw) {
|
|
75
111
|
warnings.push(`${change.name}: no deploy script found, skipping`);
|
|
@@ -78,7 +114,7 @@ const loadModuleSource = (moduleDir) => {
|
|
|
78
114
|
const { body } = (0, ast_1.parsePgpmHeader)(raw);
|
|
79
115
|
changes.push({
|
|
80
116
|
name: change.name,
|
|
81
|
-
dependencies: change.dependencies ?? [],
|
|
117
|
+
dependencies: (change.dependencies ?? []).map(dep => resolveDependencyToken(dep, plan, warnings, change.name, options.crossPackagePlans)),
|
|
82
118
|
deploy: (0, exports.stripTransactionWrapper)(body)
|
|
83
119
|
});
|
|
84
120
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgpmjs/transform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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,16 +39,16 @@
|
|
|
39
39
|
"plpgsql"
|
|
40
40
|
],
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@pgpmjs/bundle": "^0.14.
|
|
42
|
+
"@pgpmjs/bundle": "^0.14.1",
|
|
43
43
|
"makage": "^0.3.0"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@pgpmjs/ast": "^0.11.
|
|
46
|
+
"@pgpmjs/ast": "^0.11.1",
|
|
47
47
|
"@pgpmjs/naming-spec": "^0.3.0",
|
|
48
|
-
"@pgsql/scripts": "^18.3.
|
|
49
|
-
"@pgsql/semantics": "^18.1.
|
|
50
|
-
"@pgsql/transform": "^18.15.
|
|
51
|
-
"plpgsql-parser": "^18.5.
|
|
48
|
+
"@pgsql/scripts": "^18.3.4",
|
|
49
|
+
"@pgsql/semantics": "^18.1.2",
|
|
50
|
+
"@pgsql/transform": "^18.15.2",
|
|
51
|
+
"plpgsql-parser": "^18.5.3"
|
|
52
52
|
},
|
|
53
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "c9dcb0c96174dfb0bc6fd63e46cc7776ea7af173"
|
|
54
54
|
}
|
package/partition-driver.js
CHANGED
|
@@ -27,6 +27,7 @@ exports.partitionUnits = partitionUnits;
|
|
|
27
27
|
const naming_spec_1 = require("@pgpmjs/naming-spec");
|
|
28
28
|
const semantics_1 = require("@pgsql/semantics");
|
|
29
29
|
const transform_1 = require("@pgsql/transform");
|
|
30
|
+
const byte_slice_1 = require("./byte-slice");
|
|
30
31
|
/** A partition whose packages require each other in a cycle — unshippable. */
|
|
31
32
|
class PartitionCycleError extends Error {
|
|
32
33
|
packages;
|
|
@@ -86,6 +87,7 @@ exports.RESIDUAL_UNIT_PATH = 'misc/statements';
|
|
|
86
87
|
*/
|
|
87
88
|
function buildUnitGraph(sql, style, splitRiders) {
|
|
88
89
|
const facts = (0, semantics_1.classifyStatements)(sql);
|
|
90
|
+
const sqlBytes = (0, byte_slice_1.sqlSourceBytes)(sql);
|
|
89
91
|
const graph = (0, transform_1.buildStatementGraph)(facts);
|
|
90
92
|
const unitOf = new Array(facts.length).fill(-1);
|
|
91
93
|
const units = [];
|
|
@@ -168,7 +170,7 @@ function buildUnitGraph(sql, style, splitRiders) {
|
|
|
168
170
|
unitOf[i] = unitFor(null, f.kind);
|
|
169
171
|
});
|
|
170
172
|
facts.forEach((f, i) => {
|
|
171
|
-
const text =
|
|
173
|
+
const text = (0, byte_slice_1.sliceStatementBytes)(sqlBytes, f.span.start, f.span.len).trim();
|
|
172
174
|
if (!text)
|
|
173
175
|
return;
|
|
174
176
|
const unit = units[unitOf[i]];
|
package/program.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.parseSqlProgram = parseSqlProgram;
|
|
|
4
4
|
exports.emitSqlProgram = emitSqlProgram;
|
|
5
5
|
const semantics_1 = require("@pgsql/semantics");
|
|
6
6
|
const plpgsql_parser_1 = require("plpgsql-parser");
|
|
7
|
+
const byte_slice_1 = require("./byte-slice");
|
|
7
8
|
/**
|
|
8
9
|
* Parse a SQL script into a statement program. Requires `loadModule()` from
|
|
9
10
|
* `plpgsql-parser` to have completed.
|
|
@@ -12,6 +13,7 @@ function parseSqlProgram(source) {
|
|
|
12
13
|
const parseResult = (0, plpgsql_parser_1.parseSql)(source);
|
|
13
14
|
const stmts = parseResult?.stmts ?? [];
|
|
14
15
|
const facts = (0, semantics_1.classifyStatements)(source);
|
|
16
|
+
const sourceBytes = (0, byte_slice_1.sqlSourceBytes)(source);
|
|
15
17
|
const statements = [];
|
|
16
18
|
for (let i = 0; i < stmts.length; i++) {
|
|
17
19
|
const rawStmt = stmts[i];
|
|
@@ -22,7 +24,7 @@ function parseSqlProgram(source) {
|
|
|
22
24
|
stmt: rawStmt.stmt,
|
|
23
25
|
facts: facts[i],
|
|
24
26
|
span: { start, len },
|
|
25
|
-
raw:
|
|
27
|
+
raw: (0, byte_slice_1.sliceStatementBytes)(sourceBytes, start, len),
|
|
26
28
|
dirty: false
|
|
27
29
|
});
|
|
28
30
|
}
|
|
@@ -38,16 +40,22 @@ function emitSqlProgram(program) {
|
|
|
38
40
|
const { source, statements } = program;
|
|
39
41
|
if (!statements.some(s => s.dirty))
|
|
40
42
|
return source;
|
|
43
|
+
// Spans are UTF-8 byte offsets; slice statements and the verbatim gaps
|
|
44
|
+
// between them on the bytes so a multibyte character never mis-cuts the
|
|
45
|
+
// reassembly. Statement boundaries fall on character boundaries.
|
|
46
|
+
const sourceBytes = (0, byte_slice_1.sqlSourceBytes)(source);
|
|
41
47
|
const pieces = [];
|
|
42
48
|
let cursor = 0;
|
|
43
49
|
for (const statement of statements) {
|
|
44
50
|
const { start, len } = statement.span;
|
|
45
51
|
if (start > cursor)
|
|
46
|
-
pieces.push(
|
|
47
|
-
pieces.push(statement.dirty
|
|
52
|
+
pieces.push((0, byte_slice_1.sliceStatementBytes)(sourceBytes, cursor, start - cursor));
|
|
53
|
+
pieces.push(statement.dirty
|
|
54
|
+
? plpgsql_parser_1.Deparser.deparse(statement.stmt)
|
|
55
|
+
: (0, byte_slice_1.sliceStatementBytes)(sourceBytes, start, len));
|
|
48
56
|
cursor = start + len;
|
|
49
57
|
}
|
|
50
|
-
if (cursor <
|
|
51
|
-
pieces.push(
|
|
58
|
+
if (cursor < sourceBytes.length)
|
|
59
|
+
pieces.push((0, byte_slice_1.sliceStatementBytes)(sourceBytes, cursor));
|
|
52
60
|
return pieces.join('');
|
|
53
61
|
}
|
|
@@ -90,3 +90,38 @@ export interface DiffInputChange {
|
|
|
90
90
|
* schema yields an identical diff.
|
|
91
91
|
*/
|
|
92
92
|
export declare function diffChangeSets(from: DiffInputChange[], to: DiffInputChange[], options?: SemanticDiffOptions): SemanticDiffResult;
|
|
93
|
+
/** How one object a change touches relates to the diff's delta. */
|
|
94
|
+
export type ChangeObjectDelta = ObjectDelta | 'unchanged';
|
|
95
|
+
/**
|
|
96
|
+
* Whether a `to`-side change is already satisfied by the `from` side:
|
|
97
|
+
* - `satisfied`: every object it touches is unchanged in the diff
|
|
98
|
+
* - `unsatisfied`: every object it touches is added/modified in the diff
|
|
99
|
+
* - `partial`: a mix of unchanged and delta objects
|
|
100
|
+
* - `inert`: no identifiable objects (seed data, grants-only, dynamic SQL)
|
|
101
|
+
*/
|
|
102
|
+
export type ChangeSatisfaction = 'satisfied' | 'unsatisfied' | 'partial' | 'inert';
|
|
103
|
+
export interface ChangeObjectCoverage {
|
|
104
|
+
/** Canonical naming-spec path for the object. */
|
|
105
|
+
path: string;
|
|
106
|
+
delta: ChangeObjectDelta;
|
|
107
|
+
}
|
|
108
|
+
export interface ChangeCoverage {
|
|
109
|
+
/** The input change's name (as given, e.g. `pkg:change`). */
|
|
110
|
+
name: string;
|
|
111
|
+
status: ChangeSatisfaction;
|
|
112
|
+
objects: ChangeObjectCoverage[];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Cover each `to`-side change against a semantic diff's per-object delta:
|
|
116
|
+
* which of the objects the change touches are already present and identical
|
|
117
|
+
* on the `from` side. A `satisfied` change deploys nothing new — its ledger
|
|
118
|
+
* entry can be backfilled instead of executing it; an `unsatisfied` change is
|
|
119
|
+
* the genuine delta. Identity keying is shared with the diff itself, so
|
|
120
|
+
* regrouped/renamed/reordered authorship of the same objects covers the same.
|
|
121
|
+
*
|
|
122
|
+
* `diff` must come from diffing the same `from` side against these changes
|
|
123
|
+
* (`diffChangeSets(fromChanges, toChanges)`).
|
|
124
|
+
*/
|
|
125
|
+
export declare function coverChanges(changes: DiffInputChange[], diff: SemanticDiffResult, options?: {
|
|
126
|
+
style?: PathStyle;
|
|
127
|
+
}): ChangeCoverage[];
|
package/semantic-diff-driver.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.diffSchemas = diffSchemas;
|
|
4
4
|
exports.diffChangeSets = diffChangeSets;
|
|
5
|
+
exports.coverChanges = coverChanges;
|
|
5
6
|
/**
|
|
6
7
|
* Semantic diff driver: identity-keyed schema delta between two deploy
|
|
7
8
|
* surfaces.
|
|
@@ -35,6 +36,7 @@ const scripts_1 = require("@pgsql/scripts");
|
|
|
35
36
|
const semantics_1 = require("@pgsql/semantics");
|
|
36
37
|
const transform_1 = require("@pgsql/transform");
|
|
37
38
|
const plpgsql_parser_1 = require("plpgsql-parser");
|
|
39
|
+
const byte_slice_1 = require("./byte-slice");
|
|
38
40
|
const constraint_names_1 = require("./constraint-names");
|
|
39
41
|
const granularity_driver_1 = require("./granularity-driver");
|
|
40
42
|
const identityKey = (id) => `${id.kind}\u0000${id.schema ?? ''}\u0000${id.table ?? ''}\u0000${id.name}`;
|
|
@@ -51,8 +53,9 @@ function readSide(sql, warnings, label) {
|
|
|
51
53
|
const order = [];
|
|
52
54
|
const unitOf = new Array(facts.length).fill(null);
|
|
53
55
|
const byObject = new Map();
|
|
56
|
+
const sqlBytes = (0, byte_slice_1.sqlSourceBytes)(sql);
|
|
54
57
|
facts.forEach((f, i) => {
|
|
55
|
-
const text =
|
|
58
|
+
const text = (0, byte_slice_1.sliceStatementBytes)(sqlBytes, f.span.start, f.span.len).trim();
|
|
56
59
|
if (!text)
|
|
57
60
|
return;
|
|
58
61
|
let identity = groupingIdentity(f);
|
|
@@ -506,3 +509,38 @@ function diffChangeSets(from, to, options = {}) {
|
|
|
506
509
|
const flatten = (changes) => changes.map(c => c.deploy.trim()).filter(Boolean).join('\n\n');
|
|
507
510
|
return diffSchemas(flatten(from), flatten(to), options);
|
|
508
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* Cover each `to`-side change against a semantic diff's per-object delta:
|
|
514
|
+
* which of the objects the change touches are already present and identical
|
|
515
|
+
* on the `from` side. A `satisfied` change deploys nothing new — its ledger
|
|
516
|
+
* entry can be backfilled instead of executing it; an `unsatisfied` change is
|
|
517
|
+
* the genuine delta. Identity keying is shared with the diff itself, so
|
|
518
|
+
* regrouped/renamed/reordered authorship of the same objects covers the same.
|
|
519
|
+
*
|
|
520
|
+
* `diff` must come from diffing the same `from` side against these changes
|
|
521
|
+
* (`diffChangeSets(fromChanges, toChanges)`).
|
|
522
|
+
*/
|
|
523
|
+
function coverChanges(changes, diff, options = {}) {
|
|
524
|
+
const style = options.style ?? 'directory';
|
|
525
|
+
const deltaByKey = new Map();
|
|
526
|
+
for (const obj of diff.objects) {
|
|
527
|
+
deltaByKey.set(identityKey(obj.identity), obj.delta);
|
|
528
|
+
}
|
|
529
|
+
return changes.map(change => {
|
|
530
|
+
const sideWarnings = [];
|
|
531
|
+
const side = readSide(change.deploy, sideWarnings, change.name);
|
|
532
|
+
const objects = side.order.map(key => {
|
|
533
|
+
const unit = side.units.get(key);
|
|
534
|
+
return {
|
|
535
|
+
path: (0, naming_spec_1.pathFor)(unit.identity, { style }),
|
|
536
|
+
delta: deltaByKey.get(key) ?? 'unchanged'
|
|
537
|
+
};
|
|
538
|
+
});
|
|
539
|
+
const touched = objects.filter(o => o.delta !== 'unchanged').length;
|
|
540
|
+
const status = objects.length === 0 ? 'inert'
|
|
541
|
+
: touched === 0 ? 'satisfied'
|
|
542
|
+
: touched === objects.length ? 'unsatisfied'
|
|
543
|
+
: 'partial';
|
|
544
|
+
return { name: change.name, status, objects };
|
|
545
|
+
});
|
|
546
|
+
}
|
package/sub-object.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.subObjectIdentityOf = subObjectIdentityOf;
|
|
4
4
|
exports.nameUnnamedConstraints = nameUnnamedConstraints;
|
|
5
5
|
const plpgsql_parser_1 = require("plpgsql-parser");
|
|
6
|
+
const byte_slice_1 = require("./byte-slice");
|
|
6
7
|
const constraint_names_1 = require("./constraint-names");
|
|
7
8
|
/** The single AlterTableCmd of a one-command ALTER TABLE, or null. */
|
|
8
9
|
function singleCmd(stmt) {
|
|
@@ -50,6 +51,7 @@ function subObjectIdentityOf(facts) {
|
|
|
50
51
|
*/
|
|
51
52
|
function nameUnnamedConstraints(sql) {
|
|
52
53
|
const parsed = (0, plpgsql_parser_1.parseSync)(sql);
|
|
54
|
+
const sqlBytes = (0, byte_slice_1.sqlSourceBytes)(sql);
|
|
53
55
|
const pieces = [];
|
|
54
56
|
for (const raw of parsed.sql.stmts) {
|
|
55
57
|
const node = raw.stmt;
|
|
@@ -75,7 +77,7 @@ function nameUnnamedConstraints(sql) {
|
|
|
75
77
|
else {
|
|
76
78
|
const start = raw.stmt_location ?? 0;
|
|
77
79
|
const len = raw.stmt_len;
|
|
78
|
-
const text =
|
|
80
|
+
const text = (0, byte_slice_1.sliceStatementBytes)(sqlBytes, start, len);
|
|
79
81
|
const trimmed = text.trim();
|
|
80
82
|
pieces.push(trimmed.endsWith(';') ? trimmed : `${trimmed};`);
|
|
81
83
|
}
|