fix(sql): extract triggers and link them to their table - #3863
rajatnagda45 wants to merge 1 commit into
Conversation
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.
|
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. |
There was a problem hiding this comment.
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— impacttests/test_multilang.py— impact, changed-testtests/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).
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>
|
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 |
What
CREATE TRIGGERis under-extracted in two ways:A trigger with a procedural body is dropped entirely:
No
trg_afternode at all.Even a cleanly-parsed trigger gets a node with no link to its table:
audit_insexists but has notriggersedge tousers.Why
create_triggerAST branch read the subject table offkeyword_for. But the table followsON(AFTER INSERT ON users);FOR EACH ROWcarries no table, sotbl_namestayedNoneand no edge was emitted.FOR EACH ROW BEGIN … ENDbody has no grammar production, so the whole statement becomes anERRORnode. The only safety net is the whole-file routine-recovery scan — but its pattern (_ROUTINE_RECOVERY_RX) matches onlyFUNCTION/PROCEDURE, so triggers were never recovered.The fix
keyword_oninstead ofkeyword_for._TRIGGER_RECOVERY_RXused in thehas_errorrecovery block. It mints the trigger node (label without(), since a trigger is not callable — matching the AST path) and atriggersedge to the table named by the firstONafter the trigger name (the timing clauseBEFORE INSERTcarries 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_RXbecause that path labels matchesname(). A trigger already recovered via the AST path is skipped so no duplicate edge is produced.Tests
Added regression tests in
tests/test_multilang.pyfor both the cleanEXECUTE FUNCTIONform (table link) and the proceduralBEGIN … ENDform (full recovery, single edge, no dangling).-k "sql or trigger"green (37 passed).ruff checkclean.