@pgpmjs/transform 0.22.0 → 0.23.1
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/module-source.js +29 -13
- package/esm/partition-driver.js +3 -1
- package/esm/program.js +13 -5
- package/esm/semantic-diff-driver.js +3 -1
- package/esm/sub-object.js +3 -1
- package/granularity-driver.js +4 -2
- package/module-source.d.ts +19 -1
- package/module-source.js +29 -13
- package/package.json +4 -4
- package/partition-driver.js +3 -1
- package/program.js +13 -5
- package/semantic-diff-driver.js +3 -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/module-source.js
CHANGED
|
@@ -11,29 +11,45 @@ export const stripTransactionWrapper = (sql) => sql
|
|
|
11
11
|
.filter(line => !TX_LINE.test(line))
|
|
12
12
|
.join('\n')
|
|
13
13
|
.trim();
|
|
14
|
+
const CROSS_PACKAGE_TAG = /^([^:]+):@(.+)$/;
|
|
14
15
|
/**
|
|
15
|
-
* Resolve a plan dependency token to a concrete change
|
|
16
|
+
* Resolve a plan dependency token to a concrete change identity.
|
|
16
17
|
*
|
|
17
18
|
* Non-tag tokens (a bare change name, or a `pkg:change` cross-package name)
|
|
18
19
|
* pass through unchanged — they are already the identity the ledger and
|
|
19
|
-
* generated plans record. A tag token
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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.
|
|
25
29
|
*/
|
|
26
|
-
const resolveDependencyToken = (token, plan, warnings, changeName) => {
|
|
30
|
+
const resolveDependencyToken = (token, plan, warnings, changeName, crossPackagePlans) => {
|
|
27
31
|
if (!token.includes('@'))
|
|
28
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
|
+
}
|
|
29
48
|
const resolved = resolveReference(token, plan, plan.package);
|
|
30
49
|
if (resolved.error || !resolved.change) {
|
|
31
50
|
warnings.push(`${changeName}: could not resolve dependency tag "${token}" (${resolved.error ?? 'no matching change'}); left as-is`);
|
|
32
51
|
return token;
|
|
33
52
|
}
|
|
34
|
-
if (resolved.change === token) {
|
|
35
|
-
warnings.push(`${changeName}: dependency tag "${token}" is cross-package; left unresolved (resolve it in workspace context)`);
|
|
36
|
-
}
|
|
37
53
|
return resolved.change;
|
|
38
54
|
};
|
|
39
55
|
/**
|
|
@@ -42,7 +58,7 @@ const resolveDependencyToken = (token, plan, warnings, changeName) => {
|
|
|
42
58
|
* result can be flattened straight into the dials pipeline
|
|
43
59
|
* (`restructureChanges` / `partitionUnits`).
|
|
44
60
|
*/
|
|
45
|
-
export const loadModuleSource = (moduleDir) => {
|
|
61
|
+
export const loadModuleSource = (moduleDir, options = {}) => {
|
|
46
62
|
const modulePath = path.resolve(moduleDir);
|
|
47
63
|
const planResult = parsePlanFile(path.join(modulePath, 'pgpm.plan'));
|
|
48
64
|
if (!planResult.data) {
|
|
@@ -61,7 +77,7 @@ export const loadModuleSource = (moduleDir) => {
|
|
|
61
77
|
const { body } = parsePgpmHeader(raw);
|
|
62
78
|
changes.push({
|
|
63
79
|
name: change.name,
|
|
64
|
-
dependencies: (change.dependencies ?? []).map(dep => resolveDependencyToken(dep, plan, warnings, change.name)),
|
|
80
|
+
dependencies: (change.dependencies ?? []).map(dep => resolveDependencyToken(dep, plan, warnings, change.name, options.crossPackagePlans)),
|
|
65
81
|
deploy: stripTransactionWrapper(body)
|
|
66
82
|
});
|
|
67
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);
|
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/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
|
@@ -48,29 +48,45 @@ const stripTransactionWrapper = (sql) => sql
|
|
|
48
48
|
.join('\n')
|
|
49
49
|
.trim();
|
|
50
50
|
exports.stripTransactionWrapper = stripTransactionWrapper;
|
|
51
|
+
const CROSS_PACKAGE_TAG = /^([^:]+):@(.+)$/;
|
|
51
52
|
/**
|
|
52
|
-
* Resolve a plan dependency token to a concrete change
|
|
53
|
+
* Resolve a plan dependency token to a concrete change identity.
|
|
53
54
|
*
|
|
54
55
|
* Non-tag tokens (a bare change name, or a `pkg:change` cross-package name)
|
|
55
56
|
* pass through unchanged — they are already the identity the ledger and
|
|
56
|
-
* generated plans record. A tag token
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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.
|
|
62
66
|
*/
|
|
63
|
-
const resolveDependencyToken = (token, plan, warnings, changeName) => {
|
|
67
|
+
const resolveDependencyToken = (token, plan, warnings, changeName, crossPackagePlans) => {
|
|
64
68
|
if (!token.includes('@'))
|
|
65
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
|
+
}
|
|
66
85
|
const resolved = (0, ast_1.resolveReference)(token, plan, plan.package);
|
|
67
86
|
if (resolved.error || !resolved.change) {
|
|
68
87
|
warnings.push(`${changeName}: could not resolve dependency tag "${token}" (${resolved.error ?? 'no matching change'}); left as-is`);
|
|
69
88
|
return token;
|
|
70
89
|
}
|
|
71
|
-
if (resolved.change === token) {
|
|
72
|
-
warnings.push(`${changeName}: dependency tag "${token}" is cross-package; left unresolved (resolve it in workspace context)`);
|
|
73
|
-
}
|
|
74
90
|
return resolved.change;
|
|
75
91
|
};
|
|
76
92
|
/**
|
|
@@ -79,7 +95,7 @@ const resolveDependencyToken = (token, plan, warnings, changeName) => {
|
|
|
79
95
|
* result can be flattened straight into the dials pipeline
|
|
80
96
|
* (`restructureChanges` / `partitionUnits`).
|
|
81
97
|
*/
|
|
82
|
-
const loadModuleSource = (moduleDir) => {
|
|
98
|
+
const loadModuleSource = (moduleDir, options = {}) => {
|
|
83
99
|
const modulePath = path.resolve(moduleDir);
|
|
84
100
|
const planResult = (0, ast_1.parsePlanFile)(path.join(modulePath, 'pgpm.plan'));
|
|
85
101
|
if (!planResult.data) {
|
|
@@ -98,7 +114,7 @@ const loadModuleSource = (moduleDir) => {
|
|
|
98
114
|
const { body } = (0, ast_1.parsePgpmHeader)(raw);
|
|
99
115
|
changes.push({
|
|
100
116
|
name: change.name,
|
|
101
|
-
dependencies: (change.dependencies ?? []).map(dep => resolveDependencyToken(dep, plan, warnings, change.name)),
|
|
117
|
+
dependencies: (change.dependencies ?? []).map(dep => resolveDependencyToken(dep, plan, warnings, change.name, options.crossPackagePlans)),
|
|
102
118
|
deploy: (0, exports.stripTransactionWrapper)(body)
|
|
103
119
|
});
|
|
104
120
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgpmjs/transform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.1",
|
|
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.2",
|
|
43
43
|
"makage": "^0.3.0"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@pgpmjs/ast": "^0.11.
|
|
46
|
+
"@pgpmjs/ast": "^0.11.2",
|
|
47
47
|
"@pgpmjs/naming-spec": "^0.3.0",
|
|
48
48
|
"@pgsql/scripts": "^18.3.4",
|
|
49
49
|
"@pgsql/semantics": "^18.1.2",
|
|
50
50
|
"@pgsql/transform": "^18.15.2",
|
|
51
51
|
"plpgsql-parser": "^18.5.3"
|
|
52
52
|
},
|
|
53
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "43121004a8bedd2c5fcf7bf33208079d55bad01f"
|
|
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
|
}
|
package/semantic-diff-driver.js
CHANGED
|
@@ -36,6 +36,7 @@ const scripts_1 = require("@pgsql/scripts");
|
|
|
36
36
|
const semantics_1 = require("@pgsql/semantics");
|
|
37
37
|
const transform_1 = require("@pgsql/transform");
|
|
38
38
|
const plpgsql_parser_1 = require("plpgsql-parser");
|
|
39
|
+
const byte_slice_1 = require("./byte-slice");
|
|
39
40
|
const constraint_names_1 = require("./constraint-names");
|
|
40
41
|
const granularity_driver_1 = require("./granularity-driver");
|
|
41
42
|
const identityKey = (id) => `${id.kind}\u0000${id.schema ?? ''}\u0000${id.table ?? ''}\u0000${id.name}`;
|
|
@@ -52,8 +53,9 @@ function readSide(sql, warnings, label) {
|
|
|
52
53
|
const order = [];
|
|
53
54
|
const unitOf = new Array(facts.length).fill(null);
|
|
54
55
|
const byObject = new Map();
|
|
56
|
+
const sqlBytes = (0, byte_slice_1.sqlSourceBytes)(sql);
|
|
55
57
|
facts.forEach((f, i) => {
|
|
56
|
-
const text =
|
|
58
|
+
const text = (0, byte_slice_1.sliceStatementBytes)(sqlBytes, f.span.start, f.span.len).trim();
|
|
57
59
|
if (!text)
|
|
58
60
|
return;
|
|
59
61
|
let identity = groupingIdentity(f);
|
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
|
}
|