@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.
@@ -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 => sql.slice(f.span.start, f.span.start + f.span.len).trim())
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 = sql.slice(f.span.start, f.span.start + f.span.len).trim();
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};`);
@@ -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 name.
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 (`@tag`) is resolved to its change via
20
- * the plan that defines it, so a dependency written against a tag is not
21
- * re-emitted verbatim into a generated plan or a ledger row. Cross-package
22
- * tags (`pkg:@tag`) cannot be resolved from a single module's plan and are
23
- * left as-is with a warning; the resolver returns them unchanged. Anything
24
- * that fails to resolve is kept verbatim (never dropped) with a warning.
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
  }
@@ -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 = sql.slice(f.span.start, f.span.start + f.span.len).trim();
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: source.slice(start, start + len),
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(source.slice(cursor, start));
43
- pieces.push(statement.dirty ? Deparser.deparse(statement.stmt) : source.slice(start, start + len));
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 < source.length)
47
- pieces.push(source.slice(cursor));
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 = sql.slice(f.span.start, f.span.start + f.span.len).trim();
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 = len === undefined ? sql.slice(start) : sql.slice(start, start + len);
76
+ const text = sliceStatementBytes(sqlBytes, start, len);
75
77
  const trimmed = text.trim();
76
78
  pieces.push(trimmed.endsWith(';') ? trimmed : `${trimmed};`);
77
79
  }
@@ -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 => sql.slice(f.span.start, f.span.start + f.span.len).trim())
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 = sql.slice(f.span.start, f.span.start + f.span.len).trim();
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};`);
@@ -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 name.
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 (`@tag`) is resolved to its change via
57
- * the plan that defines it, so a dependency written against a tag is not
58
- * re-emitted verbatim into a generated plan or a ledger row. Cross-package
59
- * tags (`pkg:@tag`) cannot be resolved from a single module's plan and are
60
- * left as-is with a warning; the resolver returns them unchanged. Anything
61
- * that fails to resolve is kept verbatim (never dropped) with a warning.
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.22.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.1",
42
+ "@pgpmjs/bundle": "^0.14.2",
43
43
  "makage": "^0.3.0"
44
44
  },
45
45
  "dependencies": {
46
- "@pgpmjs/ast": "^0.11.1",
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": "6da0705f8aa49d62dc2defb8b135b71674611483"
53
+ "gitHead": "43121004a8bedd2c5fcf7bf33208079d55bad01f"
54
54
  }
@@ -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 = sql.slice(f.span.start, f.span.start + f.span.len).trim();
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: source.slice(start, start + len),
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(source.slice(cursor, start));
47
- pieces.push(statement.dirty ? plpgsql_parser_1.Deparser.deparse(statement.stmt) : source.slice(start, start + len));
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 < source.length)
51
- pieces.push(source.slice(cursor));
58
+ if (cursor < sourceBytes.length)
59
+ pieces.push((0, byte_slice_1.sliceStatementBytes)(sourceBytes, cursor));
52
60
  return pieces.join('');
53
61
  }
@@ -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 = sql.slice(f.span.start, f.span.start + f.span.len).trim();
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 = len === undefined ? sql.slice(start) : sql.slice(start, start + len);
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
  }