From 290d8a7b577c1a1b93f95011133e327f583e052a Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Wed, 31 Dec 2025 23:07:03 +0000 Subject: [PATCH 1/3] feat: add plpgsql-parser package for combined SQL + PL/pgSQL parsing - Combined parse function that auto-detects PL/pgSQL functions and hydrates them - Transform API for parse -> modify -> deparse pipeline - Deparse function that handles dehydration and stitching - Re-exports underlying primitives for power users - 7 tests passing --- .../__tests__/plpgsql-parser.test.ts | 114 +++++++++++++++ packages/plpgsql-parser/jest.config.js | 18 +++ packages/plpgsql-parser/package.json | 54 +++++++ packages/plpgsql-parser/src/deparse.ts | 97 +++++++++++++ packages/plpgsql-parser/src/index.ts | 19 +++ packages/plpgsql-parser/src/parse.ts | 135 ++++++++++++++++++ packages/plpgsql-parser/src/transform.ts | 88 ++++++++++++ packages/plpgsql-parser/src/types.ts | 68 +++++++++ packages/plpgsql-parser/tsconfig.esm.json | 9 ++ packages/plpgsql-parser/tsconfig.json | 9 ++ pnpm-lock.yaml | 20 +++ 11 files changed, 631 insertions(+) create mode 100644 packages/plpgsql-parser/__tests__/plpgsql-parser.test.ts create mode 100644 packages/plpgsql-parser/jest.config.js create mode 100644 packages/plpgsql-parser/package.json create mode 100644 packages/plpgsql-parser/src/deparse.ts create mode 100644 packages/plpgsql-parser/src/index.ts create mode 100644 packages/plpgsql-parser/src/parse.ts create mode 100644 packages/plpgsql-parser/src/transform.ts create mode 100644 packages/plpgsql-parser/src/types.ts create mode 100644 packages/plpgsql-parser/tsconfig.esm.json create mode 100644 packages/plpgsql-parser/tsconfig.json diff --git a/packages/plpgsql-parser/__tests__/plpgsql-parser.test.ts b/packages/plpgsql-parser/__tests__/plpgsql-parser.test.ts new file mode 100644 index 00000000..18c46a64 --- /dev/null +++ b/packages/plpgsql-parser/__tests__/plpgsql-parser.test.ts @@ -0,0 +1,114 @@ +import { parse, transformSync, deparseSync, loadModule } from '../src'; + +beforeAll(async () => { + await loadModule(); +}); + +const simpleFunctionSql = ` +CREATE OR REPLACE FUNCTION test_func(p_id int) +RETURNS void +LANGUAGE plpgsql +AS $$ +DECLARE + v_count int; +BEGIN + v_count := 0; + RAISE NOTICE 'Count: %', v_count; +END; +$$; +`; + +const multiStatementSql = ` +CREATE TABLE users (id int); + +CREATE OR REPLACE FUNCTION get_user(p_id int) +RETURNS int +LANGUAGE plpgsql +AS $$ +BEGIN + RETURN p_id; +END; +$$; + +CREATE INDEX idx_users_id ON users(id); +`; + +describe('plpgsql-parser', () => { + describe('parse', () => { + it('should parse a simple PL/pgSQL function', () => { + const result = parse(simpleFunctionSql); + + expect(result.sql).toBeDefined(); + expect(result.sql.stmts).toHaveLength(1); + expect(result.items).toHaveLength(1); + expect(result.functions).toHaveLength(1); + + const fn = result.functions[0]; + expect(fn.kind).toBe('plpgsql-function'); + expect(fn.language).toBe('plpgsql'); + expect(fn.body.raw).toContain('v_count'); + expect(fn.plpgsql.hydrated).toBeDefined(); + expect(fn.plpgsql.stats.totalExpressions).toBeGreaterThan(0); + }); + + it('should parse multi-statement SQL with mixed content', () => { + const result = parse(multiStatementSql); + + expect(result.sql.stmts).toHaveLength(3); + expect(result.items).toHaveLength(3); + expect(result.functions).toHaveLength(1); + + expect(result.items[0].kind).toBe('stmt'); + expect(result.items[1].kind).toBe('plpgsql-function'); + expect(result.items[2].kind).toBe('stmt'); + }); + + it('should skip hydration when hydrate=false', () => { + const result = parse(simpleFunctionSql, { hydrate: false }); + + expect(result.functions).toHaveLength(0); + expect(result.items).toHaveLength(1); + expect(result.items[0].kind).toBe('stmt'); + }); + }); + + describe('transformSync', () => { + it('should transform function using callback', () => { + const result = transformSync(simpleFunctionSql, (ctx) => { + const fn = ctx.functions[0]; + fn.stmt.funcname[0].String.sval = 'renamed_func'; + }); + + expect(result).toContain('renamed_func'); + }); + + it('should transform function using visitor', () => { + const result = transformSync(simpleFunctionSql, { + onFunction: (fn) => { + fn.stmt.funcname[0].String.sval = 'visitor_renamed'; + } + }); + + expect(result).toContain('visitor_renamed'); + }); + }); + + describe('deparseSync', () => { + it('should deparse parsed script back to SQL', () => { + const parsed = parse(simpleFunctionSql); + const result = deparseSync(parsed); + + expect(result).toContain('CREATE'); + expect(result).toContain('FUNCTION'); + expect(result).toContain('test_func'); + expect(result).toContain('plpgsql'); + }); + + it('should support pretty printing', () => { + const parsed = parse(simpleFunctionSql); + const result = deparseSync(parsed, { pretty: true }); + + expect(result).toContain('\n'); + }); + }); +}); diff --git a/packages/plpgsql-parser/jest.config.js b/packages/plpgsql-parser/jest.config.js new file mode 100644 index 00000000..0aa3aaa4 --- /dev/null +++ b/packages/plpgsql-parser/jest.config.js @@ -0,0 +1,18 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: "ts-jest", + testEnvironment: "node", + transform: { + "^.+\\.tsx?$": [ + "ts-jest", + { + babelConfig: false, + tsconfig: "tsconfig.json", + }, + ], + }, + transformIgnorePatterns: [`/node_modules/*`], + testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], + modulePathIgnorePatterns: ["dist/*"] +}; diff --git a/packages/plpgsql-parser/package.json b/packages/plpgsql-parser/package.json new file mode 100644 index 00000000..73816b9d --- /dev/null +++ b/packages/plpgsql-parser/package.json @@ -0,0 +1,54 @@ +{ + "name": "plpgsql-parser", + "version": "0.1.0", + "author": "Constructive ", + "description": "Combined SQL + PL/pgSQL parser with hydrated ASTs and transform API", + "main": "index.js", + "module": "esm/index.js", + "types": "index.d.ts", + "homepage": "https://github.com/constructive-io/pgsql-parser", + "license": "MIT", + "publishConfig": { + "access": "public", + "directory": "dist" + }, + "repository": { + "type": "git", + "url": "https://github.com/constructive-io/pgsql-parser" + }, + "bugs": { + "url": "https://github.com/constructive-io/pgsql-parser/issues" + }, + "scripts": { + "copy": "makage assets", + "clean": "makage clean dist", + "prepublishOnly": "npm run build", + "build": "npm run clean && tsc && tsc -p tsconfig.esm.json && npm run copy", + "build:dev": "npm run clean && tsc --declarationMap && tsc -p tsconfig.esm.json && npm run copy", + "lint": "eslint . --fix", + "test": "jest", + "test:watch": "jest --watch" + }, + "keywords": [ + "sql", + "postgres", + "postgresql", + "pg", + "plpgsql", + "query", + "ast", + "parser", + "deparser", + "transform", + "database" + ], + "devDependencies": { + "makage": "^0.1.8" + }, + "dependencies": { + "@libpg-query/parser": "^17.6.3", + "@pgsql/types": "^17.6.2", + "pgsql-deparser": "workspace:*", + "plpgsql-deparser": "workspace:*" + } +} diff --git a/packages/plpgsql-parser/src/deparse.ts b/packages/plpgsql-parser/src/deparse.ts new file mode 100644 index 00000000..49a69656 --- /dev/null +++ b/packages/plpgsql-parser/src/deparse.ts @@ -0,0 +1,97 @@ +import { deparse as deparseSql } from 'pgsql-deparser'; +import { + dehydratePlpgsqlAst, + deparseSync as deparsePlpgsql +} from 'plpgsql-deparser'; +import type { + ParsedScript, + TransformContext, + DeparseOptions, + ParsedFunction +} from './types'; + +function stitchBodyIntoSqlAst( + sqlAst: any, + fn: ParsedFunction, + newBody: string +): void { + const stmts = sqlAst.stmts; + if (!stmts || !stmts[fn.stmtIndex]) return; + + const rawStmt = stmts[fn.stmtIndex]; + const createFunctionStmt = rawStmt?.stmt?.CreateFunctionStmt; + if (!createFunctionStmt?.options) return; + + for (const opt of createFunctionStmt.options) { + if (opt?.DefElem?.defname === 'as') { + const arg = opt.DefElem.arg; + if (arg?.List?.items?.[0]?.String) { + arg.List.items[0].String.sval = newBody; + return; + } + if (arg?.String) { + arg.String.sval = newBody; + return; + } + } + } +} + +export async function deparse( + input: ParsedScript | TransformContext, + options: DeparseOptions = {} +): Promise { + const { pretty = true } = options; + + const sqlAst = input.sql; + const functions = input.functions; + + for (const fn of functions) { + const dehydrated = dehydratePlpgsqlAst(fn.plpgsql.hydrated); + const newBody = deparsePlpgsql(dehydrated); + stitchBodyIntoSqlAst(sqlAst, fn, newBody); + } + + if (sqlAst.stmts && sqlAst.stmts.length > 0) { + const results: string[] = []; + for (const rawStmt of sqlAst.stmts) { + if (rawStmt?.stmt) { + const deparsed = await deparseSql(rawStmt.stmt, { pretty }); + results.push(deparsed); + } + } + return results.join(';\n\n') + (results.length > 0 ? ';' : ''); + } + + return ''; +} + +export function deparseSync( + input: ParsedScript | TransformContext, + options: DeparseOptions = {} +): string { + const { pretty = true } = options; + + const sqlAst = input.sql; + const functions = input.functions; + + for (const fn of functions) { + const dehydrated = dehydratePlpgsqlAst(fn.plpgsql.hydrated); + const newBody = deparsePlpgsql(dehydrated); + stitchBodyIntoSqlAst(sqlAst, fn, newBody); + } + + if (sqlAst.stmts && sqlAst.stmts.length > 0) { + const results: string[] = []; + for (const rawStmt of sqlAst.stmts) { + if (rawStmt?.stmt) { + const { Deparser } = require('pgsql-deparser'); + const deparsed = Deparser.deparse(rawStmt.stmt, { pretty }); + results.push(deparsed); + } + } + return results.join(';\n\n') + (results.length > 0 ? ';' : ''); + } + + return ''; +} diff --git a/packages/plpgsql-parser/src/index.ts b/packages/plpgsql-parser/src/index.ts new file mode 100644 index 00000000..f103d2f3 --- /dev/null +++ b/packages/plpgsql-parser/src/index.ts @@ -0,0 +1,19 @@ +export * from './types'; +export { parse, parseSync, loadModule } from './parse'; +export { deparse, deparseSync } from './deparse'; +export { transform, transformSync } from './transform'; + +export { + hydratePlpgsqlAst, + dehydratePlpgsqlAst, + deparseSync as deparsePlpgsqlBody, + isHydratedExpr, + getOriginalQuery +} from 'plpgsql-deparser'; + +export { deparse as deparseSql, Deparser } from 'pgsql-deparser'; + +export { + parseSync as parseSql, + parsePlPgSQLSync as parsePlpgsqlBody +} from '@libpg-query/parser'; diff --git a/packages/plpgsql-parser/src/parse.ts b/packages/plpgsql-parser/src/parse.ts new file mode 100644 index 00000000..22f4f67b --- /dev/null +++ b/packages/plpgsql-parser/src/parse.ts @@ -0,0 +1,135 @@ +import { + parseSync as parseSqlSync, + parsePlPgSQLSync, + loadModule +} from '@libpg-query/parser'; +import type { ParseResult } from '@libpg-query/parser'; +import { + hydratePlpgsqlAst, + PLpgSQLParseResult +} from 'plpgsql-deparser'; +import type { + ParsedScript, + ParsedFunction, + ParsedStatement, + ParsedItem, + ParseOptions +} from './types'; + +export { loadModule }; + +function getLanguageFromOptions(options: any[]): string | null { + if (!options) return null; + for (const opt of options) { + if (opt?.DefElem?.defname === 'language') { + const arg = opt.DefElem.arg; + if (arg?.String?.sval) { + return arg.String.sval.toLowerCase(); + } + } + } + return null; +} + +function getBodyFromOptions(options: any[]): { raw: string; delimiter: string } | null { + if (!options) return null; + for (const opt of options) { + if (opt?.DefElem?.defname === 'as') { + const arg = opt.DefElem.arg; + if (arg?.List?.items?.[0]?.String?.sval) { + return { + raw: arg.List.items[0].String.sval, + delimiter: '$$' + }; + } + if (arg?.String?.sval) { + return { + raw: arg.String.sval, + delimiter: '$$' + }; + } + } + } + return null; +} + +function isPlpgsqlFunction(stmt: any): boolean { + const createFunctionStmt = stmt?.CreateFunctionStmt; + if (!createFunctionStmt) return false; + + const language = getLanguageFromOptions(createFunctionStmt.options); + return language === 'plpgsql'; +} + +function extractFunctionInfo(stmt: any, stmtIndex: number, fullSql: string): ParsedFunction | null { + const createFunctionStmt = stmt?.CreateFunctionStmt; + if (!createFunctionStmt) return null; + + const language = getLanguageFromOptions(createFunctionStmt.options); + if (language !== 'plpgsql') return null; + + const body = getBodyFromOptions(createFunctionStmt.options); + if (!body) return null; + + try { + const plpgsqlRaw = parsePlPgSQLSync(fullSql) as unknown as PLpgSQLParseResult; + const { ast: hydrated, stats, errors } = hydratePlpgsqlAst(plpgsqlRaw); + + return { + kind: 'plpgsql-function', + stmt: createFunctionStmt, + stmtIndex, + language: language || 'plpgsql', + body, + plpgsql: { + raw: plpgsqlRaw, + hydrated, + stats, + errors + } + }; + } catch (err) { + return null; + } +} + +export function parse(sql: string, options: ParseOptions = {}): ParsedScript { + const { hydrate = true } = options; + + const sqlResult: ParseResult = parseSqlSync(sql); + const items: ParsedItem[] = []; + const functions: ParsedFunction[] = []; + + if (sqlResult.stmts) { + for (let i = 0; i < sqlResult.stmts.length; i++) { + const rawStmt = sqlResult.stmts[i]; + const stmt = rawStmt?.stmt; + + if (stmt && isPlpgsqlFunction(stmt) && hydrate) { + const fnInfo = extractFunctionInfo(stmt, i, sql); + if (fnInfo) { + items.push(fnInfo); + functions.push(fnInfo); + continue; + } + } + + const stmtItem: ParsedStatement = { + kind: 'stmt', + stmt, + stmtIndex: i + }; + items.push(stmtItem); + } + } + + return { + sql: sqlResult, + items, + functions + }; +} + +export function parseSync(sql: string, options: ParseOptions = {}): ParsedScript { + return parse(sql, options); +} diff --git a/packages/plpgsql-parser/src/transform.ts b/packages/plpgsql-parser/src/transform.ts new file mode 100644 index 00000000..20e0c2c6 --- /dev/null +++ b/packages/plpgsql-parser/src/transform.ts @@ -0,0 +1,88 @@ +import { parse } from './parse'; +import { deparse, deparseSync } from './deparse'; +import type { + TransformOptions, + TransformContext, + TransformInput, + TransformCallback, + TransformVisitors, + ParsedFunction, + ParsedStatement +} from './types'; + +function isCallback(input: TransformInput): input is TransformCallback { + return typeof input === 'function'; +} + +function isVisitors(input: TransformInput): input is TransformVisitors { + return typeof input === 'object' && input !== null; +} + +export async function transform( + sql: string, + input: TransformInput, + options: TransformOptions = {} +): Promise { + const { hydrate = true, pretty = true } = options; + + const parsed = parse(sql, { hydrate }); + + const ctx: TransformContext = { + sql: parsed.sql, + items: parsed.items, + functions: parsed.functions + }; + + if (isCallback(input)) { + await input(ctx); + } else if (isVisitors(input)) { + for (const item of ctx.items) { + if (item.kind === 'plpgsql-function' && input.onFunction) { + await input.onFunction(item as ParsedFunction, ctx); + } else if (item.kind === 'stmt' && input.onStatement) { + await input.onStatement(item as ParsedStatement, ctx); + } + } + } + + return deparse(ctx, { pretty }); +} + +export function transformSync( + sql: string, + input: TransformInput, + options: TransformOptions = {} +): string { + const { hydrate = true, pretty = true } = options; + + const parsed = parse(sql, { hydrate }); + + const ctx: TransformContext = { + sql: parsed.sql, + items: parsed.items, + functions: parsed.functions + }; + + if (isCallback(input)) { + const result = input(ctx); + if (result instanceof Promise) { + throw new Error('transformSync does not support async callbacks. Use transform() instead.'); + } + } else if (isVisitors(input)) { + for (const item of ctx.items) { + if (item.kind === 'plpgsql-function' && input.onFunction) { + const result = input.onFunction(item as ParsedFunction, ctx); + if (result instanceof Promise) { + throw new Error('transformSync does not support async visitors. Use transform() instead.'); + } + } else if (item.kind === 'stmt' && input.onStatement) { + const result = input.onStatement(item as ParsedStatement, ctx); + if (result instanceof Promise) { + throw new Error('transformSync does not support async visitors. Use transform() instead.'); + } + } + } + } + + return deparseSync(ctx, { pretty }); +} diff --git a/packages/plpgsql-parser/src/types.ts b/packages/plpgsql-parser/src/types.ts new file mode 100644 index 00000000..0ab4ab66 --- /dev/null +++ b/packages/plpgsql-parser/src/types.ts @@ -0,0 +1,68 @@ +import type { ParseResult } from '@libpg-query/parser'; +import type { + PLpgSQLParseResult, + HydrationStats, + HydrationError +} from 'plpgsql-deparser'; + +export interface PlpgsqlFunctionBody { + raw: string; + delimiter: string; +} + +export interface PlpgsqlFunctionData { + raw: PLpgSQLParseResult; + hydrated: any; + stats: HydrationStats; + errors: HydrationError[]; +} + +export interface ParsedFunction { + kind: 'plpgsql-function'; + stmt: any; + stmtIndex: number; + language: string; + body: PlpgsqlFunctionBody; + plpgsql: PlpgsqlFunctionData; +} + +export interface ParsedStatement { + kind: 'stmt'; + stmt: any; + stmtIndex: number; +} + +export type ParsedItem = ParsedFunction | ParsedStatement; + +export interface ParsedScript { + sql: ParseResult; + items: ParsedItem[]; + functions: ParsedFunction[]; +} + +export interface ParseOptions { + hydrate?: boolean; +} + +export interface DeparseOptions { + pretty?: boolean; +} + +export interface TransformOptions extends DeparseOptions { + hydrate?: boolean; +} + +export interface TransformContext { + sql: ParseResult; + items: ParsedItem[]; + functions: ParsedFunction[]; +} + +export type TransformCallback = (ctx: TransformContext) => void | Promise; + +export interface TransformVisitors { + onFunction?: (fn: ParsedFunction, ctx: TransformContext) => void | Promise; + onStatement?: (stmt: ParsedStatement, ctx: TransformContext) => void | Promise; +} + +export type TransformInput = TransformCallback | TransformVisitors; diff --git a/packages/plpgsql-parser/tsconfig.esm.json b/packages/plpgsql-parser/tsconfig.esm.json new file mode 100644 index 00000000..800d7506 --- /dev/null +++ b/packages/plpgsql-parser/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "dist/esm", + "module": "es2022", + "rootDir": "src/", + "declaration": false + } +} diff --git a/packages/plpgsql-parser/tsconfig.json b/packages/plpgsql-parser/tsconfig.json new file mode 100644 index 00000000..1a9d5696 --- /dev/null +++ b/packages/plpgsql-parser/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src/" + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules", "**/*.spec.*", "**/*.test.*"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72caf6bb..63ee944c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -183,6 +183,26 @@ importers: version: 0.1.8 publishDirectory: dist + packages/plpgsql-parser: + dependencies: + '@libpg-query/parser': + specifier: ^17.6.3 + version: 17.6.3 + '@pgsql/types': + specifier: ^17.6.2 + version: 17.6.2 + pgsql-deparser: + specifier: workspace:* + version: link:../deparser/dist + plpgsql-deparser: + specifier: workspace:* + version: link:../plpgsql-deparser/dist + devDependencies: + makage: + specifier: ^0.1.8 + version: 0.1.8 + publishDirectory: dist + packages/proto-parser: dependencies: '@babel/generator': From 1970b6b76ac1817002d003b8ccef112f09abaa75 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Wed, 31 Dec 2025 23:09:03 +0000 Subject: [PATCH 2/3] docs(plpgsql-parser): add README.md --- packages/plpgsql-parser/README.md | 82 +++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 packages/plpgsql-parser/README.md diff --git a/packages/plpgsql-parser/README.md b/packages/plpgsql-parser/README.md new file mode 100644 index 00000000..2a6e4f2a --- /dev/null +++ b/packages/plpgsql-parser/README.md @@ -0,0 +1,82 @@ +# plpgsql-parser + +Combined SQL + PL/pgSQL parser with hydrated ASTs and transform API. + +## Installation + +```bash +npm install plpgsql-parser +``` + +## Usage + +```typescript +import { parse, transform, deparseSync, loadModule } from 'plpgsql-parser'; + +// Initialize the WASM module +await loadModule(); + +// Parse SQL with PL/pgSQL functions - auto-detects and hydrates +const result = parse(` + CREATE FUNCTION my_func(p_id int) + RETURNS void + LANGUAGE plpgsql + AS $$ + BEGIN + RAISE NOTICE 'Hello %', p_id; + END; + $$; +`); + +console.log(result.functions.length); // 1 +console.log(result.functions[0].plpgsql.hydrated); // Hydrated AST + +// Transform API for parse -> modify -> deparse pipeline +const output = transformSync(sql, (ctx) => { + // Modify the function name + ctx.functions[0].stmt.funcname[0].String.sval = 'renamed_func'; +}); + +// Deparse back to SQL +const sql = deparseSync(result, { pretty: true }); +``` + +## API + +### `parse(sql, options?)` + +Parses SQL and auto-detects PL/pgSQL functions, hydrating their bodies. + +Options: +- `hydrate` (default: `true`) - Whether to hydrate PL/pgSQL function bodies + +Returns a `ParsedScript` with: +- `sql` - The raw SQL parse result +- `items` - Array of parsed items (statements and functions) +- `functions` - Array of detected PL/pgSQL functions with hydrated ASTs + +### `transform(sql, callback, options?)` + +Async transform pipeline: parse -> modify -> deparse. + +### `transformSync(sql, callback, options?)` + +Sync version of transform. + +### `deparseSync(parsed, options?)` + +Converts a parsed script back to SQL. + +Options: +- `pretty` (default: `true`) - Whether to pretty-print the output + +## Re-exports + +For power users, the package re-exports underlying primitives: + +- `parseSql` - SQL parser from `@libpg-query/parser` +- `parsePlpgsqlBody` - PL/pgSQL parser from `@libpg-query/parser` +- `deparseSql` - SQL deparser from `pgsql-deparser` +- `deparsePlpgsqlBody` - PL/pgSQL deparser from `plpgsql-deparser` +- `hydratePlpgsqlAst` - Hydration utility from `plpgsql-deparser` +- `dehydratePlpgsqlAst` - Dehydration utility from `plpgsql-deparser` From 3c646eb7a854f815ad9005fb361fcae455a474d7 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Wed, 31 Dec 2025 23:25:00 +0000 Subject: [PATCH 3/3] docs(plpgsql-parser): add pretty headers and experimental warning --- packages/plpgsql-parser/README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/plpgsql-parser/README.md b/packages/plpgsql-parser/README.md index 2a6e4f2a..145497d2 100644 --- a/packages/plpgsql-parser/README.md +++ b/packages/plpgsql-parser/README.md @@ -1,7 +1,32 @@ # plpgsql-parser +

