From 4e713e69a8f52c8235be4ae5ea361997d99887e0 Mon Sep 17 00:00:00 2001 From: emirmrkaljevic <93012621+emirmrkaljevic@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:53:38 +0200 Subject: [PATCH] Support MERGE statement in PostgreSQL Closes #986 --- .../postgresql/postgresql.formatter.ts | 6 +++- .../postgresql/postgresql.functions.ts | 3 ++ test/postgresql.test.ts | 28 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/languages/postgresql/postgresql.formatter.ts b/src/languages/postgresql/postgresql.formatter.ts index d3d8a51fd2..324d2a79c6 100644 --- a/src/languages/postgresql/postgresql.formatter.ts +++ b/src/languages/postgresql/postgresql.formatter.ts @@ -21,11 +21,15 @@ const reservedClauses = expandPhrases([ 'FOR {UPDATE | NO KEY UPDATE | SHARE | KEY SHARE} [OF]', // Data manipulation // - insert: - 'INSERT INTO', + 'INSERT [INTO]', 'VALUES', 'DEFAULT VALUES', // - update: 'SET', + // - merge: + 'MERGE INTO', + 'WHEN [NOT] MATCHED [BY SOURCE | BY TARGET] [THEN]', + 'UPDATE SET', // other 'RETURNING', ]); diff --git a/src/languages/postgresql/postgresql.functions.ts b/src/languages/postgresql/postgresql.functions.ts index 6c142e100e..c18a2520c4 100644 --- a/src/languages/postgresql/postgresql.functions.ts +++ b/src/languages/postgresql/postgresql.functions.ts @@ -710,6 +710,9 @@ export const functions: string[] = [ // https://www.postgresql.org/docs/14/functions-statistics.html 'PG_MCV_LIST_ITEMS', + // https://www.postgresql.org/docs/17/functions-merge-support.html + 'MERGE_ACTION', + // cast 'CAST', ]; diff --git a/test/postgresql.test.ts b/test/postgresql.test.ts index e8e99adfea..8e6570b799 100644 --- a/test/postgresql.test.ts +++ b/test/postgresql.test.ts @@ -19,6 +19,7 @@ import supportsCreateView from './features/createView.js'; import supportsIsDistinctFrom from './features/isDistinctFrom.js'; import supportsArrayLiterals from './features/arrayLiterals.js'; import supportsDataTypeCase from './options/dataTypeCase.js'; +import supportsMergeInto from './features/mergeInto.js'; import behavesLikePostgresqlFormatter from './behavesLikePostgresqlFormatter.js'; describe('PostgreSqlFormatter', () => { @@ -147,6 +148,33 @@ describe('PostgreSqlFormatter', () => { supportsParams(format, { numbered: ['$'] }); supportsLimiting(format, { limit: true, offset: true, fetchFirst: true, fetchNext: true }); supportsDataTypeCase(format); + supportsMergeInto(format); + + // Issue #986 + it('formats MERGE with PostgreSQL 17 additions', () => { + const result = format( + `MERGE INTO stock t USING incoming s ON t.sku = s.sku + WHEN MATCHED AND s.qty = 0 THEN DELETE + WHEN NOT MATCHED BY SOURCE THEN DO NOTHING + WHEN NOT MATCHED BY TARGET THEN INSERT VALUES (s.sku, s.qty) + RETURNING merge_action(), t.*;` + ); + expect(result).toBe(dedent` + MERGE INTO + stock t USING incoming s ON t.sku = s.sku + WHEN MATCHED + AND s.qty = 0 THEN DELETE + WHEN NOT MATCHED BY SOURCE THEN + DO NOTHING + WHEN NOT MATCHED BY TARGET THEN + INSERT + VALUES + (s.sku, s.qty) + RETURNING + merge_action(), + t.*; + `); + }); // Regression test for issue #935 it('preserves pg_dump restrict commands', () => {