Skip to content

Add sqlglot drift tripwires for common.ai read-only SQL validation - #74487

Open
Lee-W wants to merge 3 commits into
apache:mainfrom
astronomer:common-ai-sqlglot-ast-tripwire
Open

Lee-W wants to merge 3 commits into
apache:mainfrom
astronomer:common-ai-sqlglot-ast-tripwire

Conversation

@Lee-W

@Lee-W Lee-W commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Why

apache-airflow-providers-common-ai has no upper bound on sqlglot. If an upstream release renames, splits or removes an exp expression class that utils/sql_validation.py references, the read-only check could silently stop recognising a write statement and let it through. A failure at that point should show up in CI, not in production.

What

Adds a TestSqlglotDriftTripwires test class. When one of its tests fails after a sqlglot bump, the message points at _DATA_MODIFYING_NODES / DEFAULT_ALLOWED_TYPES in utils/sql_validation.py for review.

  • No allowed type overlaps (is a parent or child of) a data-modifying type.
  • Each write statement (insert, update, delete, merge, drop, truncate, create, CREATE ... AS SELECT, alter, CTE-wrapped DELETE / INSERT, SELECT ... INTO) still parses to the node class named in the deny-list, in postgres, mysql, snowflake, bigquery and sqlite.
  • Write and DDL statements, including COPY and GRANT, are rejected in all five dialects with allow_read_only_metadata on and off, and each case pins whether the top-level type check or the deep scan rejects it. DESCRIBE ALTER and EXPLAIN DELETE are rejected by the deep scan in metadata mode.
  • Each write statement (insert, update, delete, merge, drop, truncate, create, CREATE ... AS SELECT, alter, CTE-wrapped DELETE / INSERT / MERGE, SELECT ... INTO) still parses to the node class named in the deny-list, in postgres, mysql, snowflake, bigquery and sqlite.
  • Write and DDL statements, including COPY and GRANT, are rejected in all five dialects with allow_read_only_metadata on and off, and each case pins whether the top-level type check or the deep scan rejects it. DESCRIBE ALTER, DESCRIBE ALTER SYSTEM (parsed as exp.Command) and EXPLAIN DELETE are rejected by the deep scan in metadata mode.

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: [Claude] following the guidelines


  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

@developer-rpai developer-rpai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice defensive work — an unpinned sqlglot silently reclassifying a write as a read would be a nasty failure mode, and tripwires are the right belt-and-suspenders.

A few observations:

  1. The tests import _DATA_MODIFYING_NODES (underscore-private) from sql_validation. If a refactor renames that constant — exactly the drift this guards — the tests fail at import time rather than with a useful message. Testing through the public parse_sql/collect_table_references API would keep the tripwire semantics without the private coupling.
  2. _DIALECTS covers postgres/mysql/snowflake/bigquery/sqlite, but the write-denial parse assertions only run under postgres. MERGE/COPY/SELECT ... INTO parse trees differ by dialect — worth parametrizing at least the dialect-sensitive statements.
  3. On the overlap test: if sqlglot renames a denied class, the old name fails test (a) first anyway, so the failure ordering is sensible — a one-line comment noting that would save the next reader some puzzling.
  4. Scope question: the root cause named in the comment block is the missing upper bound on sqlglot in pyproject.toml. Tripwires are good, but is an upper-bound pin also wanted?

Comment thread providers/common/ai/tests/unit/common/ai/utils/test_sql_validation.py Outdated
Comment thread providers/common/ai/tests/unit/common/ai/utils/test_sql_validation.py Outdated
Comment thread providers/common/ai/tests/unit/common/ai/utils/test_sql_validation.py Outdated
Comment thread providers/common/ai/tests/unit/common/ai/utils/test_sql_validation.py Outdated
Comment thread providers/common/ai/tests/unit/common/ai/utils/test_sql_validation.py Outdated
@Lee-W
Lee-W force-pushed the common-ai-sqlglot-ast-tripwire branch from cbbde87 to e5c5cc4 Compare October 10, 2026 07:23
@Lee-W
Lee-W force-pushed the common-ai-sqlglot-ast-tripwire branch from e5c5cc4 to 3124621 Compare October 11, 2026 06:58
Lee-W added 3 commits October 11, 2026 10:38
sqlglot has no upper bound in common.ai, so an upstream rename, split or removal of an expression class could make the read-only check silently let a write through. These tests fail in CI instead, pointing at the node list in sql_validation.py to review.
- Run the write-rejection matrix with allow_read_only_metadata on and off,
  add DESCRIBE ALTER and EXPLAIN DELETE cases, and pin the guard each
  statement is expected to hit via match=
- Parse every write statement to its denied node in every dialect from a
  single {statement: node} mapping, including CTE and CREATE ... AS SELECT
- Rename the side-effect function test to say it is only flagged when
  allowed_tables is set, and pin that validate_sql itself accepts the call
- Drop tripwires that could not fail for the drift they described and the
  duplicated read-statement and allow-list control tests
- Add a DESCRIBE ALTER SYSTEM row to the wrapped-write matrix so removing
  exp.Command from the deny-list fails a test in metadata mode
- Add a cte_merge row (MERGE ... THEN DELETE inside a CTE) so removing
  exp.Merge fails the validate_sql cases, not only the node-type check
- Drop test_explain_wrapped_write_still_blocked, which the matrix's
  explain_delete_mysql row already covers
@Lee-W
Lee-W force-pushed the common-ai-sqlglot-ast-tripwire branch from 3124621 to 3023832 Compare October 11, 2026 09:38

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants