feat(sql): extend the table scanner with DDL verbs and the Access IN clause - #275
Merged
Merged
Conversation
…clause
The shared SQL table scanner captured names after FROM/JOIN/INTO/UPDATE
only, so two real Access idioms were invisible to the graph:
- CREATE TABLE / ALTER TABLE / DROP TABLE targets produced no
reference at all, so "who writes to table X" missed the code that
creates or destroys X outright;
- the Access `IN "<path>"` clause, which points a query at ANOTHER
database file, looked like a plain local access — a cross-backend
edge with no representation.
Design decisions taken:
- The three DDL verbs join the existing clause alternation and all
carry access: 'write'. Creating, reshaping or dropping a table are
all mutations of the named table, so a write is the honest tag. The
two-word keywords are normalized to a single interior space so the
emitted `clause` stays a stable enum value.
- The `IN` operand is a FILE, not a table, so it gets its own scanner
(`scanSqlExternalBackends`) rather than being folded into the table
rows. Only a QUOTED operand matches, which is what separates the
external-backend clause from the `IN (1,2)` value-list operator
without a parser. The empty operand of the ODBC/dBASE connect-string
form (`IN "" [ODBC;…]`) names a DSN, not a file we can key on, and is
dropped — silent beats wrong.
- The external backend is a `file`-kind node: it IS a file, just not
one this index parsed. No new NodeKind is needed, and
`metadata.external` keeps it distinguishable from indexed files in
every query. The edge is tagged `synthesizedBy:
'vba-external-backend'`, matching the shape #257 Half B will need for
linked-table origins.
- Paths are normalized (lowercase, forward slashes, collapsed and
trimmed separators, UNC prefix preserved) and the node is keyed on
the normalized path via a synthetic file path — the same trick the
TempVars sweep uses — so the same backend named from two different
queries converges on ONE node. Every field of the node derives from
that path alone, so all three emitters produce a byte-identical node
and none can silently rewrite another's fields; per-site position
lives on the edge, where call-site information belongs.
- All three consumers of the shared scanner (in-code SQL sweep, form
RecordSource/RowSource bindings, saved queries) were wired up, since
a change to this module is a change to all three.
The reserved-word reject list is untouched and is now regression-tested
explicitly: its exact contents are pinned, and every one of its 21 tokens
is swept against every capturing clause including the three new DDL
verbs, so `WHERE` can still never be emitted as a table name.
Closes #256
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019gmKKUq1ng5ESk6Qhxu77d
ardelperal
force-pushed
the
feat/issue-256
branch
from
September 2, 2026 05:54
de482cf to
f543be4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Task T15 of the VBA node-discovery plan. The shared SQL table scanner — the one module every SQL path in the project uses — gained two things:
CREATE TABLE/ALTER TABLE/DROP TABLE. All three targets are captured and carryaccess: 'write'; creating, reshaping or dropping a table are all mutations of it, so "who writes to table X" now reports them. The two-word keywords are normalized to a single interior space soclausestays a stable enum value.The Access
IN "<path>"clause. The operand is an external database file, not a table, so it gets its own scanner rather than being folded into the table rows. It emits afile-kind node keyed on the normalized path (lowercase, forward slashes, collapsed/trimmed separators, UNC prefix preserved) withmetadata: { external: true, backendPath }, plus areferencesedge taggedsynthesizedBy: 'vba-external-backend'. No newNodeKind.The node is keyed via a synthetic file path (same trick the TempVars sweep uses) and every one of its fields derives from the normalized path alone — no line, no column, no per-caller language — so all three emitters produce a byte-identical node and the same backend named from two different queries converges on ONE node. Per-site position lives on the edge.
Only a quoted operand matches, which is what separates the external-backend clause from the
IN (1,2)value-list operator without writing a parser. The empty operand of the ODBC/dBASE connect-string form (IN "" [ODBC;DSN=x]) names a DSN, not a file we can key on, and is dropped — silent beats wrong. A first alternative also handles the VBA doubled-quote shape (… IN ""C:\x.accdb"""), which is how a SQL literal built in VBA source reaches the scanner.All three consumers were wired up, since a change to this module is a change to all three: the in-code SQL sweep (
vba/sql-wrapper.tsvia a new context helper), the formRecordSource/RowSourcebindings, and the saved-query extractor.The risk: the reserved-word reject list
SQL_RESERVED_TABLE_TOKENSis the project's main defence against emittingWHEREas a table name, and adding new capturing clauses is exactly the change that could weaken it. It is untouched, and is now regression-tested explicitly rather than incidentally:CREATE TABLE ? (…)/DROP TABLE ?— the?-sentinel shape a dropped concat operand produces — capture nothing.Testing
__tests__/sql-clause-coverage.test.ts: 78 tests, all green. Covers DDL verbs, the reject-list regression sweep, path normalization, theINscanner (double/single/doubled quotes, value-list operator, empty operand, dedup), the node builder, and all three consumers end-to-end — including the acceptance criterion that the same external path named from two different queries converges on one file node.npx tsc --noEmit: clean.npx vitest run(full suite, afternpm run build): 3273 passed, 21 failed. Those 21 are pre-existing and environmental, not caused by this change — I verified it by reverting the source changes and re-running the same four files, which produced the identical 21 failures (worktree-detection15,npm-sdk2,multi-repo-workspace2,extraction2 — all WindowsEPERMtemp-dir cleanup inafterEach, plus two npm-bundle packaging tests). None of them assert extraction behaviour.Acceptance criteria I could NOT verify
sqlTablesReferencedrises; no reserved word appears among the new names." I did not run the T0 coverage probe against a real Access project — no.accdb-backed corpus was available in this environment. The direction is guaranteed by construction (only new captures are added; no existing capture path was narrowed) and the reserved-word half is covered by the sweep above, but the actual before/after number is not measured here.🤖 Generated with Claude Code
https://claude.ai/code/session_019gmKKUq1ng5ESk6Qhxu77d