Skip to content

Commit 45c05b7

Browse files
authored
Merge pull request #302 from constructive-io/feat/plpgsql-parser-multifn-bodies
fix(plpgsql-parser): multi-function scripts no longer all get the first function's body
2 parents 71bffc8 + b96a43c commit 45c05b7

2 files changed

Lines changed: 70 additions & 3 deletions

File tree

‎packages/plpgsql-parser/__tests__/plpgsql-parser.test.ts‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,62 @@ describe('plpgsql-parser', () => {
9191

9292
expect(result).toContain('visitor_renamed');
9393
});
94+
95+
it('should preserve each function body in multi-function scripts', () => {
96+
const multiFunctionSql = `
97+
CREATE FUNCTION f1() RETURNS void LANGUAGE plpgsql AS $$
98+
BEGIN
99+
RAISE NOTICE 'one';
100+
END;
101+
$$;
102+
103+
CREATE FUNCTION f2() RETURNS void LANGUAGE plpgsql AS $$
104+
BEGIN
105+
RAISE NOTICE 'two';
106+
END;
107+
$$;
108+
109+
CREATE FUNCTION f3() RETURNS trigger LANGUAGE plpgsql AS $$
110+
BEGIN
111+
IF TG_OP = 'INSERT' THEN
112+
RETURN NEW;
113+
END IF;
114+
END;
115+
$$;
116+
`;
117+
const result = transformSync(multiFunctionSql, () => {});
118+
119+
expect(result).toContain(`'one'`);
120+
expect(result).toContain(`'two'`);
121+
expect(result).toContain('RETURN new');
122+
expect((result.match(/'one'/g) || []).length).toBe(1);
123+
expect((result.match(/'two'/g) || []).length).toBe(1);
124+
});
125+
126+
it('should associate the correct plpgsql AST with each parsed function', () => {
127+
const multiFunctionSql = `
128+
CREATE TABLE t (id int);
129+
130+
CREATE FUNCTION fa() RETURNS void LANGUAGE plpgsql AS $$
131+
BEGIN
132+
RAISE NOTICE 'alpha';
133+
END;
134+
$$;
135+
136+
CREATE FUNCTION fb() RETURNS void LANGUAGE plpgsql AS $$
137+
BEGIN
138+
RAISE NOTICE 'beta';
139+
END;
140+
$$;
141+
`;
142+
const parsed = parse(multiFunctionSql);
143+
144+
expect(parsed.functions).toHaveLength(2);
145+
expect(JSON.stringify(parsed.functions[0].plpgsql.raw)).toContain('alpha');
146+
expect(JSON.stringify(parsed.functions[0].plpgsql.raw)).not.toContain('beta');
147+
expect(JSON.stringify(parsed.functions[1].plpgsql.raw)).toContain('beta');
148+
expect(JSON.stringify(parsed.functions[1].plpgsql.raw)).not.toContain('alpha');
149+
});
94150
});
95151

96152
describe('deparseSync', () => {

‎packages/plpgsql-parser/src/parse.ts‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,14 @@ function isPlpgsqlFunction(stmt: any): boolean {
6161
return language === 'plpgsql';
6262
}
6363

64-
function extractFunctionInfo(stmt: any, stmtIndex: number, fullSql: string): ParsedFunction | null {
64+
function getStatementSql(sqlBuffer: Buffer, rawStmt: any): string {
65+
const start = rawStmt?.stmt_location ?? 0;
66+
const len = rawStmt?.stmt_len;
67+
const end = len !== undefined ? start + len : sqlBuffer.length;
68+
return sqlBuffer.slice(start, end).toString('utf8');
69+
}
70+
71+
function extractFunctionInfo(stmt: any, stmtIndex: number, stmtSql: string): ParsedFunction | null {
6572
const createFunctionStmt = stmt?.CreateFunctionStmt;
6673
if (!createFunctionStmt) return null;
6774

@@ -72,7 +79,10 @@ function extractFunctionInfo(stmt: any, stmtIndex: number, fullSql: string): Par
7279
if (!body) return null;
7380

7481
try {
75-
const plpgsqlRaw = parsePlPgSQLSync(fullSql) as unknown as PLpgSQLParseResult;
82+
// Parse only this statement's SQL. Parsing the full script would return
83+
// every function's PL/pgSQL AST in plpgsql_funcs, and downstream deparse
84+
// pairs each statement with plpgsql_funcs[0].
85+
const plpgsqlRaw = parsePlPgSQLSync(stmtSql) as unknown as PLpgSQLParseResult;
7686
const { ast: hydrated, stats, errors } = hydratePlpgsqlAst(plpgsqlRaw);
7787

7888
return {
@@ -99,14 +109,15 @@ export function parse(sql: string, options: ParseOptions = {}): ParsedScript {
99109
const sqlResult: ParseResult = parseSqlSync(sql);
100110
const items: ParsedItem[] = [];
101111
const functions: ParsedFunction[] = [];
112+
const sqlBuffer = Buffer.from(sql, 'utf8');
102113

103114
if (sqlResult.stmts) {
104115
for (let i = 0; i < sqlResult.stmts.length; i++) {
105116
const rawStmt = sqlResult.stmts[i];
106117
const stmt = rawStmt?.stmt;
107118

108119
if (stmt && isPlpgsqlFunction(stmt) && hydrate) {
109-
const fnInfo = extractFunctionInfo(stmt, i, sql);
120+
const fnInfo = extractFunctionInfo(stmt, i, getStatementSql(sqlBuffer, rawStmt));
110121
if (fnInfo) {
111122
items.push(fnInfo);
112123
functions.push(fnInfo);

0 commit comments

Comments
 (0)