Skip to content

feat(syntax): check Snowflake views and streams against their grammar - #185

Closed
kvlonge wants to merge 32 commits into
mainfrom
feat/syntax-ddl-views
Closed

kvlonge wants to merge 32 commits into
mainfrom
feat/syntax-ddl-views

Conversation

@kvlonge

@kvlonge kvlonge commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

Why

After the table grammar, the view, materialized view, stream and semantic view forms were the next ddl gap: Raw fallbacks, false rejects of ALTER VIEW properties, and stream options tied to sources Snowflake does not tie them to. This replaces them with checked grammars that match the recorded receipts.

Changes

  • Streams, views, materialized views and semantic views build nodes through checked grammars. A stream takes any option, AT or BEFORE (with VERSION) on every source, as Snowflake parses them.
  • ALTER VIEW reads SET and UNSET of properties, IF EXISTS and the name SET may omit; ALTER MATERIALIZED VIEW reads RECLUSTER. An ALTER VIEW without an action and a CREATE missing STREAM are refused.
  • A view's column list may follow its properties; a view COMMENT without = is refused.
  • Pins evidence 5783d7bb and re-records baseline.json and corpus-verdicts.json: seven non-Snowflake view clauses (SECURITY DEFINER/INVOKER, SQL SECURITY, LOCKING, COMMENT without =) are now refused.
  • Root .gitattributes marks tests, scripts and dev-only tools as linguist-documentation, so the language bar counts shipped code:
Language Before After (est.)
Rust 80.9% 96.8%
Python 17.2% 0.3%
MDX 0.6% 0.9%
JavaScript 0.5% 0.7%
Other 0.8% 1.3%

Verification

Snapshots on evidence 5783d7bb, #179's parser → this head:

before after
satisfied obligations 75,187 76,359
false accepts 1,748 1,592
contradictions 2,661 2,385

syntax-coverage diff: 0 regressions, 1,172 obligations gained; three documented forms are now accepted (ALTER VIEW SET SECURE with properties, UNSET SECURE twice, DEFINE … RECURSIVE VIEW).

In the view and stream forms (2,524 judged cases): false rejects 26 → 0, false accepts 8 → 0. The two unjudged view forms now have receipts and match them.

make check-ci passes; crate tests cover each root cause.

CREATE EXTERNAL TABLE was checked against its grammar but kept as raw
text, so every accepted statement counted as an opaque fallback. It now
returns a CreateObject with column definitions, computed expressions,
inline constraints and properties, and the grammar still refuses the
same statements.
A CREATE TABLE the table model cannot hold, such as one with column
masking or projection policies or table policies, was checked against
its grammar and kept as raw text. It now becomes a CreateObject of kind
TABLE: columns with their options, out-of-line constraints and table
properties as properties. The shared name, name list, tag and value
checks now return what they read.
…as nodes

CREATE DYNAMIC ICEBERG TABLE and CREATE INTERACTIVE TABLE were checked
against their grammar and kept as raw text. They now become a
CreateObject with their columns, clustering, properties and the query
or REFRESH USING insert.
… tables

These kinds fell through to the generic CREATE, which kept any text as
raw, so malformed statements were accepted. They now have grammars that
build a CreateObject: an event table takes table properties, an event
routing table its rules and an online feature table its key, properties
and source. Partial kinds such as CREATE FEATURE TABLE are refused, and
a table property may now be written WITH COMMENT = <value>.
…r grammar

CREATE ICEBERG TABLE and CREATE DYNAMIC TABLE went through the generic
table parser, which refused valid column options and properties and
accepted properties with no value. In the faithful Snowflake modes they
are now built by the table grammar with each kind's properties: an
iceberg table may have no columns, partitions only over its columns and
takes no OR ALTER; a dynamic table needs TARGET_LAG, WAREHOUSE and a
query or REFRESH USING. TRANSIENT now reaches both kinds, and a table
the generic parser finishes with raw properties is built by the table
grammar when that holds.
… grammar

DEFINE TABLE and DEFINE DYNAMIC TABLE were refused outright, because
the generic table parser behind them was not checked closely. They are
now built by the table grammar like their CREATE OR ALTER forms.
LOCAL and GLOBAL before TEMP or TEMPORARY, and VOLATILE before TABLE,
sent the statement to the generic CREATE, which kept it as raw text.
They now mark a temporary table. The table grammar also takes a
constraint COMMENT and a GENERATED ALWAYS AS ( <expr> ) [ VIRTUAL ]
column.
The tag, contact and policy actions of ALTER TABLE, ICEBERG TABLE, VIEW
and MATERIALIZED VIEW were checked against their grammar and kept as
raw text. They now become an AlterObject: the action's words, the added
columns or dropped policy as operands, and the tags, contacts and
policies as properties, one per column for a column action.
ALTER ICEBERG TABLE ADD PARTITION BY and DROP PARTITION BY were refused
as if they were the external-table partition actions, and a transform
with no arguments, such as YEAR ( ), was refused in every partition
clause. Both actions now take one partition expression like REPLACE
PARTITION BY, and a transform may be written with no arguments.
An added column now takes the CREATE TABLE column options, including
constraint properties such as NOT ENFORCED or ENABLE VALIDATE, and must
have a type. An out-of-line constraint after ADD takes the same
properties and keeps them in order on the constraint. A constraint
without ADD and ADD ( <column> ) are refused.
…constraint renames

ALTER TABLE takes DROP KEY for the clustering key, DROP UNIQUE and DROP
FOREIGN KEY with an optional column list, each with CASCADE or RESTRICT,
and RENAME CONSTRAINT <name> TO <name>. A key or primary key still takes
no column list.
ALTER [ ICEBERG ] TABLE ADD and DROP SEARCH OPTIMIZATION read their
targets with the dynamic table grammar and build an AlterObject: ON
needs a target, a list of targets is one action, and a reserved word is
refused.
…ctions

