Skip to content

fix(sql): extract triggers and link them to their table - #3863

Closed
rajatnagda45 wants to merge 1 commit into
Graphify-Labs:v8from
rajatnagda45:fix/sql-trigger-extraction
Closed

rajatnagda45 wants to merge 1 commit into
Graphify-Labs:v8from
rajatnagda45:fix/sql-trigger-extraction

Conversation

@rajatnagda45

Copy link
Copy Markdown
Contributor

What

CREATE TRIGGER is under-extracted in two ways:

  1. A trigger with a procedural body is dropped entirely:

    CREATE TRIGGER trg_after AFTER INSERT ON users
    FOR EACH ROW
    BEGIN
      UPDATE stats SET cnt = cnt + 1;
    END;

    No trg_after node at all.

  2. Even a cleanly-parsed trigger gets a node with no link to its table:

    CREATE TRIGGER audit_ins AFTER INSERT ON users EXECUTE FUNCTION log_it();

    audit_ins exists but has no triggers edge to users.

Why

  • (2) The create_trigger AST branch read the subject table off keyword_for. But the table follows ON (AFTER INSERT ON users); FOR EACH ROW carries no table, so tbl_name stayed None and no edge was emitted.
  • (1) A FOR EACH ROW BEGIN … END body has no grammar production, so the whole statement becomes an ERROR node. The only safety net is the whole-file routine-recovery scan — but its pattern (_ROUTINE_RECOVERY_RX) matches only FUNCTION/PROCEDURE, so triggers were never recovered.

The fix

  • Read the trigger's table after keyword_on instead of keyword_for.
  • Add a dedicated _TRIGGER_RECOVERY_RX used in the has_error recovery block. It mints the trigger node (label without (), since a trigger is not callable — matching the AST path) and a triggers edge to the table named by the first ON after the trigger name (the timing clause BEFORE INSERT carries none). It reuses the same masked-source scan and delimited-identifier atom as the routine recovery, so commented-out / single-quoted DDL can't fabricate a trigger. TRIGGER is deliberately kept out of _ROUTINE_RECOVERY_RX because that path labels matches name(). A trigger already recovered via the AST path is skipped so no duplicate edge is produced.

Tests

Added regression tests in tests/test_multilang.py for both the clean EXECUTE FUNCTION form (table link) and the procedural BEGIN … END form (full recovery, single edge, no dangling). -k "sql or trigger" green (37 passed). ruff check clean.

Two defects left CREATE TRIGGER under-extracted:

- The create_trigger AST branch read the subject table off keyword_for, but the
  table follows ON ('... AFTER INSERT ON users'); FOR EACH ROW carries no table.
  So even a cleanly-parsed trigger got a node with no link to the table it fires
  on. Read the table after keyword_on instead.

- A trigger with a procedural body ('FOR EACH ROW BEGIN ... END') has no grammar
  production, so the statement lands in an ERROR node and only the whole-file
  routine-recovery scan can save it — but that scan's pattern matched only
  FUNCTION/PROCEDURE, so the trigger and its table were dropped entirely. Add a
  dedicated trigger-recovery pattern that mints the trigger node (label without
  (), since a trigger is not callable) and a triggers edge to the table named by
  the first ON after the trigger name. A trigger already recovered via the AST
  path is skipped to avoid a duplicate edge.

Added regression tests for both the clean EXECUTE FUNCTION form and the
procedural BEGIN...END form.
@github-actions

Copy link
Copy Markdown

Thanks for the pull request, @rajatnagda45. A maintainer will review it soon.

Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions.

A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.


Graphify review — findings

Fixes SQL trigger extraction to link a trigger to the table it fires on by reading the subject table from the ON clause (AFTER INSERT ON users) rather than FOR, which carried no table and left every trigger unlinked. Adds regex-based recovery via _TRIGGER_RECOVERY_RX for triggers with procedural FOR EACH ROW BEGIN...END bodies that fail grammar parsing and land in ERROR nodes, emitting the trigger node and its triggers edge while skipping ids already owned by a cleanly-parsed trigger to avoid duplicates. Triggers are kept out of the routine-recovery path so they aren't mislabeled as callable name().

Worth a look

  • Trigger recovery fabricates graph entries from SQL string literals — graphify/extractors/sql.py:739 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Trigger recovery regex crosses statement boundaries — graphify/extractors/sql.py:55 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 345 functions depend on the 155 functions this change touches.

Health — this change adds coupling hotspots:

  • new: main() — 98 callers, 3 callees
  • new: dispatch_command() — 2 callers, 125 callees
  • new: extract_sql() — 23 callers, 9 callees
  • new: walk() — 1 callers, 8 callees
  • new: test_poisoned_manifest_is_healed() — 0 callers, 6 callees

Verification — 345 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 167 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

3 of 302 test file(s) selected (1%) via static blast radius.

  • tests/test_extract.py — impact
  • tests/test_multilang.py — impact, changed-test
  • tests/test_pg_introspect.py — impact

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

· 5 more finding(s) on lines outside this diff (see the check run).

safishamsi added a commit that referenced this pull request Sep 28, 2026
SQL triggers (#3863), Groovy enums (#3861), R namespace-qualified
constructors (#3864) and R6 self$/private$ calls (#3865), markdown
wikilink index ignore-rules (#3826), foreign manifest key syntax (#3879),
tweet oEmbed host-only rewrite (#3880), and the refreshed install banner
(#3892).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@safishamsi

Copy link
Copy Markdown
Member

Shipped in v0.9.71 (now on PyPI) via an authorship-preserving cherry-pick, so your commit keeps contributor-graph credit. Thanks @rajatnagda45! SQL CREATE TRIGGER is extracted and linked to its table, including procedural BEGIN...END bodies.

Release: https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.71

@safishamsi safishamsi closed this Sep 28, 2026
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.

2 participants