diff --git a/pgpm/core/__tests__/packaging/round-trip-diff.test.ts b/pgpm/core/__tests__/packaging/round-trip-diff.test.ts new file mode 100644 index 000000000..b645f63e8 --- /dev/null +++ b/pgpm/core/__tests__/packaging/round-trip-diff.test.ts @@ -0,0 +1,28 @@ +import { mergeSqlStatements } from '../../src/packaging/package'; + +/** + * The round-trip check (parse -> deparse -> reparse) must ignore positional + * metadata. PG17+ emits list_start/list_end (A_ArrayExpr) and + * rexpr_list_start/rexpr_list_end (A_Expr IN lists), which shift whenever the + * deparsed SQL is formatted differently than the source. + */ +describe('AST round-trip diff detection', () => { + it('does not flag ARRAY[...] expressions', async () => { + const { diff } = await mergeSqlStatements( + `CREATE TABLE things ( + id int, + tags text[] DEFAULT ARRAY['a', 'b', 'c']::text[] + );`, + {} + ); + expect(diff).toBeUndefined(); + }); + + it('does not flag IN (...) lists', async () => { + const { diff } = await mergeSqlStatements( + `SELECT * FROM things WHERE id IN (1, 2, 3);`, + {} + ); + expect(diff).toBeUndefined(); + }); +}); diff --git a/pgpm/core/src/packaging/package.ts b/pgpm/core/src/packaging/package.ts index 4f7851170..8c647b9d8 100644 --- a/pgpm/core/src/packaging/package.ts +++ b/pgpm/core/src/packaging/package.ts @@ -22,6 +22,24 @@ export const cleanTree = (tree: any): any => { }); }; +/** + * Same as cleanTree, but also drops the byte-offset fields PG17+ added for list + * expressions (`ARRAY[...]`, `x IN (...)`). They are positional metadata, not + * semantics, so they must not count as a round-trip difference. Kept separate + * from cleanTree so migration hashes (hashSqlFile) stay stable. + */ +export const cleanTreeForDiff = (tree: any): any => { + return transformProps(tree, { + stmt_len: noop, + stmt_location: noop, + location: noop, + list_start: noop, + list_end: noop, + rexpr_list_start: noop, + rexpr_list_end: noop, + }); +}; + interface PackageModuleOptions { usePlan?: boolean; extension?: boolean; @@ -94,11 +112,11 @@ export const mergeSqlStatements = async ( sql: `${topLine}${finalSql}`, }; - const diff = JSON.stringify(cleanTree(tree1)) !== JSON.stringify(cleanTree(tree2)); + const diff = JSON.stringify(cleanTreeForDiff(tree1)) !== JSON.stringify(cleanTreeForDiff(tree2)); if (diff) { results.diff = true; - results.tree1 = JSON.stringify(cleanTree(tree1), null, 2); - results.tree2 = JSON.stringify(cleanTree(tree2), null, 2); + results.tree1 = JSON.stringify(cleanTreeForDiff(tree1), null, 2); + results.tree2 = JSON.stringify(cleanTreeForDiff(tree2), null, 2); } return results;