ALTER COLUMN takes SET DATA TYPE <type> RENAME FIELDS (or a bare
RENAME), [ SET ] WRITE DEFAULT <expr> and DROP WRITE DEFAULT. SET DATA
needs TYPE. DROP VERSION_TAG with nothing after it drops a column of
that name.
ALTER [ ICEBERG ] TABLE takes RECLUSTER [ MAX_SIZE = <n> ] [ WHERE
<condition> ], SUSPEND RECLUSTER and RESUME RECLUSTER, and ALTER ICEBERG
TABLE takes REFRESH [ '<metadata path>' ], each as an AlterObject.
ALTER TABLE SET takes <property> = <value> pairs separated by spaces or
commas, keeping each as a property, and refuses SET with no property
name.
ALTER TABLE refuses DROP PARTITION BY, which only an iceberg table
takes, and MODIFY DATA FUNCTION, MODIFY DATA METRIC or MODIFY METRIC
FUNCTION without the full DATA METRIC FUNCTION. ALTER ICEBERG TABLE
refuses a RENAME without COLUMN or TO.
A hybrid table's INDEX needs a name and a column list and may take
INCLUDE ( <columns> ), which the index keeps. A hybrid table the table
parser cannot finish, such as one whose comment is a name, is read with
the CREATE TABLE grammar.
EXECUTE AS USER <user> USE SECONDARY ROLES takes a comma-separated list
of roles, not only one.
ALTER SESSION SET refuses a setting with no value and a SET with no
setting, and reads settings separated by spaces as well as commas.
TO_TIMESTAMP ( ) and TRY_TO_TIMESTAMP ( ) parse, as Snowflake's parser
takes them; their arity is a later check. The converter already refuses
them as calls without an argument.
… grammar

The table grammar satisfies 2,899 more obligations with no coverage
regression; re-record the baseline so check-syntax holds the gain.

Eight corpus statements are now refused, as Snowflake refuses them:
ALTER TABLE ADD with a parenthesised column, a MySQL index prefix
length or column position, a UNIQUE constraint with no column list,
and a dynamic table's column list after its properties.
The table grammar built column and external names without their token's
range, so a refused ALTER TABLE ADD COLUMN pointed only at the table
name. Names now keep the range of the token they were read from.
…eipts

The evidence now includes about 3,000 more Snowflake receipts for DDL,
utility, security and class statements. 143 cases that were only
observed get an accept or reject mark from them.
CREATE STREAM kept everything after STREAM as raw text, so malformed
statements were accepted. A stream on a table, view, external, dynamic
or event table or a stage now builds a CreateObject: its tags and COPY
GRANTS before ON, the source, AT or BEFORE where the source takes it,
and the properties that source documents. CREATE OR ALTER STREAM and
DEFINE STREAM read a table only. A time travel point may name a STREAM.
CREATE SEMANTIC VIEW was checked against its grammar and then kept as
raw text, and DEFINE SEMANTIC VIEW was refused. Both now build a
CreateObject whose clauses are properties: tables, relationships,
facts, dimensions, metrics and verified queries as lists of their
parts, with the item expressions parsed.
…eir grammar

Recursive, volatile, secure materialized and interactive materialized
views were kept as raw text, and view-level clauses such as COPY = ( ),
JOIN POLICY, AGGREGATION POLICY and WITH CONTACT were refused. A
Snowflake view now reads its scope, column options, view clauses and
query against the grammar of its kind: a materialized view takes
CLUSTER BY and no CHANGE_TRACKING, and CREATE OR ALTER VIEW takes only
CHANGE_TRACKING and COMMENT. Column policies after the column list,
empty column lists and string column names are refused, as Snowflake
refuses them.
The evidence adds receipts for DDL and security definitions. 36 cases
that were only observed get an accept or reject mark from them.
…mmar

ALTER VIEW now reads Snowflake's SET and UNSET of view properties,
IF EXISTS, and the name Snowflake lets SET omit. An ALTER VIEW with no
action is refused. ALTER MATERIALIZED VIEW reads RECLUSTER and a SET
with the name omitted. A CREATE whose name is followed by ON, a stream
missing its STREAM keyword, is refused.
Snowflake parses any option, and AT or BEFORE, on every stream source
and refuses a misplaced one only after parsing, so the grammar no
longer ties options to sources. AT and BEFORE take VERSION, a tag value
may be a number, and CREATE OR ALTER STREAM takes tags.
Snowflake parses a CREATE VIEW whose column list follows a view
property, such as CHANGE_TRACKING = TRUE ( id ), and refuses a view
COMMENT written without =.
…and stream grammar

On the pinned evidence the view, stream and semantic view grammar
satisfies 1,172 more obligations with no coverage regression; re-record
the baseline so check-syntax holds the gain.

Seven corpus statements are now refused, as Snowflake's view grammar
has no such clause: SECURITY DEFINER or INVOKER and SQL SECURITY, a
LOCKING clause, and a view COMMENT without =.
Test suites, development scripts and development-only tools (the
coverage and format checks, the fuzz targets and the Snowflake
recorder) are marked linguist-documentation, so the language bar
reflects the code SQLQuack ships.
Base automatically changed from feat/syntax-ddl-tables to main November 12, 2026 00:50

kvlonge commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

Replaced by #198: the same change rebased on main, plus the receipt fixes for ALTER VIEW SET or UNSET SECURE with other properties and RECURSIVE after a temporary scope (0 new false accepts against receipts). Opened as a new branch rather than force-pushing this one.

@kvlonge kvlonge closed this Nov 12, 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.

1 participant