@pgsql/transform 18.4.1 → 18.5.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.
Files changed (3) hide show
  1. package/esm/facts.js +41 -1
  2. package/facts.js +40 -0
  3. package/package.json +2 -2
package/esm/facts.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { walk as walkSql } from '@pgsql/traverse';
2
- import { transformSync, walk as walkPlpgsql } from 'plpgsql-parser';
2
+ import { parseSql, transformSync, walk as walkPlpgsql } from 'plpgsql-parser';
3
3
  const SECURITY_TAGS = new Set([
4
4
  'CreatePolicyStmt',
5
5
  'AlterPolicyStmt',
@@ -241,6 +241,43 @@ function classifyOne(nodeTag, node) {
241
241
  collectRoles(node, facts.roles);
242
242
  return facts;
243
243
  }
244
+ /** Read a `CreateFunctionStmt` DefElem option's scalar/list value. */
245
+ function functionOption(node, defname) {
246
+ for (const opt of node.options ?? []) {
247
+ if (opt?.DefElem?.defname === defname)
248
+ return opt.DefElem.arg;
249
+ }
250
+ return undefined;
251
+ }
252
+ /**
253
+ * Collect references from a `LANGUAGE sql` function body supplied as a string
254
+ * literal (`AS $$ ... $$`). That body is an opaque String node the AST walker
255
+ * never parses, so — mirroring the schema transformer's body rewrite — parse
256
+ * it standalone and walk each statement with the facts visitor. The standard
257
+ * `BEGIN ATOMIC` / `RETURN` `sql_body` form is already part of the AST and is
258
+ * covered by the outer walk, so only the string form needs this.
259
+ */
260
+ function collectSqlBodyReferences(node, facts) {
261
+ const language = functionOption(node, 'language')?.String?.sval;
262
+ if (typeof language !== 'string' || language.toLowerCase() !== 'sql')
263
+ return;
264
+ const asArg = functionOption(node, 'as');
265
+ const items = asArg?.List?.items ?? [];
266
+ const body = items[0]?.String?.sval;
267
+ if (typeof body !== 'string')
268
+ return;
269
+ try {
270
+ const stmts = parseSql(body)?.stmts ?? [];
271
+ const visitor = createFactsVisitor(facts, facts.bodyReferences);
272
+ for (const stmt of stmts) {
273
+ if (stmt?.stmt)
274
+ walkSql(stmt.stmt, visitor);
275
+ }
276
+ }
277
+ catch {
278
+ // A non-parseable body (C symbol name, etc.) contributes no references.
279
+ }
280
+ }
244
281
  /**
245
282
  * Classify each top-level statement in a SQL script into {@link StatementFacts}.
246
283
  *
@@ -260,6 +297,9 @@ export function classifyStatements(sql) {
260
297
  if (stmtNode) {
261
298
  walkSql(stmtNode, createFactsVisitor(facts));
262
299
  }
300
+ if (nodeTag === 'CreateFunctionStmt') {
301
+ collectSqlBodyReferences(node, facts);
302
+ }
263
303
  allFacts.push(facts);
264
304
  }
265
305
  for (const fn of ctx.functions ?? []) {
package/facts.js CHANGED
@@ -244,6 +244,43 @@ function classifyOne(nodeTag, node) {
244
244
  collectRoles(node, facts.roles);
245
245
  return facts;
246
246
  }
247
+ /** Read a `CreateFunctionStmt` DefElem option's scalar/list value. */
248
+ function functionOption(node, defname) {
249
+ for (const opt of node.options ?? []) {
250
+ if (opt?.DefElem?.defname === defname)
251
+ return opt.DefElem.arg;
252
+ }
253
+ return undefined;
254
+ }
255
+ /**
256
+ * Collect references from a `LANGUAGE sql` function body supplied as a string
257
+ * literal (`AS $$ ... $$`). That body is an opaque String node the AST walker
258
+ * never parses, so — mirroring the schema transformer's body rewrite — parse
259
+ * it standalone and walk each statement with the facts visitor. The standard
260
+ * `BEGIN ATOMIC` / `RETURN` `sql_body` form is already part of the AST and is
261
+ * covered by the outer walk, so only the string form needs this.
262
+ */
263
+ function collectSqlBodyReferences(node, facts) {
264
+ const language = functionOption(node, 'language')?.String?.sval;
265
+ if (typeof language !== 'string' || language.toLowerCase() !== 'sql')
266
+ return;
267
+ const asArg = functionOption(node, 'as');
268
+ const items = asArg?.List?.items ?? [];
269
+ const body = items[0]?.String?.sval;
270
+ if (typeof body !== 'string')
271
+ return;
272
+ try {
273
+ const stmts = (0, plpgsql_parser_1.parseSql)(body)?.stmts ?? [];
274
+ const visitor = createFactsVisitor(facts, facts.bodyReferences);
275
+ for (const stmt of stmts) {
276
+ if (stmt?.stmt)
277
+ (0, traverse_1.walk)(stmt.stmt, visitor);
278
+ }
279
+ }
280
+ catch {
281
+ // A non-parseable body (C symbol name, etc.) contributes no references.
282
+ }
283
+ }
247
284
  /**
248
285
  * Classify each top-level statement in a SQL script into {@link StatementFacts}.
249
286
  *
@@ -263,6 +300,9 @@ function classifyStatements(sql) {
263
300
  if (stmtNode) {
264
301
  (0, traverse_1.walk)(stmtNode, createFactsVisitor(facts));
265
302
  }
303
+ if (nodeTag === 'CreateFunctionStmt') {
304
+ collectSqlBodyReferences(node, facts);
305
+ }
266
306
  allFacts.push(facts);
267
307
  }
268
308
  for (const fn of ctx.functions ?? []) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pgsql/transform",
3
- "version": "18.4.1",
3
+ "version": "18.5.0",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "AST-based SQL transformation, qualification, classification and closure analysis for PostgreSQL",
6
6
  "main": "index.js",
@@ -49,5 +49,5 @@
49
49
  "classify",
50
50
  "plpgsql"
51
51
  ],
52
- "gitHead": "df667707ab725d762cac0daab26a07d1b202d1e1"
52
+ "gitHead": "a2b52f04fca42931150378d5b4966b7d5a922de6"
53
53
  }