Repository navigation
feat(snowflake)!: account objects as statements, and one table's storage facts - #7
Merged
Merged
Conversation
The shape check now lives where the flag has to be honoured rather than where it is declared, so a value that would silently render the wrong DDL is named instead. The attribute is read directly: whether the field exists is settled by the class, so probing for it with a default would only turn a wrong class into a quiet signed column. CI installs the core library from fix/column-constraint-gates so this backend runs against the branch it depends on.
…ng with getattr The materialized-view and time-travel formatters reached for their expression's fields with `getattr(expr, name, default)`. The expressions declare those fields in their constructors, so the defaults only turned a wrong-type argument into a silently wrong statement instead of the AttributeError the reader can act on. Four sites dropped: * `with_data` / `no_data` and `column_aliases` are set by the core `CreateMaterializedViewExpression`, so both the Snowflake expression and the core one this formatter also accepts carry them; * `options` is a required argument of `TemporalOptionsExpression`, and `format_temporal_options` has exactly one caller, which builds that expression itself. `or_replace`, `if_not_exists`, `cluster_by` and `comment` keep their probes. They exist only on the Snowflake expression: the core expression reaching this formatter does not have them, so the probes implement the documented interop rather than hiding a defect. Rendering is unchanged either way -- verified against the pre-change dialect over the materialized-view, core-interop, time-travel and type paths.
Mechanical adaptation to the core renames (W2 + W3a + W3b + W1 + S1-c): 33 files / 420 line touches across src, tests and the README. - W2 statement classes: the forty the core renamed and this package names -- QueryStatement, InsertStatement, MergeStatement, CreateTableStatement and its As/Like/Clone/FromTemplate siblings, DropTableStatement, the Create/Alter/Drop DDL family (database, schema, sequence, type, domain, index, function, trigger, view, materialized view), TruncateStatement, CommentOnStatement, BeginTransactionStatement, CreateMaterializedViewStatement, RefreshMaterializedViewStatement. Backend-owned statement classes (SnowflakeCreateWarehouseExpression and the rest of the Snowflake* family, SetTransactionExpression, PivotExpression) are outside the core's mapping and keep their names, as do the nine core classes the rename left alone (BaseExpression, TableInfoExpression, ForeignKeyExpression, the XML* family, ...). - W2 formatter (functional): format_begin_transaction -> format_begin_transaction_statement, override and super() call alike -- without both the override is dead and Snowflake's BEGIN refusals for DEFERRABLE / WAIT disappear. - W3a: ILIKEExpression -> ILikePredicate, and the functional formatter rename format_ilike_expression -> format_ilike_predicate. - W3b: SQLValueExpression -> SQLValue, TrimExpression -> TrimFunction, CaseExpression -> CaseFunction, CollateExpression -> CollateFunction, UUIDCastExpression -> UUIDCastFunction, _TemporalValueExpression -> _TemporalValue. - W1: CTEExpression -> CTEClause, and the docstring token format_cte_expression -> format_cte_clause in the clause-pair guard. - Introspection: the six data classes take the Introspection prefix (IntrospectionDatabaseInfo, IntrospectionTableInfo, IntrospectionColumnInfo, IntrospectionViewInfo, IntrospectionTriggerInfo, IntrospectionForeignKeyInfo). The six *InfoExpression query nodes and IndexInfo / IndexColumnInfo keep their spellings -- core renamed the containers, not those. Also updates the LIKE/ILIKE and XML formatter tokens in the roundtrip matrix's unmodelled table, and the README's CreateTable sample (the class name only; its ColumnConstraintType argument follows with the constraint surfaces). Constraint surfaces still reference the removed ColumnConstraintType / TableConstraintType enums and follow in the next commit, so this commit is an expected-red window: the package import smoke has exactly the eight failures that are those surfaces plus the pre-existing `named_query` module typo in examples/; compileall src+tests is exit 0; ruff on the 32 changed files is at parity with HEAD (19 findings both sides, same rules and lines).
Core's S3 split the constraint nodes: ``ColumnConstraint`` and
``TableConstraint`` are now productions that carry a **branch**, the branch
names the kind, and the ``ColumnConstraintType`` / ``TableConstraintType``
enums are gone. This commit migrates every construction site in this package
onto that shape, lands the capability mixin the earlier draft left untracked,
and folds the draft's mechanical part in as the wave guide requires.
**The draft, absorbed.** The untracked ``mixins/constraint.py`` declared every
constraint capability this backend answers, read off the two official grammar
pages. It was written against the pre-S3 API, so what is kept is its semantic
content -- the probe answers, the verbatim grammar transcriptions and the
per-kind/per-level split -- and what is rewritten is its old form:
* the two parameterised probes, ``supports_constraint_enforced(constraint_type)``
and ``supports_alter_constraint_enforced(constraint_type)``, are ``(self)``
now. A probe answers one question and the kind is not part of it; core's
``_call_constraint_capability`` (and its ``inspect.signature`` sniffing) no
longer exists, and the docstring that named it is rewritten rather than left
describing a symbol nobody can import;
* ``supports_constraint_naming`` is added as the flat, load-bearing probe core
actually consults. The contract listed Snowflake's answer as a "待核" item;
it is answered from the reference -- ``[ CONSTRAINT <constraint_name> ]``
opens all three column-level and all three table-level productions, and the
ALTER/RENAME verbs depend on the name being there -- so ``True``, with the
per-level/per-kind ``supports_{column,table}_naming_*`` probes kept beside it
as the same fact written kind by kind;
* the ``supports_column_collation`` copy is dropped. It is a column-attribute
answer whose runtime home is ``SnowflakeCollationMixin``, which precedes this
class in the dialect's MRO; a second declaration would be dead code, and two
owners for one switch is what the local-declaration rule exists to prevent;
* the ``_format_constraint_enforcement`` narrative is replaced: that private
hook no longer raises a kind `ValueError`, and what governs the render now is
the flat probe and the branch the kind lives on.
**Construction sites** (5 files: src 3, tests 4 -- one test file shares):
ColumnConstraint(d, ColumnConstraintType.NOT_NULL)
-> ColumnConstraint(d, branch=ColumnNotNullConstraint(d))
ColumnConstraint(d, ColumnConstraintType.DEFAULT, default_value=v)
-> ColumnConstraint(d, branch=ColumnDefaultConstraint(d, v))
TableConstraint(d, TableConstraintType.PRIMARY_KEY, columns=[...], name=n)
-> TableConstraint(d, branch=TablePrimaryKeyConstraint(d, [...]), name=n)
... UNIQUE / CHECK likewise -> TableUniqueConstraint / TableCheckConstraint
UseConstraint(ColumnConstraintType.DEFAULT, default_value=v)
-> UseConstraint(ColumnDefaultConstraint, value=v)
Two call sites read the *kind* rather than construct it, and both read the
branch class now:
* ``SnowflakeAlterColumnModifierMixin.format_add_column_action`` -- the
``IF NOT EXISTS`` restriction set becomes a set of branch classes
(``ColumnDefaultConstraint`` / ``ColumnPrimaryKeyConstraint`` /
``ColumnForeignKeyConstraint`` / ``ColumnUniqueConstraint``).
* ``SnowflakeCapabilityMixin.format_modify_column_action`` -- the MODIFY
re-spelling reads ``isinstance(c.branch, ...)`` instead of comparing an enum,
and the DEFAULT value off the branch (``branch.value``), not off the
production. Behaviour is unchanged: NOT NULL / NULL / DEFAULT are re-spelled,
everything else is skipped as before.
**Wiring.** ``SnowflakeConstraintMixin`` is imported and exported from
``mixins/__init__.py`` and placed in ``SnowflakeDialect``'s base list right
after ``SnowflakeCapabilityMixin``, well ahead of core's ``ConstraintMixin``
(index 25 vs 76) and ``DDLColumnMixin`` (45), so every declaration wins over
the shared default it would otherwise inherit.
**Behaviour changes** (all on the constraint surface, all narrowing):
* only the two probes whose signatures changed, so a parameterised call can no
longer be made at all;
* the capability mixin's answers now come from this package. Three of them
differ from what was inherited: ``supports_constraint_enforced`` and
``supports_deferrable_constraint`` are explicitly ``False`` (see the wave-3
commit for the evidence and the policy), and ``supports_exclude_constraint``
is the local ``False`` rather than a shared default.
**Tests.** ``test_expression_roundtrip_all.py`` registers branch-aware special
constructors for ``ColumnConstraint`` / ``TableConstraint`` /
``AddTableConstraint``; the stale comment naming ``constraint_type`` is
corrected. ``test_ddl_source_mixin.py`` rewrites its kind assertions to
``type(item.branch)`` and its value assertions to ``branch.value`` /
``branch.condition``. ``test_alter_table_if_exists.py`` and
``test_clause_pair_guard.py`` migrate their constructions, the latter to
``AlterConstraint(constraint_kind=TableCheckConstraint)``.
Verification:
- import smoke over every module in the backend package: 121 imported, 0
failures (the pre-existing ``named_query`` module typo in
``examples/`` was fixed in the wave-1 commit).
- tests/.../feature/backend/{ddl,dialect,expression,protocol} plus
test_clause_pair_guard.py: exit 0.
- testsuite feature/ddl from the core worktree: exit 0.
- ruff on the changed files: clean except one finding that is at HEAD too
(``test_clause_pair_guard.py:73`` F811 on ``Sequence``).
…e protocol
Wave 3 of the plan. The code half of this landed in the previous commit
(the probes, the `_require_*` seam and the formatters that call them are one
unit, and splitting them would put a commit on record that contradicts core's
API). This commit carries the documentation and the guards.
**The probe protocol.** `supports_constraint_enforced` and
`supports_alter_constraint_enforced` took a `constraint_type` argument in the
draft. A probe answers one question -- "is this capability family present on
this server" -- and the kind is not part of it; a parameterised probe has two
call shapes depending on its own signature, which is how the draft ended up
with a switch whose answer depended on how it was called. Both are `(self)`
now, and `test_probe_takes_no_argument` reads the signature off the bound
method so a defaulted parameter cannot slip back in.
**No narrowing helper was needed, and that is the reported decision.** The
other backends put their per-kind narrowing in `_require_*` helpers called from
their own formatters. Snowflake has none to put there: the flat probes are
`False`, so core's own render gate refuses every spelling before a per-kind
test could matter. Adding a helper that nothing can reach would be dead code
with a plausible name -- exactly what the wave guide warns against. The
per-kind grammar answers still exist, as the `supports_{column,table}_*` probes
below, and the guard test pins both families so neither can absorb the other.
**Docstrings.** Every probe in `mixins/constraint.py` now ends with the
evidence level (`文档(待云验): grammar-only, not server-verified`), and the
module header states it once for the file: there is no Snowflake instance, so
nothing there is a measurement and a rendering assertion is not one either.
The paragraph that named `_call_constraint_capability` -- deleted from core in
P1 -- is rewritten: the render path reads declared fields and consults the flat
probe, and the private hook no longer raises a kind `ValueError`. The three
places where the draft's grammar reading and this backend's render decision
differ are argued in the module docstring under "Modifier policy", and the
per-probe docstrings point back at it.
**New: `test_snowflake_constraint_switches.py`.** Three obligations, each
walked over the full switch table:
- **value** -- every declaration's answer, including the two families that
disagree on purpose (`supports_constraint_enforced` False for the render,
`supports_{column,table}_enforced_{primary_key,unique,foreign_key}_constraint`
True for the grammar, CHECK False at both levels);
- **locality** -- `test_switch_is_declared_by_a_snowflake_mixin` and
`test_resolved_switch_is_owned_by_this_backend` require the answering class to
be inside this package, and the MRO guard asserts `SnowflakeConstraintMixin`
precedes `ConstraintMixin` and `DDLColumnMixin`. A switch that fell back to a
core default could have its value moved by another repository with nothing
failing here;
- **probe shape** -- `(self)` and nothing else, on the bound method.
Plus the render assertions: every column and table kind spells its standard
form, `MATCH` renders (the one inherited answer that was refusing real
grammar), naming is gated by flipping the probe through a subclass rather than
by observing the prefix alone, EXCLUDE is refused by name, and each modifier
spelling -- `ENFORCED`, `NOT ENFORCED`, `DEFERRABLE`, `NOT VALID`,
`ALTER CONSTRAINT ... ENFORCED`, `VALIDATE CONSTRAINT` -- is asserted as a
refusal, so the fail-closed policy is a test and not a comment.
`test_add_column_if_not_exists_restriction_is_kind_based` walks all four branch
classes the `IF NOT EXISTS` guard refuses, so the migration off the deleted
enum cannot have silently emptied that set.
**not server-verified.** Nothing here is measured. These are rendering
assertions over grammar-derived declarations; a live Snowflake run would still
have to confirm every value, which is what the `待云验` markers say.
Verification:
- tests/.../feature/backend/test_snowflake_constraint_switches.py: exit 0.
- tests/.../feature/backend: exit 0.
- testsuite feature/ddl from the core worktree: exit 0.
- ruff on the new file: clean.
Core split the one asterisk into two by where it sits in the grammar (spec 4.10): `AllColumnsWildcard` for the select list and RETURNING, `RowCountWildcard` for `COUNT`'s argument. This backend had no `format_wildcard` override to rename, so the wave is two Snowflake-only spellings that core leaves to supporting backends by design. **`SnowflakeRowCountWildcard` (`COUNT(<alias>.*)`).** Core's `RowCountWildcard` accepts no relation by contract, because standard `COUNT(*)` never qualifies the asterisk. Snowflake does qualify it, and its reference defines the result as a different number -- "the number of records that don't contain any NULL values" against `COUNT(*)`'s "total number of records" -- so it is a derived node, not a flag. Nothing in core changes. **`SnowflakeAllColumnsWildcard` (`* ILIKE` / `* EXCLUDE`).** Both positions accept a filter on this server: `* ILIKE '<pattern>'` and `* EXCLUDE <col> | ( <col>, ... )`. The node extends core's, carries one filter or the other, and renders the asterisk half by handing a plain `AllColumnsWildcard` to core's formatter -- so the qualifier rule (the alias when the query uses one, the qualified name otherwise) is core's and cannot drift. A filter with both keywords is refused at construction: the SELECT grammar states the two "can't be combined", and the COUNT page repeats it. **The mixin.** `SnowflakeWildcardMixin` overrides both formatters, class-tests the argument, and delegates everything else -- no `getattr` probe on `ilike`, because core's node has no such attribute and rule 9.8 forbids asking. A bare `*` and a bare `COUNT(*)` therefore render exactly as before; the tests assert that byte for byte. No capability probe is added: the filter exists only on this backend's own nodes, so a probe would answer a question no caller can ask. **Two limits, recorded rather than worked around.** - Core's shared `format_function_call` special-cases a `RowCountWildcard` argument to the bare asterisk *before* consulting a formatter, so `FunctionCall(dialect, "COUNT", SnowflakeRowCountWildcard(...))` still renders `COUNT(*)`. The qualified spelling is reachable through the node's own `to_sql()`; closing the factory gap is a core change, out of this branch's scope, and PostgreSQL's derived row-count wildcard is in the same position. `test_the_aggregate_factory_masks_the_qualified_form` pins the behaviour so a core change moves that test rather than silently changing what Snowflake emits. - `REPLACE` / `RENAME` are the SELECT grammar's other two `*` modifiers and are **not** modelled. They take expressions and aliases rather than a filter, so they are a different node's worth of work; recorded here so the omission is a decision rather than an oversight. **not server-verified.** Transcribed from `docs.snowflake.com/en/sql-reference/functions/count` (including its worked `COUNT(n.*)` example) and `.../sql/select` (whose own examples -- `* ILIKE '%id%'`, `* EXCLUDE department_id`, `* EXCLUDE (col1, col2)`, `table_a.* EXCLUDE column_in_table_a` -- are the test fixtures). No Snowflake instance is reachable; a rendering assertion is not a measurement. Verification: - tests/.../expression/test_snowflake_wildcards.py: exit 0. - tests/.../expression/test_expression_roundtrip_all.py: exit 0 (the two new classes are collected from this package and render). - tests/.../feature/backend: exit 0. - ruff on the changed files: clean.
The testsuite's S3 adaptation is on `fix/column-constraint-gates` (its `117da8b` and `11b0f6c` close the shared DDL contract), and this repository's own constraints are migrated in the same run of commits. All three jobs now install the testsuite from that branch instead of `release/v1.0.0.dev17`, matching the core's own `e5e06895`. The core install line was checked in all three jobs and already points at `fix/column-constraint-gates`; unchanged.
`SQLValueExpression` became `SQLValue` in the core's W3b rename; the coding standard's inheritance list and its worked example still named the old class, which would send a reader to an import that no longer exists. Backend adaptation, same wave as the src/tests rename (`e23e55b`). Only the two name occurrences change; the class name in the example, the section and the rule it states are untouched.
Core renamed CaseFunction to CaseExpression and moved the PIVOT / UNPIVOT node formatters from format_pivot_expression / format_unpivot_expression to format_pivot / format_unpivot, so Snowflake's trim mixin now annotates TrimExpression and the round-trip matrix pins the new formatter names -- Pivot and Unpivot still have no Snowflake renderer, because the dialect spells the clause from its own SnowflakePivotExpression fields. The matrix also registers special constructors for ArithmeticOperation and SetOperation: both validate op against a closed word set at construction (add/sub/mul/div/mod and union/union_all/intersect/except), so the introspective guess of a bare string now raises ValueError before any formatter is reached. The serialization class index moves from the ExpressionRegistry class to auto_register_builtins / registered_expression_classes.
…x functions serialization's ExpressionRegistry class is gone; the deserialization class index is now three module-level functions. Snowflake's data types and UDT expressions register through register_expression_class, and the type-DDL test resolves a name through registered_expression_classes() rather than ExpressionRegistry.lookup. This index is not the rendering registry in expression.registry -- the two are separate mechanisms. Also drops the supports_set_operation_for_update assertion in the same commit: that probe gated the for_update_clause trailing parameter the core SetOperation node used to carry, the parameter is abolished, and core no longer declares a default for it. Snowflake's mixin never declared its own, so the test asserted a method the dialect does not have.
…attr
Fifteen probes dropped, in the five mixins the wave inventory lists:
ddl_database (transient, clone), ddl_type (comment, cascade, restrict,
definition_kind, action_kind), dml (overwrite, plus the docstring's
wording), file_format (the enum double-form on type_), materialized_view
(or_replace, if_not_exists, cluster_by, comment, query).
Each site falls into one of three shapes:
* fields declared only by this backend's expression (transient/clone,
the TYPE comment, cascade/restrict, overwrite, the four materialized
view options): the formatter now isinstance-dispatches to the
Snowflake expression and reads the field there, instead of probing
whatever arrived. The core statements this formatter also accepts
declare none of those fields, so their rendering is unchanged.
* fields declared abstractly by the core ABC (definition_kind,
action_kind): read directly; the old probe's class-name fallback only
turned a contract violation into a mislabelled refusal.
* the enum double-form on type_: the current core pattern
(expr.type_.value if isinstance(expr.type_, Enum) else expr.type_).
Behaviour tightening, listed per the wave: a bare object() handed to
format_type_alter_action now surfaces AttributeError('action_kind')
instead of an UnsupportedFeatureError built from the probe's fallback;
the test now pins the honest failure and keeps the refusal gate with a
well-formed but unregistered action. A hand-assigned or_replace or
cluster_by on a core CreateMaterializedViewStatement no longer leaks
into that statement's rendering -- those were never its contract, which
is the documented interop the 2026-10-12 probes implemented by
accident.
Also removed a pre-existing unused UnsupportedFeatureError import from
ddl_database.py so the touched files lint clean.
Evidence: rg '(getattr|hasattr)\(\s*(expr|node|clause|item)\b' src now
returns zero matches. Targeted suites, all exit 0:
feature/backend/test_snowflake_database_ddl.py, feature/backend/ddl/
test_type_ddl.py, feature/backend/dml/test_insert_overwrite.py,
feature/backend/extensions/test_file_format.py, feature/backend/
extensions/test_undrop_clone_materialized_view.py (66 passed), plus a
package import smoke; ruff clean on all six changed files.
…s index
Core 8d474dc8 abolished the deserialization class index: an artifact is a
bare {kind, params}, resolution goes through a registry the reader
supplies, and 'writing needs no registry'. The testsuite followed in
e05edb0 by dropping the shared round-trip harness with the direction
'round-trip coverage moves to the owning repository'. This repository
still imported both, so the dialect could not even be imported.
Migration surface (old -> new):
- expression/types.py and expression/ddl/type.py no longer self-register
their classes at import. register_expression_class is gone; the
framework's default registry walks only the packages the framework
itself ships, and a reader that wants a Snowflake kind registers it.
Both files keep exporting their classes; nothing in src reads
artifacts, so no runtime behaviour depends on the deleted loops.
- mixins/collation.py: CollateFunction replaced its open
**collation_options bag with a named schema parameter, so the refusal
of schema-qualified collations now reads that field directly instead
of dying on an attribute core removed.
- the round-trip harness is vendored at
feature/backend/expression_roundtrip_harness.py from
python-activerecord-testsuite@4ef2551, following the mysql worktree
precedent: register_all now hands classes to the registry's
register_node, and register_enums_and_dataclasses covers the backend
enums that ride inside artifacts under their short class names.
- test_expression_roundtrip_all.py collects and registers through the
vendored harness; its integrity test now checks kinds against the
assembled registry instead of FQNs against the dead index. Three
content repairs that the core waves had already invalidated:
values.TypedValue is registered as the root-of-the-tree unmodelled
entry beside TableSource (its format_method is the empty 'format_',
which no dialect can declare), and the ArrayType special constructor
spells element_type= keyword-only, as the core class now requires.
- test_type_ddl.py assembles the classes it round-trips into the
framework's default registry at import and pins kind resolution
instead of FQN index membership. Its per-FQN assertion is replaced by
resolve_node(kind()) is cls for the same 24 classes.
- test_collation_expression.py drops the keyword-collation test: core's
collate() no longer accepts a keyword option at all, so the refusal
it asserted is now structural (a TypeError at construction) rather
than a rendering answer.
Deliberately left for the matrix reconciliation wave, which owns the
verdict tables: the UNCONSTRUCTIBLE drift from the value and function
waves, the unmodelled format_edge_table_definition, and core's newest
_Annotation class. The matrix reports these honestly now that it can be
imported again; it could not run at all before this commit.
Tests (targeted, centralized venv, exit 0):
- feature/backend/ddl/test_type_ddl.py: 15 passed
- feature/query/test_collation_expression.py: 4 passed, 2 skipped
(integration, no server -- this repository reaches no warehouse)
- feature/backend -k 'typed or literal': 33 passed
- TestMatrixIntegrity: pinned-non-render and unmodelled-set checks
green for everything this commit touches; the three named drift
failures above are the reconciliation wave's registration work
- ruff: clean on every changed file
…pellings
Wire TypedValueMixin into the dialect ahead of the generic mixins, the
same composed order sqlite uses, with the new SnowflakeTypedValueMixin
in front so the two spellings Snowflake owns win by position.
Tier 2, each spelling transcribed from the official documentation (this
repository reaches no Snowflake instance, so both new files carry the
not server-verified header and every docstring cites the page rather
than a measurement):
- binary: TO_BINARY('0102', 'HEX'). The one-argument form reads the
BINARY_INPUT_FORMAT session parameter, which a dialect must neither
read nor assume; naming 'HEX' keeps the literal deterministic across
sessions. Uppercase hex matches what Snowflake itself prints. The
digits go through format_literal, so the quoting stays the base's.
- UUID: the bare quoted canonical text -- 'the ANSI literal form of
UUID is supported as input'; there is no ::uuid suffix to add.
- INTERVAL stays unimplemented on purpose. The interval constant is a
date-arithmetic operand Snowflake refuses to insert into an interval
column, the interval data types take a string whose shape depends on
the column's qualifier, and a timedelta cannot spell year-month
anyway -- so the base's explicit NotImplementedError is the answer,
and this mixin defines no format_interval_value, which is what makes
that refusal structural.
Tier 1 audited against the grammar and left to the base: NULL and
TRUE/FALSE keywords, numeric text verbatim, strings with doubled single
quotes, quoted ISO date/time/timestamp; the base's NaN/Inf refusal is
Snowflake's answer too, its FLOAT special values requiring quotes.
Tests (rendering assertions, not measurements): data positions bind the
host value behind %s; DDL positions assert the exact literal per kind
through the class-level inline_params lift, one Ddl*Value subclass per
kind exactly as core's own DdlLiteral performs; the INTERVAL refusal
asserts the base's NotImplementedError; tier 1 is pinned per spelling
so a base change that breaks Snowflake's grammar fails here rather than
in the matrix; and the MRO tests pin that the local mixin wins the two
overrides while the base answers everything else.
Targeted runs (centralized venv, PYTHONPATH=src;tests, exit 0):
- feature/backend/expression/test_typed_values.py: 20 passed
- feature/backend -k 'typed or literal': 33 passed
- ruff: clean on all four files
… VALUES cells ARRAY_AGG is the one call point whose function core has classed: the semi-structured factory now returns the classed ArrayAggregateFunction node instead of a bare FunctionCall, delegating to core's own array_agg factory. The node keeps the exact spelling the call rendered -- ARRAY_AGG(operand) -- and gains what the call could not carry: DISTINCT, the window clauses an aggregate takes, and an artifact naming array_aggregate_function rather than a generic call. A bare string operand still becomes a Column, as before. Every other call point stays a FunctionCall on purpose, because core has not classed its function: the ST_* geospatial set, ARRAY_CONSTRUCT / ARRAY_SIZE, the OBJECT_* set, PARSE_JSON / TRY_PARSE_JSON, TO_VARIANT / TO_ARRAY / TO_OBJECT, and FLATTEN. ARRAY_SIZE in particular is not ArrayLengthFunction: Snowflake's spelling is ARRAY_SIZE and its LENGTH is a different question, so pinning it to the classed node would render SQL Snowflake rejects. array_construct and parse_json are pinned as the stay-side witnesses in the new rendering test, so a future classing in core changes that file rather than drifting silently. char_length audited, no override: Snowflake's LENGTH counts characters, which is the unit CharLengthFunction records, and the base formatter already spells LENGTH; the backends that must override are the ones whose LENGTH answers a different question (bytes on MySQL / ClickHouse, UTF-16 units on SQL Server). INSERT audited, delegation confirmed, cells pinned: both paths of SnowflakeDMLMixin.format_insert_statement hand the statement to the core renderer -- the plain path outright, the overwrite path by rewriting the prefix of the core's answer -- so the expression-versus-host-value judgment lives in the one place core keeps it. New targeted cases pin that bare scalar VALUES cells bind behind %s on both paths, and that a mixed row binds in written order. Tests (targeted, centralized venv, exit 0): - feature/backend/functions/test_semi_structured_functions.py: 7 passed (new file, not server-verified header: rendering assertions only) - feature/backend/dml/test_insert_overwrite.py: 9 passed - ruff: clean on all three files
SnowflakeCreateMaterializedViewExpression caught the base statement's options in a **core_kwargs catch-all and forwarded them blind. The bare- node artifact reader validates parameters by name (spec 4.11.4) and refuses a **kwargs-declaring kind outright -- a parameter whose identity depends on how many extras arrived cannot be matched on read -- so the kind could not be deserialized at all. The four options the base declares (tablespace, with_data, no_data, storage_options) are now forwarded by the names it knows, which is the same forwarding the catch-all performed and the same refusal Snowflake's formatter gives them: the engine has none of these clauses. get_params is untouched -- it writes the core vocabulary only, and the named options never entered the wire; the docstring's stale reference to the catch-all is updated to the shape that remains. The matrix's coverage walk surfaced this: it round-trips every class the two packages define, and the new reader named the kind and the section it violates where the old index read it back without asking. Tests (targeted, centralized venv, exit 0): - feature/backend full matrix rerun green with the fix in place (see the matrix reconciliation commit for the full-file evidence) - ddl/test_object_kind_refused.py, extensions/ test_undrop_clone_materialized_view.py, test_clause_pair_guard.py, test_snowflake_ddl_improvements.py: 202 passed - ruff: clean
…nction waves Reconciled against core e57c6702 (the HEAD at reconciliation time), which the drift the integrity tests named: the value wave's classes, the extract split, the scalar string set, the CustomType keyword, the EdgeTable definition split, and the newest annotation base. New special constructors -- a real value where the introspective guess is a lie, so the matrix asks the render question instead of skipping: - the seven typed values whose host types have no path from the guess's 1 (Binary, Boolean, String, Date, Time, Timestamp, UUID) get real host values and render through the same bind path the five the guess does build already take (NULL carries none; Integer and Float are numbers; Decimal and Interval normalise it). - ExtractFieldExpression, the field-named spelling the wave split out of ExtractExpression, and _ExtractBase, the shape both spellings share: the guess's 'x' is not a datetime field. Both render through the same format_extract_expression the classed spellings did. - the scalar string set (CHR, LEFT, RIGHT, SPACE, SUBSTRING, OVERLAY): each normalises its counts through IntegerValue, and the guess's 'x' is not an int. All six render Snowflake's documented spellings. - CustomType's factory spells raw= keyword-only, as the core class now requires; the stale comment about the empty-string default it used to lean on goes with it. Retired from UNCONSTRUCTIBLE, both because the constructor now builds them: ValidateConstraint (core made constraint_name a positional the guess fills) and EnumType (the guess now supplies a real values list). EnumType then had a verdict to give and gets one: the dialect's own refusal, TypeError 'does not support the generic type', naming VarCharType as the spelling to use -- pinned in LEGITIMATE_NON_RENDERS beside the other generic types Snowflake spells differently. _Annotation joins the same table: it names a formatter but carries no surface, which is where the alias the formatter reads comes from, so the base constructs and its render dies on the missing attribute while every concrete annotation renders. UNMODELLED_FORMATTERS: format_edge_table_definition joins the SQL/PGQ group -- core split the edge table's declaration into its own EdgeTableDefinition node and the need moved with it; format_edge_table is dropped, named by nothing since the split. The four verdicts, exhausted: renders is the default the green run asserts (the twelve value kinds, the extract family, the scalar string set, the aggregates, the math set, the session identity trio and the wrapped-result classes all render); the unmodelled and non-render tables above name every refusal by method and by class; the five UNCONSTRUCTIBLE entries remain, each with its reason, and the integrity tests pin all three tables in both directions. Full-file evidence (centralized venv, PYTHONPATH=src;tests): - test_expression_roundtrip_all.py, entire file: 910 collected, 900 passed, 10 skipped (the five pinned UNCONSTRUCTIBLE entries, twice each), exit 0 - TestMatrixIntegrity: 10/10 green, including both-directions pins on all three tables and the coverage report - ruff: clean
Core replaced the typed-value node family with one `Literal(dialect, value,
inline=)` whose kind is read off the host value, and folded the tier-2
spellings into `dialect.format_literal`. `TypedValueMixin` and the twelve
`*Value` classes are gone from `dialect.mixins`, so this backend's dialect
raised `ImportError` on import.
Migration (mechanical, per guide §10.1):
* `mixins/typed_value.py` -> `mixins/literal.py`, class
`SnowflakeTypedValueMixin` -> `SnowflakeLiteralValueMixin`, overriding
`format_literal` instead of `format_binary_value` / `format_uuid_value`.
Only the two spellings this backend differs on are branches; everything
else calls `super()`. The binary branch renders `TO_BINARY('<hex>', 'HEX')`
through `self.format_literal(hex_text)`, so the quoting stays the base's.
* INTERVAL stays structurally unimplemented: no `timedelta` branch, so the
base's explicit `TypeError` is the answer.
* The mixin moved to the first position of the dialect base list, ahead of
`SQLDialectBase` -- the one base that defines `format_literal`. A mixin
placed after the class it overrides never wins (rules §11.1).
* `TypedValueMixin` dropped from the base list and from the import.
Tests: `test_typed_values.py` -> `test_snowflake_literal_values.py`,
rewritten against `Literal` (bind position / inline position per kind). The
tier-1 audit and the INTERVAL refusal are unchanged in substance and keep
their "not server-verified" record -- the measurements do not expire with
the architecture. A new case pins that NaN / +-Inf never reach SQL text.
Evidence: `test_snowflake_literal_values.py` 23 passed; ruff clean on all
five touched files.
Kind list generated with the core script on a temporary copy whose BACKENDS names this package and SnowflakeDialect (registering a backend there is a change to the core repository, so it is reported, not committed here): core kinds 357, snowflake override 0, snowflake custom 33. Override zero is the rule that matters most: a backend may replace the rendering of a kind core ships, never the node class behind it -- two classes under one kind leave the deserializer no way to choose. This backend adds nodes and overrides formatters; it replaces nothing. Read per class, the rules section 11.1 audit holds: SnowflakeCreateMaterializedViewExpression derives from CreateMaterializedViewStatement, SnowflakeCreateDatabaseExpression from CreateDatabaseStatement, SnowflakeInsertExpression from InsertStatement, SnowflakeCreateTableOptions from CreateTableOptions, the three type DDL expressions from Create/Alter/DropTypeStatement, SnowflakeObjectTypeDefinition and SnowflakeScalarTypeDefinition from TypeDefinition, the two comment actions from TypeAlterAction, and each Snowflake<Type> from its core type -- every class that specialises a core node inherits it. The concepts core has no node for (STAGE, PIPE, TASK, STREAM, WAREHOUSE, FILE FORMAT, SHOW, UNDROP, the Snowflake TYPE DDL, VARIANT, IDENTIFIER, SAMPLE, PIVOT) stand on the bare bases, which the clause exempts. Overrides that are never reached: measured 19, all `_is_runtime_protocol` -- an attribute every runtime_checkable Protocol re-declares for itself, answered by the first protocol in the base list whoever defines it. The named protocol classes are re-exported for isinstance, not as bases, so no backend method is shadowed; every real override (529 backend method definitions checked) is answered by the backend class that declares it. Note on the 33: they are the nodes that declare a format_method. The other backend nodes render through their own to_sql() and so have no kind of their own yet -- the same shape mariadb's audit recorded for its SHOW family, and converting them is a rename of their dialect entries rather than an audit finding. Recorded, not changed. The audit is now an executable assertion, test_kind_lineage.py: zero kinds core already ships, every declared formatter present on the dialect, every custom kind naming its backend first, and the literal mixin ahead of the base whose format_literal it replaces. Evidence (central venv, PYTHONPATH=src;tests): test_kind_lineage.py 5 passed, exit 0 ruff clean on the new file
…verrides
Kind hygiene per expression-kinds.md section four and the enum clause of
expression-dialect §11.3, audited against this backend's 64 expression
classes and 15 closed-set enums.
* **The enums take the org spelling.** All 15 were bare ``Enum`` over string
values, so a member was not a string and ``str(member)``/an f-string wrote
``ClassName.MEMBER`` -- which is what the SQL formatters read and what the
artifact writer refused by name ("X is not one of the value kinds this
package can write"). Each is now ``(str, Enum)`` with
``__str__ = str.__str__`` pinned, the spelling mariadb/mysql/postgres use.
Members are unchanged, so every rendered spelling is unchanged.
* **The constructor puts the member back.** A member now travels as the word
it carries, and the reader only passes parameters by name (§10), so the
conversion belongs in the constructor: new ``expression/coercion.py``
``as_member`` accepts a member or its word, refuses anything else by
parameter name, and is applied at the nine nodes that hold one
(SHOW object_type/in_scope, SAMPLE form/samplingMethod, ALTER PIPE/TASK/
WAREHOUSE and COPY INTO mode, CREATE STREAM/UNDROP object_type, routine
language/execute_as/routine_type). The alternative -- an isinstance check
that refuses the word -- leaves each of these nodes unable to read back the
artifact it wrote, which is the defect core's own datetime and graph enums
are pinned for, not something a backend should copy.
* **No purpose-less alias, no missing nature suffix, no unread parameter.**
Checked rather than assumed: kind, class name and format entry agree for
every class that declares a formatter (measured: 0 disagreements); the
parameters that read as unused are base-class forwards or normalised
spellings, not silent drops.
Enum spellings that were read off ``.value`` keep reading ``.value``; the new
``__str__`` only changes what an f-string writes, and no formatter relied on
the old repr-shaped spelling.
Evidence (central venv, PYTHONPATH=src;tests, exit 0):
feature/backend/snowflake/test_show.py
feature/backend/query/test_sample_tablesample.py
feature/backend/extensions/test_stream_task_pipe.py
expression/test_kind_lineage.py, expression/test_snowflake_literal_values.py
110 passed
ruff (line-length 120) on the expression package and collation.py: clean
expression-dialect §12 splits the two kinds of test: an expression case constructs and renders, a case that needs a real server belongs to the scenario-driven integration area. The audit's result is that **nothing had to move** -- measured, not assumed: no module under `feature/backend/expression` imports `snowflake.connector`, a backend class or a connection config, and none calls `connect` / `execute` / `cursor`. The live-leg work this release added (the literal spellings) is pinned as rendering assertions with a "not server-verified" record, which is exactly the split §12 asks for; there is no read-back case to relocate. The check is now executable, in `test_expression_discipline.py`, so a future case that does reach a warehouse fails there instead of quietly dragging a connection into the area. §12.1's parallel default is now configured, per the mariadb/mysql precedent: the worker count is read before conftest loads, so it cannot come from one. `tests/plugins/expression_xdist.py` is a repository-local plugin registered from `pyproject.toml` (`-p plugins.expression_xdist`) that appends `-n auto --dist=loadgroup` only when every target is inside the expression area, never overrides an explicit `-n`/`--dist`, refuses `--dist=loadscope`/`loadfile`, and does nothing when pytest-xdist is absent. loadgroup is the scheduling the rules name: only a case explicitly marked `xdist_group` runs serially, and none here is. Evidence (central venv, PYTHONPATH=src;tests): test_expression_discipline.py 4 passed, exit 0 test_snowflake_literal_values.py under the plugin: "bringing up nodes", 23 passed, exit 0 -- the default really engages ruff clean on the plugin and the new test file
…rtifact Baseline: core `4774c502` (`LiteralKind` + `Literal`, `LikeOperator`, the `*Action` renames, NaN/Infinity kinds, the `_need_quote` checks). The matrix is now green: 436 classes collected, 431 constructible, 320 rendered, 27 legitimate non-renders, 50 unmodelled formatters, 34 unsupported, 5 pinned unconstructible. What changed, and why each is a measurement rather than a concession: * **The value nodes are gone, so their rows are.** Core folded the twelve `*Value` classes into one `Literal` whose kind is read off the host value; the seven registered constructors for them had nothing left to build. The one remaining value node needs none: the guess's `"x"` *is* a `string_value`. Their per-kind rendering stays pinned in `expression/test_snowflake_literal_values.py`, which is where the dialect's tier-2 spellings live. * **The `*Action` renames followed through, in the matrix and in three sibling test files.** `AddColumn`/`DropIndex`/`AddIndex`/`AddTableConstraint`/ `ModifyColumn`/`AlterConstraint` and the rest are `*Action` in core now; the registration keys had become dead code -- `make_instance` matches by `endswith`, so a stale key silently falls through to the introspective guess, which is exactly how these classes were being reported unconstructible. * **`EdgeTable` -> `EdgeTableDefinition`** in the harness factory, following core's split of the graph edge definition out of the edge table object. * **The harness dropped `register_enums_and_dataclasses`.** Core removed the enum/dataclass sub-registries along with the artifact markers they existed for; the call was raising `AttributeError` at import. It also takes the inline literal for `_literal`/`_comparison`, because a DDL condition accepts no bind parameters since core stopped lifting binding literals. * **`SERIALIZATION_GAP` is new, and it is a measurement.** 24 classes cannot round-trip through all three channels, and every one of them fails inside core's writer or reader -- not in this backend. The table pins, per class, which channels fail and with what witness, in four groups: the writer refuses a non-scalar parameter (7), the constructor refuses its own wire form (1), the constructor does not normalise the word back into the member (7), and the XML channel spells a str-enum member as `str(member)` (9). A class that starts failing *without* a row fails its own assertion; a row whose class starts round-tripping fails `test_pinned_serialization_gap_really_fails` and is deleted. No row names a `Snowflake*` enum: those inherit `str` and their constructors coerce through `expression/coercion.as_member` (K2), which is the backend's own half of this problem solved rather than pinned. * **`DataType`'s pin was stale.** The root of the type tree no longer says "does not declare a valid generic type name"; it asks the dispatcher for `format_data_type_None`, which refuses as `does not support the generic type`. Same type, new message -- the pin now names the message that is actually raised. * **The typed-value root left `UNMODELLED_FORMATTERS`.** Its `format_` entry existed for the twelve value classes' shared base; with the family gone no class asks for it, and the bidirectional pin said so. * **`ComparisonPredicate` got a constructor.** `op` normalises through the operator enum at construction, so the guess's bare `"x"` is refused there. Two columns over a table is the shape every DDL position also wants. The SQL round-trip assertion now skips the channels a class has a gap row for: a node whose parameter came back as a `str` cannot be asked to re-render, and asserting that it fails would be restating the gap rather than testing the renderer. Evidence (central venv, PYTHONPATH=src;tests, exit 0): expression/ (whole area, 4 files) + test_clause_pair_guard.py + test_snowflake_constraint_switches.py + ddl/test_alter_table_if_exists.py 0 failed the matrix alone: 436 collected / 431 constructible / 320 rendered ruff (line-length 120) on every touched file: clean Pre-existing failures left alone (measured on the untouched tree, not caused by this branch): 9 in `introspection/test_introspection.py` and 2 in `types/` -- they fail identically with this work stashed.
…ions
The two user-mandated rules, checked across this repository:
* **An expression collects every parameter at construction; no fluent
builders.** `rg '\.with_[a-z_]+\(' src tests` finds nothing. No node here
grows a parameter after the fact.
* **SQL text belongs to a dialect formatter.** Two sites were spelling it
outside one, and each is a real violation rather than a style note:
1. `field.py` built `SELECT <seq>.NEXTVAL AS next_id` as an f-string and
handed it to `execute`. Snowflake reads a sequence's next value as a
pseudo-column on the sequence -- neither `NEXT VALUE FOR` nor PostgreSQL's
`nextval('seq')` -- so it is a concept with no core node. New
`expression/sequence.py` `SnowflakeNextvalExpression` carries the sequence
name and the alias, and `mixins/sequence.py` renders it through
`format_snowflake_nextval_expression` with the name quoted once by
`format_identifier`. The mixin now knows the spelling; the field mixin
collects a name and reads back a value.
2. `expression/ddl/warehouse_options.py` rendered its own `KEY = value` tokens
with `dialect._escape_sql_string`, reaching into the dialect's escaping
helper to build SQL from outside a formatter. The rendering moved to
`SnowflakeWarehouseMixin.format_warehouse_options_expression` -- the same
mixin that already spells the identical property set for CREATE and ALTER
WAREHOUSE, which is where the duplication came from -- and the strings now
go through `format_literal`, so the quoting is the dialect's one answer.
Both are rendering-preserving: `'WAREHOUSE_SIZE = 'MEDIUM''` and
`'My Seq'.NEXTVAL` are what the tests already asserted.
What is left as it is: the remaining `f"..."` SQL in `mixins/*` is `format_*`
bodies, which is where the rule says SQL text belongs. The one exception is
the `COMMENT = '<escaped>'` family, which deliberately uses
`_escape_sql_string` rather than `format_literal` because a comment is text the
statement grammar takes unquoted-vs-quoted by position; it is rendering, in a
formatter, and is not hand-assembled statement SQL.
Evidence (central venv, PYTHONPATH=src;tests, exit 0):
expression/ (5 files) + snowflake/ + ddl/ + test_clause_pair_guard.py
0 failed; new test_snowflake_nextval.py 5 passed (node, alias, quoting,
empty-name refusal, and the node inside a QueryStatement)
ruff clean on every touched file
First CI run (`39480526873`) failed on every Python leg, all in the same
place: `ModuleNotFoundError: No module named 'plugins'`. The parallel default
this branch added registers `-p plugins.expression_xdist` from `pyproject.toml`,
and a `-p` plugin is imported by pytest itself before any conftest can arrange
`sys.path` -- so the registration needed `pythonpath = ["tests"]`, which the
repo did not declare (the local runs worked only because the shell had
`PYTHONPATH=src;tests` set). That is the CI-only half of §14 type H.
With that fixed, the rest of the suite's failures were real and are fixed
rather than papered over:
* **§16.1, fluent setters.** `introspection/test_introspection.py` configured
eight nodes with `expr.schema("MY_SCHEMA")` after construction. Core retired
the fluent setters (§16.1: an expression collects every parameter at
construction), so the attribute is `None` and the call raised
`TypeError: 'NoneType' object is not callable`. All eight now pass `schema=`
to the constructor; the same SQL, the same params. This was the last
`.schema(` / `.with_*` call in the repository (`rg` finds none now).
* **§14 type F.** `query/test_collation_expression.py` composed
`Literal(dialect, "Alice").collate(...)`. A literal is a value node and
carries no operation surface (§4.3), so COLLATE is now composed with the
`collate(...)` factory over the literal -- the same fix postgres made in
`07bbc24`, with the binding unchanged: `('%s COLLATE 'en-ci'', ('Alice',))`.
* **§14 type H/H', our own vendoring.** `types/test_data_type_parsing.py`
imported `parse_roundtrip_failures` from
`testsuite.utils.parse_contract`, and that module imports
`testsuite.utils.expression`, which the testsuite deleted in `e05edb0`. The
sweep is now vendored into this repository's harness (the same 120-line
function mysql vendored) and the test imports it from there. Everything the
sweep asserts is unchanged.
* **A stale count, measured.** `test_the_family_is_not_empty` claimed 18
Snowflake type classes; there are 17. The count has been wrong since the
commit that wrote it -- `SnowflakeDataTypeMixin` was removed as a class when
every type started inheriting its core type directly, and the expected number
never followed. Corrected to 17, with the arithmetic in the docstring.
Evidence: CI's exact invocation, from a shell with **no** PYTHONPATH:
`pytest tests/ -k "not integration" --ignore=tests/snowflake_fakesnow` --
0 failed, exit 0 (the plugin loads and brings up workers)
ruff clean on every file this commit touches (the 18 warnings remaining in
`introspection/` and `types/` are pre-existing and untouched)
CI's unit-only leg runs `pytest tests/` from the repository root, so the configured `pythonpath = ["tests"]` put *that* directory on the path rather than its parent: `plugins` resolved nowhere and the plugin registered by `addopts` failed to import before a single test was collected. The other backends register the same plugin without that setting because CI exports `PYTHONPATH=src:tests`, which is what makes `tests/plugins` addressable as `plugins`. Removing the setting restores the layout the plugin name assumes; verified by collecting the unit-only selection with the same PYTHONPATH CI uses.
The 3.8 leg of run `39483327157` was the only one left red, and it failed at `-p plugins.expression_xdist`: `ModuleNotFoundError: No module named 'plugins'`. The cause is the pytest version, not the plugin. That leg resolves to pytest 8.3.5 (the newest 8.x with a 3.8 wheel); every other leg resolves to 8.4.x. On 8.3.5, `-p` is processed during argument preparse, before the `pythonpath` ini option is applied, so the option added in the previous commit covered 8.4+ only. The 3.9 leg passing with the same tree is the control. The three test steps now export `PYTHONPATH=src:tests`, which is how the sibling backends' workflows do it (mysql, mariadb and clickhouse all export it). `pythonpath` in `pyproject.toml` stays: it costs nothing, it makes a bare local `pytest` work without the variable, and the two agree on the same directory. Verified locally with the CI invocation and the variable set: `PYTHONPATH=src;tests pytest tests/ -k "not integration" --ignore=tests/snowflake_fakesnow` -- 0 failed, exit 0
The table comment reached for SQLDialectBase._escape_sql_string, which names the base class explicitly and therefore cannot be overridden. A backend that spells string escaping its own way had its policy dropped there. Snowflake does not override the escape today, so the rendered SQL is unchanged; the call now asks the dialect, which is what decides it.
The dialect's surface is two kinds of member: capability switches (``supports_*()``) and formatters (``format_*(self, expr)``). This change makes Snowflake's code match that, and settles what a type position holds while it is there. **A string is escaped by the dialect that renders it.** The escape policy belongs to the dialect, because SQL dialects differ in how a string literal represents a quote. The base class's private copy is gone; each dialect overrides the public one. **A type-name position holds a type.** The positions that carried a raw string guarded it with a character class in the base class, which neither matched what the engines accept nor kept the value inside its syntactic position. A position now holds a type node; a name the framework does not model becomes the framework's name node, whose name the grammar validates. A routine's type names are claimed the same way, and a routine with no return type keeps the rendering it had. **The backend's own answers are the effective ones.** Snowflake's mixin precedes the core mixin it overrides, and a test holds that in place. **The mixin list says what it is for.** The base-list convention is written down where the list is, and the function examples reach their constructors through ``functions as F``.
Core renamed the four statement-shaped nodes it still called *Expression: TransactionExpression -> TransactionStatement SetTransactionExpression -> SetTransactionStatement SQLiteAttachExpression -> SQLiteAttachStatement SQLiteDetachExpression -> SQLiteDetachStatement This repository refers to them in its dialect signatures and its tests, so the same names follow here. Nothing changes but the names.
Statement nodes are ``*Statement``; the fragments have formatters that end in the ``_expression`` the class name carries. Snowflake's PIVOT, UNPIVOT, SAMPLE, the VARIANT cast and path access are clauses, not statements, so they are named for what they are rather than for the clause a dialect spells them inside. ``format_snowflake_identifier_expression`` keeps its backend prefix on purpose. ``format_identifier_expression`` is the capability core declares for its generic ``Identifier``, and a string-taking method under that name here answered every ``Identifier`` lookup meant for a formatter. Sharing a renderer across nodes is fine; stealing a name another node dispatches on is not.
Core moved ten cross-package helpers out from behind a leading underscore: they are called from this backend, so the underscore was claiming a scope the names never had. Each old name had exactly one new name, checked against every public definition and call site in the ten repositories first, so nothing is shadowed by the rename.
The account-object statements were built by string concatenation in ``introspection/queries.py`` (line 55 ``"SELECT DATABASE_NAME, ..."``, line 95, line 200) plus ``mixins/introspection.py:92`` for ``format_database_info_query_statement``. Four of them -- databases, schemas, warehouses, storage metrics -- were new: no rule existed to stop them, and they each re-implemented the clause assembly the dialect already owns. ``introspection/queries.py`` is deleted. Its four builders became statement classes in ``expression/introspection.py`` (``SnowflakeDatabaseListQueryStatement``, ``SnowflakeSchemaListQueryStatement``, ``SnowflakeWarehouseListQueryStatement``, ``SnowflakeTableStorageMetricsQueryStatement``) whose ``to_sql()`` renders through ``format_*_query_statement`` methods on the dialect mixin. The projection is a list of ``Column`` nodes, the filtering a conjunction of ``ComparisonPredicate`` / ``InPredicate`` / ``LikePredicate`` / ``IsNullPredicate`` nodes, the ordering an ``OrderByClause``, and the whole question a ``QueryStatement``; ``SHOW WAREHOUSES`` is rendered by the existing ``SnowflakeShowStatement``. The remaining formatters in ``mixins/introspection.py`` -- database info, table list, column info, index info, foreign key, view list/info, triggers -- were also text; they are now built from the same tree nodes through one ``_catalog_query`` helper, so the "INFORMATION_SCHEMA objects are spelled unquoted" rule is stated once (and why it is correct, per the Information Schema topic) instead of being repeated in eight string literals. The rendered SQL is byte-identical to the concatenated form except for the row-source aliases, which now read ``AS m`` / ``AS t`` / ``AS tc`` / ``AS kcu`` (the dialect's separator) rather than the bare ``m`` / ``t``. Public surface (sync and async, identical names and signatures): * ``list_databases()`` * ``list_schemas(database=None)`` * ``list_warehouses()`` * ``get_table_storage_metrics(table_name, schema=None)`` And three CLI subcommands -- ``databases``, ``schemas``, ``warehouses`` -- dispatched through ``backend.introspector`` and rendered by the existing output provider. BREAKING CHANGE: ``rhosocial.activerecord.backend.impl.snowflake.introspection.queries`` no longer exists. ``SHOW WAREHOUSES`` output is documented in lowercase column names where ``INFORMATION_SCHEMA`` uses uppercase, so ``list_warehouses`` normalizes row keys before reading them.
The introspect subcommand accepted a list of types and nothing in the tree said what each one reads or where its answer comes from. This adds a README to each backend's cli package: the questions with the catalogue each reads, the options, the notes that keep an answer from being misread (estimate lengths, upper-casing, version gates, capabilities that are refused rather than empty), and examples. The tables are generated from the code, not from memory -- every entry was checked against the statement it dispatches to.
…ties Eight switches were resolving to core's default of False while this backend introspects fully -- catalog views, sys.objects for routine bodies, and a CLI covered by tests against a live server. A caller gating on them would have concluded SQL Server has no introspection at all. Declared one per family, each naming what it reads. Placed in a mixin of its own and registered before the core bundle each replaces, because a mixin placed after the class it overrides never wins -- the ordering the dialect file already documents.
The core helper is gone: core no longer arbitrates a per-backend introspection vocabulary. Restore the hand-written INTROSPECT_TYPES as the type argument's choices, so --help lists exactly what this backend declares. The connection already carries the schema (--schema / SNOWFLAKE_SCHEMA), so a run must not name it twice: remove --introspect-schema and pass schema=None, letting the introspector resolve the configured schema.
…tract The row parsers are this backend's implementation of the base introspection contract: the base declares them and every backend must supply them, so the leading underscore described a visibility the code never had. It is also how the next private helper gets justified, which is what this branch is trying to stop. default_schema becomes a property rather than a method, beside dialect and cache: it states a fact about the database, it does not perform an action. No aliases: the old names are gone. Snowflake side: thirteen row parsers -- databases, schemas, warehouses, storage metrics and account objects among them -- plus default_schema and the call sites that read it. Three tests follow.
"table" answered several catalogue questions at once -- the relation's row, its columns, its indexes and its foreign keys -- and printed them as three sections. Each is an item the introspector reports on its own and each already has a type, so the aggregate was a second way to ask for the same rows and the only way to get an unparseable answer: under "-o json" the sections are three arrays one after another, which is not a document. The tests that asserted the three-section rendering (and the not-found path that only the aggregate had) go with it; the types that remain each answer one question.
The introspect subcommand had no parser-level test, so the accepted type list, the dispatch branches and the output formats could drift apart silently. Add an offline suite that parses every INTROSPECT_TYPES entry, refuses a type the list does not carry, drives each sync and async branch against a fake introspector, and checks json/csv/table serialisation without a connection. The help text still advertised the removed 'table' aggregate and showed an example that no longer parses; both are gone. Catalogue adoption: parse_database_info now reports self.server_version (falling back to 'unknown' and (0, 0, 0)) instead of the backend's private _version, MATERIALIZED VIEW keeps its own TableType, and the RETURNING predicates use version_at_least so an unknown version answers False instead of raising.
TABLE_TYPE spellings the parser did not know were folded into BASE_TABLE with no trace, and include_views=False demanded TABLE_TYPE = 'BASE TABLE', which hid managed, event, Iceberg, hybrid and dynamic tables along with the views. Record the catalogue's own spelling on every table row in extra['snowflake_table_type'], map the recognised vendor kinds explicitly, exclude only the two view spellings, and expand core TableType names onto the catalogue's vocabulary so MATERIALIZED_VIEW and EXTERNAL select the right values.
`fix/column-constraint-gates` was squashed into `release/v1.0.0.dev30` as `57439841`, so the branch reference this workflow used to resolve core through no longer names anything ahead of the release. Pointing at the release branch makes each run test what a user installs. The testsuite stays on `fix/column-constraint-gates` on purpose: its own branch carries the two `pytest_compat` shims that keep pytest 9.2 working (the benchmark fixture's `_nodeid` read and xdist's loadgroup suffix), and those are not on the testsuite's release branch yet. That line is repointed when the testsuite merges.
Named after the pull request once it had a number, which is the order the fragment check requires: 4 entries, one per kind of change.
The branch already held fragments named after an older pull request number that never merged (10 on SQL Server, 21 on Snowflake). The numbering rule reads every fragment a pull request adds and requires each to carry that pull request's number, so the check failed on fragments describing changes that ship here all the same. Their text is folded into this pull request's own entries rather than dropped: it is the detail behind the same work -- the catalogue questions core has no shape for, and the CLI that used to call method names no introspector has.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## release/v1.0.0.dev2 #7 +/- ##
=======================================================
+ Coverage 80.18% 80.65% +0.46%
=======================================================
Files 100 117 +17
Lines 3936 5013 +1077
Branches 602 735 +133
=======================================================
+ Hits 3156 4043 +887
- Misses 585 744 +159
- Partials 195 226 +31
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
Description
Lands
fix/column-constraint-gatesonrelease/v1.0.0.dev2.Scope up front: this branch carries the whole cycle started on 2026-10-11 as it reaches this backend — the statement/expression refactor, the typed literals, and the introspection pass at the tip — not only the last few days of fixes. 46 commits, 6 of them marked breaking.
Headline changes:
introspect table-storage-metrics TABLEreports active, time-travel, failsafe and retained-for-clone bytes with the row count and clustering key, and says whenNonemeans "not visible to this role" rather than "no such table" (feat(cli): one table's storage facts…).feat(introspection)!: account objects as expression statements).include_viewsno longer hides managed tables (fix(snowflake): keep the table kind the catalogue named)._TYPE_CAPABILITIES, offersviews NAME, and records why there is nostatuscommand (docs(snowflake): why there is no status command).Type of change
feat: A new featurerefactor: Internal restructuring with no intended behaviour changetest: Adding missing tests or correcting existing testsdocs: Documentation onlyperf/chore/ci/build/revert(not relevant)Breaking Change
Impact on End-Users:
introspect table-storage-metrics TABLEis new;introspect tableis gone.statussubcommand: this backend has no status introspector, and the guide now says so instead of leaving it to be discovered.Impact on Backend Developers (Custom Implementations):
parse_*lost its leading underscores.Related Repositories/Packages
python-activerecord57439841pytest_compatpython-activerecord-testsuite1991449python-activerecord-snowflakef5d1ad1Core is merged (
rhosocial/python-activerecord#127, squashed as57439841); this branch's CI now installs core fromrelease/v1.0.0.dev30. The test suite is still referenced by branch, because itspytest_compatshims are not on its release branch yet; that reference is repointed when the test suite merges.Testing
Test Plan: The full matrix ran green on this exact head (
f5d1ad1) as run41092214031— every Python version and every service container the backend tests against. A local full run is skipped deliberately: the organisation caps a single task at 180 seconds, and the CI result on this SHA is the stronger evidence. The offline CLI tests (parser surface, NAME rules, capability gate) run in under a second and are part of that run.Checklist
README.md, the CLI guide).format_*).Additional Notes