+ +

+ +

+ + + + + +

+ Combined SQL + PL/pgSQL parser with hydrated ASTs and transform API. +> **⚠️ Experimental:** This package is currently experimental. If you're looking for just SQL parsing, see [`pgsql-parser`](https://www.npmjs.com/package/pgsql-parser). For just PL/pgSQL deparsing, see [`plpgsql-deparser`](https://www.npmjs.com/package/plpgsql-deparser). + +## Overview + +This package provides a unified API for parsing SQL scripts containing PL/pgSQL functions. It combines the SQL parser and PL/pgSQL parser, automatically detecting and hydrating PL/pgSQL function bodies. + +Key features: + +- Auto-detects `CREATE FUNCTION` statements with `LANGUAGE plpgsql` +- Hydrates PL/pgSQL function bodies into structured ASTs +- Transform API for parse → modify → deparse workflows +- Re-exports underlying primitives for power users + ## Installation ```bash @@ -80,3 +105,7 @@ For power users, the package re-exports underlying primitives: - `deparsePlpgsqlBody` - PL/pgSQL deparser from `plpgsql-deparser` - `hydratePlpgsqlAst` - Hydration utility from `plpgsql-deparser` - `dehydratePlpgsqlAst` - Dehydration utility from `plpgsql-deparser` + +## License + +MIT