… drift
The no_destructive_sql validator referenced both exp.AlterTable and
exp.Grant in one function. No single installable sqlglot version exposes
both: AlterTable was renamed to Alter, and Grant was added later. On the
pinned range (sqlglot>=23.0.0,<24.0) exp.Grant does not exist, so the
isinstance(statement, exp.Grant) check raised AttributeError for every
query, including safe SELECTs, disabling the destructive-SQL validator
entirely.
Resolve the ALTER/GRANT/TRUNCATE expression classes via getattr with the
known aliases and filter out the ones absent on the installed version, so
the same code works whether sqlglot exposes Alter, AlterTable, Grant, and
TruncateTable. GRANT/REVOKE still fall through to the existing Command
handling on versions without a dedicated Grant node, so no destructive
coverage is dropped. The TRUNCATE check also now matches the dedicated
TruncateTable node used by current sqlglot.
Add a regression test asserting the validator runs on the installed
sqlglot without AttributeError.
Found via a paper-reproduction audit of the shipped control-plane code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Imran Siddique <imran.siddique@opaque.co>
Summary
The
no_destructive_sqlvalidator in the agent-control-plane policy engine references bothexp.AlterTableandexp.Grant(sqlglot expression classes) in a single function. No single installable sqlglot version exposes both symbols:AlterTablewas renamed toAlterin a later release.Grantwas only added later still.On the pinned range in
pyproject.toml(sqlglot>=23.0.0,<24.0),exp.Grantdoes not exist. Because the checks are sequentialif isinstance(...)statements, theisinstance(statement, exp.Grant)line raisesAttributeErrorfor every query, including safeSELECTs. The destructive-SQL validator is therefore completely non-functional on the pinned version.Reproduction on the pinned sqlglot (23.17.0):
The
no_destructive_sqlvalidator raisesAttributeErroron a plainSELECT * FROM users.Fix
Resolve the destructive-statement expression classes in a version-robust way and filter out any that are absent:
alter_types = (getattr(exp, "Alter", None), getattr(exp, "AlterTable", None))withNonefiltered out.grant_types = (getattr(exp, "Grant", None),)withNonefiltered out.truncate_types = (getattr(exp, "TruncateTable", None),)withNonefiltered out.No destructive-statement coverage is dropped:
Command-node handling on versions that lack a dedicatedGrantnode.TruncateTablenode used by current sqlglot (it previously only matched aCommandnode, which does not occur on the pinned version).Verification
AttributeError.tests/test_sql_policy.py: 43 passed (was 16 failing due to the AttributeError cascade, plus one TRUNCATE failure, before this change).test_no_attributeerror_on_installed_sqlglotthat fails if the validator ever again references a sqlglot symbol missing on the installed version.Provenance
Found via a paper-reproduction audit of the shipped control-plane code.
🤖 Generated with Claude Code