From 88d3a311e28755e41cec5aba3042e62e8712736c Mon Sep 17 00:00:00 2001 From: Alexey Date: Thu, 21 May 2026 21:09:58 +0300 Subject: [PATCH] fix(plpgsql): sync generator with committed grammar Update the PL/pgSQL grammar generator so it matches the committed plpgsql/grammar.js for drift-sensitive rules. - emit alias declarations before variable declarations - emit FETCH as optional FROM plus required cursor name - add a lightweight generator drift test for decl_statement and stmt_fetch This prevents future regeneration from undoing the current committed grammar shape. --- script/generate-plpgsql-grammar.js | 8 +++--- script/generate-plpgsql-grammar.test.js | 35 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 script/generate-plpgsql-grammar.test.js diff --git a/script/generate-plpgsql-grammar.js b/script/generate-plpgsql-grammar.js index d8808bc..4ab8da3 100644 --- a/script/generate-plpgsql-grammar.js +++ b/script/generate-plpgsql-grammar.js @@ -196,6 +196,9 @@ module.exports = grammar({ ), decl_statement: $ => choice( + // Alias declaration: name ALIAS FOR target ; + // Must precede variable declaration so ALIAS is not consumed as a type name. + seq($.decl_varname, $.kw_alias, $.kw_for, $.any_identifier, ';'), // Variable declaration: name [CONSTANT] type [COLLATE collation] [NOT NULL] [DEFAULT|:=|= expr] ; seq( $.decl_varname, @@ -206,8 +209,6 @@ module.exports = grammar({ optional($.decl_defval), ';' ), - // Alias declaration: name ALIAS FOR target ; - seq($.decl_varname, $.kw_alias, $.kw_for, $.any_identifier, ';'), // Cursor declaration: name [NO SCROLL | SCROLL] CURSOR [(args)] FOR|IS query ; seq( $.decl_varname, @@ -560,7 +561,8 @@ module.exports = grammar({ stmt_fetch: $ => seq( $.kw_fetch, optional($.fetch_direction), - optional(seq($.kw_from, $.any_identifier)), + optional($.kw_from), + $.any_identifier, $.kw_into, $.into_target, ';' diff --git a/script/generate-plpgsql-grammar.test.js b/script/generate-plpgsql-grammar.test.js new file mode 100644 index 0000000..cbb0db2 --- /dev/null +++ b/script/generate-plpgsql-grammar.test.js @@ -0,0 +1,35 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { test } = require('node:test'); + +const projectRoot = path.join(__dirname, '..'); + +function readRepoFile(relativePath) { + return fs.readFileSync(path.join(projectRoot, relativePath), 'utf8'); +} + +function extractTopLevelRule(source, ruleName) { + const pattern = new RegExp( + String.raw`^ ${ruleName}: \$ => [\s\S]*?^ \),`, + 'm' + ); + const match = source.match(pattern); + assert.ok(match, `Could not find top-level rule ${ruleName}`); + return match[0].trimEnd(); +} + +test('PL/pgSQL generator template matches committed grammar for drift-sensitive rules', () => { + const generator = readRepoFile('script/generate-plpgsql-grammar.js'); + const grammar = readRepoFile('plpgsql/grammar.js'); + + for (const ruleName of ['decl_statement', 'stmt_fetch']) { + assert.equal( + extractTopLevelRule(generator, ruleName), + extractTopLevelRule(grammar, ruleName), + `${ruleName} differs between script/generate-plpgsql-grammar.js and plpgsql/grammar.js` + ); + } +});