plpgsql-parse 0.2.0

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.
package/esm/deparse.js ADDED
@@ -0,0 +1,573 @@
1
+ /**
2
+ * Enhanced deparse that re-injects body comments into PL/pgSQL function
3
+ * bodies after the standard plpgsql-deparser has produced its output.
4
+ *
5
+ * Also handles outer SQL comments/whitespace via pgsql-parse's deparser.
6
+ *
7
+ * This module does NOT modify plpgsql-deparser or pgsql-deparser.
8
+ */
9
+ import { parsePlPgSQLSync, } from '@libpg-query/parser';
10
+ import { isRawComment, isRawWhitespace, isRawStmt, } from 'pgsql-parse';
11
+ import { Deparser } from 'pgsql-deparser';
12
+ import { hydratePlpgsqlAst, dehydratePlpgsqlAst, deparseFunctionSync, } from 'plpgsql-deparser';
13
+ import { groupCommentsByAnchor } from './body-scanner';
14
+ /**
15
+ * Deparse a PlpgsqlParseResult back to SQL, preserving both outer
16
+ * comments/whitespace AND comments inside PL/pgSQL function bodies.
17
+ */
18
+ export function deparseSync(result, options = {}) {
19
+ const { pretty = true, newline = '\n' } = options;
20
+ const enhanced = result.enhanced;
21
+ const functionMap = new Map();
22
+ for (const fn of result.functions) {
23
+ functionMap.set(fn.stmtIndex, fn);
24
+ }
25
+ const lines = [];
26
+ for (let i = 0; i < enhanced.stmts.length; i++) {
27
+ const entry = enhanced.stmts[i];
28
+ if (isRawComment(entry)) {
29
+ const commentText = `--${entry.RawComment.text}`;
30
+ if (entry.RawComment.trailing && lines.length > 0) {
31
+ lines[lines.length - 1] += ' ' + commentText;
32
+ }
33
+ else {
34
+ lines.push(commentText);
35
+ }
36
+ }
37
+ else if (isRawWhitespace(entry)) {
38
+ for (let b = 0; b < entry.RawWhitespace.lines; b++) {
39
+ lines.push('');
40
+ }
41
+ }
42
+ else if (isRawStmt(entry)) {
43
+ const fnComments = functionMap.get(i);
44
+ if (fnComments) {
45
+ // This is a PL/pgSQL function — deparse with body comments
46
+ const sql = deparseWithBodyComments(entry, fnComments, pretty);
47
+ if (sql)
48
+ lines.push(sql);
49
+ }
50
+ else {
51
+ // Standard SQL statement
52
+ const sql = Deparser.deparse({ version: 0, stmts: [entry] }, { pretty });
53
+ if (sql)
54
+ lines.push(sql);
55
+ }
56
+ }
57
+ }
58
+ return lines.join(newline);
59
+ }
60
+ /**
61
+ * Async version of deparseSync.
62
+ */
63
+ export async function deparse(result, options = {}) {
64
+ return deparseSync(result, options);
65
+ }
66
+ /**
67
+ * Deparse a single PL/pgSQL CREATE FUNCTION statement, re-injecting
68
+ * body comments into the deparsed function body.
69
+ */
70
+ function deparseWithBodyComments(rawStmt, fnComments, pretty) {
71
+ const stmt = rawStmt?.stmt;
72
+ if (!stmt)
73
+ return null;
74
+ const createFunctionStmt = stmt.CreateFunctionStmt;
75
+ if (!createFunctionStmt)
76
+ return null;
77
+ // Parse the PL/pgSQL body to get the AST
78
+ // We need the full CREATE FUNCTION SQL for parsePlPgSQL, but we
79
+ // only have the AST node. So we first deparse without comments
80
+ // to get valid SQL, then parse the PL/pgSQL body.
81
+ try {
82
+ // First, get a clean deparse of the statement to extract the body
83
+ const cleanSql = Deparser.deparse({ version: 0, stmts: [rawStmt] }, { pretty });
84
+ if (!cleanSql)
85
+ return null;
86
+ // Parse PL/pgSQL from the clean SQL
87
+ const plpgsqlRaw = parsePlPgSQLSync(cleanSql);
88
+ const { ast: hydrated } = hydratePlpgsqlAst(plpgsqlRaw);
89
+ const dehydrated = dehydratePlpgsqlAst(hydrated);
90
+ const plpgsqlFunc = dehydrated.plpgsql_funcs?.[0]?.PLpgSQL_function;
91
+ if (!plpgsqlFunc)
92
+ return cleanSql;
93
+ // Get return info for correct RETURN statement handling
94
+ const returnInfo = getReturnInfoForStmt(createFunctionStmt);
95
+ // Deparse the PL/pgSQL body
96
+ const deparsedBody = deparseFunctionSync(plpgsqlFunc, undefined, returnInfo);
97
+ // Re-inject comments into the deparsed body
98
+ const enhancedBody = reinjectBodyComments(fnComments.originalBody, deparsedBody, fnComments.comments, plpgsqlRaw);
99
+ // Stitch the enhanced body back into the AST
100
+ stitchBodyIntoAst(createFunctionStmt, enhancedBody);
101
+ // Deparse the full statement with the enhanced body
102
+ const result = Deparser.deparse({ version: 0, stmts: [rawStmt] }, { pretty });
103
+ return result || null;
104
+ }
105
+ catch {
106
+ // If anything fails, fall back to standard deparse
107
+ return Deparser.deparse({ version: 0, stmts: [rawStmt] }, { pretty }) || null;
108
+ }
109
+ }
110
+ /**
111
+ * Re-inject comments into a deparsed PL/pgSQL function body.
112
+ *
113
+ * Strategy:
114
+ * 1. Collect statement linenos from the PL/pgSQL AST (DFS order)
115
+ * 2. Associate each comment with the next statement by lineno
116
+ * 3. Walk the deparsed output line-by-line, tracking statement indices
117
+ * 4. Insert comments before their associated statement's output
118
+ *
119
+ * Uses sequential keyword matching: the deparser emits statements in
120
+ * the same order as the AST's body arrays, so we match them in order.
121
+ */
122
+ function reinjectBodyComments(originalBody, deparsedBody, comments, plpgsqlAst) {
123
+ if (comments.length === 0)
124
+ return deparsedBody;
125
+ // 1. Collect statement linenos from the AST
126
+ const stmtLinenos = collectStmtLinenos(plpgsqlAst);
127
+ // 2. Group comments by anchor statement
128
+ const groups = groupCommentsByAnchor(comments, stmtLinenos);
129
+ if (groups.length === 0)
130
+ return deparsedBody;
131
+ // 3. Build a map of anchorLineno → comment texts
132
+ const commentsByAnchor = new Map();
133
+ const trailingComments = [];
134
+ for (const group of groups) {
135
+ if (group.anchorLineno !== null) {
136
+ const existing = commentsByAnchor.get(group.anchorLineno) ?? [];
137
+ existing.push(...group.comments);
138
+ commentsByAnchor.set(group.anchorLineno, existing);
139
+ }
140
+ else {
141
+ trailingComments.push(...group.comments);
142
+ }
143
+ }
144
+ // 4. Walk the deparsed body and insert comments at statement boundaries
145
+ const depLines = deparsedBody.split('\n');
146
+ const result = [];
147
+ // Build an ordered list of (lineno, keyword) for matching
148
+ const stmtKeywords = buildStmtKeywords(plpgsqlAst);
149
+ let stmtKeyIdx = 0;
150
+ const usedAnchors = new Set();
151
+ for (const line of depLines) {
152
+ const trimmed = line.trim().toUpperCase();
153
+ // Try to match this line to the next expected statement
154
+ if (stmtKeyIdx < stmtKeywords.length) {
155
+ const { lineno, keywords } = stmtKeywords[stmtKeyIdx];
156
+ if (lineMatchesKeywords(trimmed, keywords)) {
157
+ // Insert comments anchored to this statement
158
+ if (commentsByAnchor.has(lineno) && !usedAnchors.has(lineno)) {
159
+ const indent = line.match(/^\s*/)?.[0] ?? '';
160
+ for (const commentText of commentsByAnchor.get(lineno)) {
161
+ result.push(indent + commentText);
162
+ }
163
+ usedAnchors.add(lineno);
164
+ }
165
+ stmtKeyIdx++;
166
+ }
167
+ }
168
+ result.push(line);
169
+ }
170
+ // Append any trailing comments before the final END
171
+ if (trailingComments.length > 0) {
172
+ // Find the last END line and insert before it
173
+ const lastEndIdx = findLastEndLine(result);
174
+ if (lastEndIdx >= 0) {
175
+ const indent = result[lastEndIdx].match(/^\s*/)?.[0] ?? ' ';
176
+ const commentLines = trailingComments.map(c => indent + c);
177
+ result.splice(lastEndIdx, 0, ...commentLines);
178
+ }
179
+ else {
180
+ for (const c of trailingComments) {
181
+ result.push(' ' + c);
182
+ }
183
+ }
184
+ }
185
+ return result.join('\n');
186
+ }
187
+ /**
188
+ * Collect all statement linenos from a PL/pgSQL parse result in DFS body order.
189
+ */
190
+ function collectStmtLinenos(ast) {
191
+ const linenos = [];
192
+ if (!ast.plpgsql_funcs)
193
+ return linenos;
194
+ for (const funcNode of ast.plpgsql_funcs) {
195
+ if ('PLpgSQL_function' in funcNode) {
196
+ const func = funcNode.PLpgSQL_function;
197
+ if (func.action) {
198
+ collectStmtLinenosFromNode(func.action, linenos);
199
+ }
200
+ }
201
+ }
202
+ return linenos.sort((a, b) => a - b);
203
+ }
204
+ /**
205
+ * Recursively collect linenos from PL/pgSQL statement nodes.
206
+ */
207
+ function collectStmtLinenosFromNode(stmtNode, linenos) {
208
+ const [kind, data] = Object.entries(stmtNode)[0];
209
+ if (data?.lineno !== undefined) {
210
+ linenos.push(data.lineno);
211
+ }
212
+ // Recurse into body arrays
213
+ const bodyKeys = ['body', 'then_body', 'else_body', 'else_stmts', 'action'];
214
+ for (const key of bodyKeys) {
215
+ const arr = data?.[key];
216
+ if (Array.isArray(arr)) {
217
+ for (const child of arr) {
218
+ collectStmtLinenosFromNode(child, linenos);
219
+ }
220
+ }
221
+ }
222
+ // Recurse into elsif_list
223
+ if (data?.elsif_list) {
224
+ for (const elsif of data.elsif_list) {
225
+ const elsifData = elsif?.PLpgSQL_if_elsif;
226
+ if (elsifData?.stmts) {
227
+ for (const child of elsifData.stmts) {
228
+ collectStmtLinenosFromNode(child, linenos);
229
+ }
230
+ }
231
+ }
232
+ }
233
+ // Recurse into case_when_list
234
+ if (data?.case_when_list) {
235
+ for (const when of data.case_when_list) {
236
+ const whenData = when?.PLpgSQL_case_when;
237
+ if (whenData?.stmts) {
238
+ for (const child of whenData.stmts) {
239
+ collectStmtLinenosFromNode(child, linenos);
240
+ }
241
+ }
242
+ }
243
+ }
244
+ // Recurse into exception handlers
245
+ const excList = data?.exceptions?.exc_list ??
246
+ data?.exceptions?.PLpgSQL_exception_block?.exc_list;
247
+ if (excList) {
248
+ for (const exc of excList) {
249
+ const excData = exc?.PLpgSQL_exception;
250
+ if (excData?.action) {
251
+ for (const child of excData.action) {
252
+ collectStmtLinenosFromNode(child, linenos);
253
+ }
254
+ }
255
+ }
256
+ }
257
+ }
258
+ /**
259
+ * Build an ordered list of (lineno, keywords) for matching deparsed output.
260
+ * Keywords are the first tokens expected for each statement type.
261
+ */
262
+ function buildStmtKeywords(ast) {
263
+ const result = [];
264
+ if (!ast.plpgsql_funcs)
265
+ return result;
266
+ for (const funcNode of ast.plpgsql_funcs) {
267
+ if ('PLpgSQL_function' in funcNode) {
268
+ const func = funcNode.PLpgSQL_function;
269
+ if (func.action) {
270
+ collectStmtKeywordsFromBlock(func.action, result);
271
+ }
272
+ }
273
+ }
274
+ // Sort by lineno to match comment association order
275
+ result.sort((a, b) => a.lineno - b.lineno);
276
+ return result;
277
+ }
278
+ /**
279
+ * Recursively collect statement keywords from a block's body.
280
+ * Only collects from direct body statements, not nested blocks' bodies
281
+ * (those are handled when their enclosing block keyword is matched).
282
+ */
283
+ function collectStmtKeywordsFromBlock(blockNode, result) {
284
+ const block = blockNode?.PLpgSQL_stmt_block;
285
+ if (!block?.body)
286
+ return;
287
+ for (const stmt of block.body) {
288
+ const [kind, data] = Object.entries(stmt)[0];
289
+ if (data?.lineno === undefined)
290
+ continue;
291
+ const keywords = getKeywordsForStmtKind(kind, data);
292
+ result.push({ lineno: data.lineno, keywords });
293
+ // Recurse into nested structures to collect their direct children
294
+ if (kind === 'PLpgSQL_stmt_block') {
295
+ collectStmtKeywordsFromBlock(stmt, result);
296
+ }
297
+ else if (kind === 'PLpgSQL_stmt_if') {
298
+ collectStmtKeywordsFromIf(data, result);
299
+ }
300
+ else if (kind === 'PLpgSQL_stmt_case') {
301
+ collectStmtKeywordsFromCase(data, result);
302
+ }
303
+ else if (kind === 'PLpgSQL_stmt_loop' || kind === 'PLpgSQL_stmt_while' ||
304
+ kind === 'PLpgSQL_stmt_fori' || kind === 'PLpgSQL_stmt_fors' ||
305
+ kind === 'PLpgSQL_stmt_forc' || kind === 'PLpgSQL_stmt_foreach_a' ||
306
+ kind === 'PLpgSQL_stmt_dynfors') {
307
+ if (data.body) {
308
+ for (const child of data.body) {
309
+ const [childKind, childData] = Object.entries(child)[0];
310
+ if (childData?.lineno !== undefined) {
311
+ result.push({ lineno: childData.lineno, keywords: getKeywordsForStmtKind(childKind, childData) });
312
+ }
313
+ if (childKind === 'PLpgSQL_stmt_block') {
314
+ collectStmtKeywordsFromBlock(child, result);
315
+ }
316
+ }
317
+ }
318
+ }
319
+ // Recurse into exception handlers
320
+ if (kind === 'PLpgSQL_stmt_block') {
321
+ const excList = data?.exceptions?.exc_list ??
322
+ data?.exceptions?.PLpgSQL_exception_block?.exc_list;
323
+ if (excList) {
324
+ for (const exc of excList) {
325
+ const excData = exc?.PLpgSQL_exception;
326
+ if (excData?.action) {
327
+ for (const child of excData.action) {
328
+ const [childKind, childData] = Object.entries(child)[0];
329
+ if (childData?.lineno !== undefined) {
330
+ result.push({ lineno: childData.lineno, keywords: getKeywordsForStmtKind(childKind, childData) });
331
+ }
332
+ }
333
+ }
334
+ }
335
+ }
336
+ }
337
+ }
338
+ }
339
+ function collectStmtKeywordsFromIf(ifData, result) {
340
+ // then_body
341
+ if (ifData.then_body) {
342
+ for (const child of ifData.then_body) {
343
+ const [kind, data] = Object.entries(child)[0];
344
+ if (data?.lineno !== undefined) {
345
+ result.push({ lineno: data.lineno, keywords: getKeywordsForStmtKind(kind, data) });
346
+ }
347
+ if (kind === 'PLpgSQL_stmt_block') {
348
+ collectStmtKeywordsFromBlock(child, result);
349
+ }
350
+ }
351
+ }
352
+ // elsif_list
353
+ if (ifData.elsif_list) {
354
+ for (const elsif of ifData.elsif_list) {
355
+ const elsifData = elsif?.PLpgSQL_if_elsif;
356
+ if (elsifData?.stmts) {
357
+ for (const child of elsifData.stmts) {
358
+ const [kind, data] = Object.entries(child)[0];
359
+ if (data?.lineno !== undefined) {
360
+ result.push({ lineno: data.lineno, keywords: getKeywordsForStmtKind(kind, data) });
361
+ }
362
+ if (kind === 'PLpgSQL_stmt_block') {
363
+ collectStmtKeywordsFromBlock(child, result);
364
+ }
365
+ }
366
+ }
367
+ }
368
+ }
369
+ // else_body
370
+ if (ifData.else_body) {
371
+ for (const child of ifData.else_body) {
372
+ const [kind, data] = Object.entries(child)[0];
373
+ if (data?.lineno !== undefined) {
374
+ result.push({ lineno: data.lineno, keywords: getKeywordsForStmtKind(kind, data) });
375
+ }
376
+ if (kind === 'PLpgSQL_stmt_block') {
377
+ collectStmtKeywordsFromBlock(child, result);
378
+ }
379
+ }
380
+ }
381
+ }
382
+ function collectStmtKeywordsFromCase(caseData, result) {
383
+ if (caseData.case_when_list) {
384
+ for (const when of caseData.case_when_list) {
385
+ const whenData = when?.PLpgSQL_case_when;
386
+ if (whenData?.stmts) {
387
+ for (const child of whenData.stmts) {
388
+ const [kind, data] = Object.entries(child)[0];
389
+ if (data?.lineno !== undefined) {
390
+ result.push({ lineno: data.lineno, keywords: getKeywordsForStmtKind(kind, data) });
391
+ }
392
+ if (kind === 'PLpgSQL_stmt_block') {
393
+ collectStmtKeywordsFromBlock(child, result);
394
+ }
395
+ }
396
+ }
397
+ }
398
+ }
399
+ if (caseData.have_else && caseData.else_stmts) {
400
+ for (const child of caseData.else_stmts) {
401
+ const [kind, data] = Object.entries(child)[0];
402
+ if (data?.lineno !== undefined) {
403
+ result.push({ lineno: data.lineno, keywords: getKeywordsForStmtKind(kind, data) });
404
+ }
405
+ if (kind === 'PLpgSQL_stmt_block') {
406
+ collectStmtKeywordsFromBlock(child, result);
407
+ }
408
+ }
409
+ }
410
+ }
411
+ /**
412
+ * Map PL/pgSQL statement kind to the keywords that would start
413
+ * its deparsed output line (uppercase).
414
+ */
415
+ function getKeywordsForStmtKind(kind, data) {
416
+ const map = {
417
+ PLpgSQL_stmt_block: ['BEGIN', '<<'],
418
+ PLpgSQL_stmt_assign: [], // starts with variable name — handled separately
419
+ PLpgSQL_stmt_if: ['IF'],
420
+ PLpgSQL_stmt_case: ['CASE'],
421
+ PLpgSQL_stmt_loop: ['LOOP'],
422
+ PLpgSQL_stmt_while: ['WHILE'],
423
+ PLpgSQL_stmt_fori: ['FOR'],
424
+ PLpgSQL_stmt_fors: ['FOR'],
425
+ PLpgSQL_stmt_forc: ['FOR'],
426
+ PLpgSQL_stmt_foreach_a: ['FOREACH'],
427
+ PLpgSQL_stmt_exit: ['EXIT', 'CONTINUE'],
428
+ PLpgSQL_stmt_return: ['RETURN'],
429
+ PLpgSQL_stmt_return_next: ['RETURN'],
430
+ PLpgSQL_stmt_return_query: ['RETURN'],
431
+ PLpgSQL_stmt_raise: ['RAISE'],
432
+ PLpgSQL_stmt_assert: ['ASSERT'],
433
+ PLpgSQL_stmt_execsql: [], // SQL statement — handled separately
434
+ PLpgSQL_stmt_dynexecute: ['EXECUTE'],
435
+ PLpgSQL_stmt_dynfors: ['FOR'],
436
+ PLpgSQL_stmt_getdiag: ['GET'],
437
+ PLpgSQL_stmt_open: ['OPEN'],
438
+ PLpgSQL_stmt_fetch: ['FETCH'],
439
+ PLpgSQL_stmt_close: ['CLOSE'],
440
+ PLpgSQL_stmt_perform: ['PERFORM'],
441
+ PLpgSQL_stmt_call: ['CALL'],
442
+ PLpgSQL_stmt_commit: ['COMMIT'],
443
+ PLpgSQL_stmt_rollback: ['ROLLBACK'],
444
+ PLpgSQL_stmt_set: ['SET'],
445
+ };
446
+ const keywords = map[kind];
447
+ if (keywords && keywords.length > 0)
448
+ return keywords;
449
+ // For assignment and execsql, try to extract the first word from the expr
450
+ if (kind === 'PLpgSQL_stmt_assign') {
451
+ // Assignment: extract from the expression query string
452
+ const query = data?.expr?.PLpgSQL_expr?.query;
453
+ if (query) {
454
+ const firstWord = query.trim().split(/[\s:=]+/)[0].toUpperCase();
455
+ if (firstWord)
456
+ return [firstWord];
457
+ }
458
+ }
459
+ if (kind === 'PLpgSQL_stmt_execsql') {
460
+ // execsql: the first word of the SQL query
461
+ const query = data?.sqlstmt?.PLpgSQL_expr?.query;
462
+ if (query) {
463
+ const firstWord = query.trim().split(/\s+/)[0].toUpperCase();
464
+ if (firstWord)
465
+ return [firstWord];
466
+ }
467
+ }
468
+ return [];
469
+ }
470
+ /**
471
+ * Check if a deparsed line starts with any of the given keywords.
472
+ */
473
+ function lineMatchesKeywords(trimmedUpperLine, keywords) {
474
+ if (keywords.length === 0) {
475
+ // No keywords to match — match any non-structural line
476
+ // (not BEGIN, END, DECLARE, EXCEPTION, WHEN, ELSE, ELSIF)
477
+ const structural = ['BEGIN', 'END', 'DECLARE', 'EXCEPTION', 'WHEN', 'ELSE', 'ELSIF', 'END IF', 'END LOOP', 'END CASE'];
478
+ return !structural.some(s => trimmedUpperLine.startsWith(s)) && trimmedUpperLine.length > 0;
479
+ }
480
+ for (const kw of keywords) {
481
+ if (trimmedUpperLine.startsWith(kw)) {
482
+ // Make sure it's a word boundary (followed by space, semicolon, or end)
483
+ const nextChar = trimmedUpperLine[kw.length];
484
+ if (nextChar === undefined || nextChar === ' ' || nextChar === ';' || nextChar === '\t' || nextChar === '>' || nextChar === '\n') {
485
+ return true;
486
+ }
487
+ }
488
+ }
489
+ return false;
490
+ }
491
+ /**
492
+ * Find the index of the last END line in the result array.
493
+ */
494
+ function findLastEndLine(lines) {
495
+ for (let i = lines.length - 1; i >= 0; i--) {
496
+ if (lines[i].trim().toUpperCase().startsWith('END')) {
497
+ return i;
498
+ }
499
+ }
500
+ return -1;
501
+ }
502
+ /**
503
+ * Replace the function body in a CREATE FUNCTION AST node.
504
+ */
505
+ function stitchBodyIntoAst(createFunctionStmt, newBody) {
506
+ if (!createFunctionStmt?.options)
507
+ return;
508
+ for (const opt of createFunctionStmt.options) {
509
+ if (opt?.DefElem?.defname === 'as') {
510
+ const arg = opt.DefElem.arg;
511
+ if (arg?.List?.items?.[0]?.String) {
512
+ arg.List.items[0].String.sval = newBody;
513
+ return;
514
+ }
515
+ if (arg?.String) {
516
+ arg.String.sval = newBody;
517
+ return;
518
+ }
519
+ }
520
+ }
521
+ }
522
+ /**
523
+ * Extract return info from a CREATE FUNCTION statement for correct
524
+ * RETURN statement handling in the PL/pgSQL deparser.
525
+ */
526
+ function getReturnInfoForStmt(createFunctionStmt) {
527
+ // Check for RETURNS TABLE / SETOF
528
+ const returnType = createFunctionStmt?.returnType;
529
+ if (returnType) {
530
+ // SETOF
531
+ if (returnType.TypeName?.setof) {
532
+ return { kind: 'setof' };
533
+ }
534
+ // RETURNS TABLE
535
+ if (returnType.TypeName?.names) {
536
+ const names = returnType.TypeName.names;
537
+ for (const n of names) {
538
+ if (n?.String?.sval === 'record') {
539
+ return { kind: 'setof' };
540
+ }
541
+ }
542
+ }
543
+ }
544
+ // Check for OUT parameters
545
+ const params = createFunctionStmt?.parameters;
546
+ if (params) {
547
+ for (const p of params) {
548
+ const fp = p?.FunctionParameter;
549
+ if (fp?.mode === 'FUNC_PARAM_OUT' || fp?.mode === 'FUNC_PARAM_INOUT' || fp?.mode === 'FUNC_PARAM_TABLE') {
550
+ return { kind: 'out_params' };
551
+ }
552
+ }
553
+ }
554
+ // Check for RETURNS void
555
+ if (returnType?.TypeName?.names) {
556
+ const names = returnType.TypeName.names;
557
+ for (const n of names) {
558
+ if (n?.String?.sval === 'void') {
559
+ return { kind: 'void' };
560
+ }
561
+ }
562
+ }
563
+ // Check for RETURNS trigger
564
+ if (returnType?.TypeName?.names) {
565
+ const names = returnType.TypeName.names;
566
+ for (const n of names) {
567
+ if (n?.String?.sval === 'trigger') {
568
+ return { kind: 'trigger' };
569
+ }
570
+ }
571
+ }
572
+ return { kind: 'scalar' };
573
+ }
package/esm/index.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * plpgsql-parse — Comment and whitespace preserving PL/pgSQL parser.
3
+ *
4
+ * Drop-in enhancement over plpgsql-parser that preserves SQL -- line
5
+ * comments and vertical whitespace (blank lines) through parse→deparse
6
+ * round trips, both at the outer SQL level (between statements) and
7
+ * inside PL/pgSQL function bodies.
8
+ *
9
+ * This package wraps plpgsql-parser, plpgsql-deparser, and pgsql-parse
10
+ * without modifying any of them.
11
+ */
12
+ // Enhanced parse functions (comment/whitespace preserving)
13
+ export { parse, parseSync, loadModule } from './parse';
14
+ // Enhanced deparse functions
15
+ export { deparse, deparseSync } from './deparse';
16
+ // Body scanner (for advanced use)
17
+ export { scanBodyComments, groupCommentsByAnchor } from './body-scanner';
18
+ export { isRawComment, isRawWhitespace, isRawStmt } from 'pgsql-parse';