Skip to content

fix(traverse): dispatch visitors for untagged typed embedded fields#313

Merged
pyramation merged 2 commits into
mainfrom
fix/traverse-typed-field-dispatch
Jul 22, 2026
Merged

fix(traverse): dispatch visitors for untagged typed embedded fields#313
pyramation merged 2 commits into
mainfrom
fix/traverse-typed-field-dispatch

Conversation

@pyramation

Copy link
Copy Markdown
Collaborator

Summary

walk() used the runtime schema (src/18/runtime-schema.ts) for descent but not for dispatch: concrete typed embedded fields (e.g. CreatePolicyStmt.table: RangeVar) are bare untagged objects in libpg_query JSON ({schemaname, relname, ...} — no {RangeVar: {...}} wrapper), so the single-capitalized-key tag detection never fired and the RangeVar visitor was never called for them. Concretely, walking CREATE POLICY p ON app.posts FOR SELECT USING (true); with a RangeVar visitor never visited app.posts.

Fix — synthesize the tag from the runtime schema when descending into a concrete typed field with an untagged value:

// walk(): field descent now routes through walkFieldValue
walkFieldValue(value, declaredType, cb, parent, keyPath) {
  if (declaredType !== 'Node' && schemaMap.has(declaredType) && !isTaggedNode(value)) {
    walkNode(declaredType, value, cb, parent, keyPath);  // synthesized NodePath(tag=declaredType)
  } else {
    walk(value, cb, parent, keyPath);  // tagged values: unchanged
  }
}
  • walkNode (extracted from the old tagged branch) constructs the NodePath, honors the false = skip-children return contract, and continues descending using the node's own NodeSpec. Both array (field.isArray, e.g. ParseResult.stmts: RawStmt[]) and scalar cases are handled.
  • visit() had the same gap (it only recursed into single-key objects); it now consults the runtime schema per field and synthesizes the type for untagged concrete-typed values, both scalar and array.
  • Already-tagged values keep the existing behavior exactly.

Affected typed fields include CreatePolicyStmt.table, IndexStmt.relation, CreateTrigStmt.relation/.constrrel, AlterTableStmt.relation, IntoClause.rel, ColumnDef.typeName, ParseResult.stmts, etc.

Impact

Visitors now fire on nodes they previously never saw — downstream visitors will see additional (correct) node visits (e.g. RangeVar for policy/index/trigger/ALTER TABLE targets). In-repo consumers audited: plpgsql-parser (walkSqlExpressions/sqlVisitor uses @pgsql/traverse walk, benefits directly; all 303 tests pass); @pgsql/transform and pgsql-deparser do not use traverse. Downstream workarounds like manual per-node extraction in constructive-db's facts classifier can be removed.

Tests

New packages/traverse/__tests__/typed-fields.test.ts (real libpg_query PG18 AST shapes): CreatePolicyStmt.table, IndexStmt.relation, CreateTrigStmt.relation, AlterTableStmt.relation, ColumnDef.typeName, array case (ParseResult.stmtsRawStmt), false-return skip-children on a synthesized tag, and tagged-node behavior unchanged — for both walk and visit. Full workspace pnpm run build && pnpm run test passes (deparser 714, transform 1437, plpgsql-parser 303, traverse 23, etc.).

Link to Devin session: https://app.devin.ai/sessions/0ce0d0a13c624fb8b8eb22c77d448fb6
Requested by: @pyramation

@pyramation pyramation self-assigned this Jul 22, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@pyramation
pyramation merged commit 3b061b8 into main Jul 22, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant