feat(sqlserver): Part 4 — relation quoting and factory wiring - #14
Merged
Conversation
axellpadilla
added a commit
to dbt-sqlserver-next/dbt-sqlserver-v2-roadmap
that referenced
this pull request
Aug 2, 2026
§5.5's relation section asked whether the shared rendering path escapes an embedded delimiter, and left where to fix it open. It does not: `BaseRelation::quoted` in dbt-schemas interpolates verbatim, so `x"q` rendered `"x"q"`. Fixed with a SqlServer arm in `Relation`'s override rather than in the shared default, because `quoted()` is inherited by every adapter with a quote character and the wider fix would change Snowflake, Postgres, Redshift and Fabric rendering inside a SQL Server PR. Two additions the section didn't have. `normalize_component` needs to stay on the lowercase arm even though `get_canonical_fqn` moves to the pass-through one — they model different things, and passing through there would split one object into two `semantic_fqn`s under a case-insensitive collation. And `adapter_factory.rs` `backend_of` is an E0004 that no issue in the series claims; it landed with Part 4 because nothing can construct an adapter without it. dbt-sqlserver-next/dbt-core#14. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two exhaustive matches stopped compiling when Part 1 added the variant -- `create_static_relation` and `backend_of` -- and `relation_impl.rs` has none, so every arm it needs is a silent-correctness one that shows up as wrong SQL rather than a build failure. `backend_of` maps `SqlServer` to `Backend::SQLServer`, the same ADBC backend Fabric rides. No adapter can be constructed without it, and no issue in the series claimed it. In `relation_impl.rs`: - `get_database` joins the group that raises `InvalidConfig` when `database` is unset. The `_` arm returns an empty string, so without this a relation missing a database renders as `.schema.table`. - `get_canonical_fqn` joins `Fabric | Bigquery`, which pass an unquoted path part through verbatim. SQL Server stores the case it was given. `normalize_component` is deliberately left alone: it models how the server resolves an unquoted name, and folding to lower case is the right model for a case-insensitive collation, which is where Fabric already sits. - `quoted` doubles an embedded delimiter for `SqlServer`. The shared default interpolates verbatim, so `x"q` rendered `"x"q"` -- unparseable, and v2 would have rejected names v1 accepts. The override is scoped to this adapter rather than fixed in `dbt-schemas`, where it would change rendering for every adapter that quotes. - `new_sqlserver` mirrors `new_fabric`; its callers arrive with the metadata module. `include_policy` needs nothing: `_ => Policy::trues()` is already right for 3-part naming, and the explicit arms there are all adapters that drop a path part. Five tests in a `sqlserver` module: three-part rendering, the missing database error, delimiter doubling, case preservation through `get_canonical_fqn`, and construction through `RelationStatic`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
axellpadilla
force-pushed
the
part-4-sqlserver-relation
branch
from
August 2, 2026 17:28
d76021e to
9d83076
Compare
axellpadilla
added a commit
to dbt-sqlserver-next/dbt-sqlserver-v2-roadmap
that referenced
this pull request
Aug 2, 2026
Every case decision in the Rust is a per-AdapterType constant, and SQL Server is the only supported adapter whose case behavior is configurable per database, per column and per comparison. `05`'s risk list now names all four sites that bake the assumption in, with what each one gets wrong on a _CS_ server: `normalize_component` (folds, merging MODEL and model — v1 has the same defect and skips the test that catches it), `get_canonical_fqn` (passes through, safe either way), `infer_seed_column_name_strategy`, and the Part 5 catalog queries, where v1 forces the issue with `collate database_default`. The first is marked TODO in the code as of dbt-sqlserver-next/dbt-core#14. The census in §5.3 said the remaining 45 all belong to dbt-adapter. It was 46, and one is in dbt-df-providers — `infer_seed_column_name_strategy`, which no issue in the Part series claims and which is itself a collation decision. Measured with `cargo check --workspace` rather than per-crate, which is what missed it the first time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 2, 2026
axellpadilla
changed the base branch from
part-3-sqlserver-auth
to
sqlserver-v2-port
August 3, 2026 01:18
2 tasks
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.
Part 4 of the SQL Server Fusion adapter series. Part of dbt-labs#15714.
Two exhaustive matches stopped compiling when Part 1 added the variant, and
relation_impl.rshas none — every arm it needs is a silent-correctness one that shows up as wrong SQL rather than a build failure, so this was worked from the list in the plan rather than fromcargo buildoutput.backend_of— not claimed by any issueadapter_factory.rsbackend_ofmapsAdapterType::SqlServertoBackend::SQLServer, the same ADBC backend Fabric rides. It is one of the 45E0004s and no issue in the series names it; #4 is the closest home, since without it no adapter can be constructed at all. Flagging it so the series checklist can be corrected.relation_impl.rsget_databasejoins theDatabricks | Fabric | Postgres | …group that raisesInvalidConfigwhendatabaseis unset. The_arm returns an empty string, so without the arm a relation missing a database renders as.schema.tablerather than erroring.get_canonical_fqnjoinsFabric | Bigquery, which pass an unquoted path part through verbatim rather than case-folding it. SQL Server stores an identifier with the case it was given.normalize_componentis deliberately left alone. It models how the server resolves an unquoted name, which is a different question — folding to lower case is the correct canonicalization for a case-insensitive collation, and that is the bucket Fabric already sits in. Passing through here would makeMyTableandmytabledistinct semantic FQNs on a server that treats them as one object.quotednow doubles an embedded delimiter for SQL Server. The shared default indbt-schemasis:so
x"qrendered as"x"q"— unparseable, and v2 would reject names v1 accepts. dbt-sqlserver v1 fixed exactly this in dbt-msft/dbt-sqlserver#795, and the escaping is part of the quoting decision this port is built on.Scoped to
SqlServerrather than fixed in the shared default on purpose:quoted()is inherited by every adapter with a quote character, and widening it here would change rendering for Snowflake, Postgres, Redshift and Fabric in a SQL Server PR. Worth raising separately — the shared default looks wrong for all of them — but not worth burying in this diff.new_sqlservermirrorsnew_fabric. Its callers (metadata/get_relation.rs, the metadata module) arrive in Part 5, which is why it has none yet.include_policyneeds nothing. Its_ => Policy::trues()is already correct for 3-part naming; every explicit arm there is an adapter that drops a path part. #4's checklist lists it as work — it isn't.Verification
Five tests in a new
sqlservermodule: three-part rendering, the missing-database error, delimiter doubling (dom\usr+x"q→"my_db"."dom\usr"."x""q"), case preservation throughget_canonical_fqn, and construction throughRelationStatic.dbt-adapterstill has 43E0004s (Parts 5–7), socargo test -p dbt-adaptercannot run on this branch as-is. To actually run these rather than just write them, I stubbed the remaining 43 arms locally withtodo!(), ran the suite, and reverted the stubs before committing:dbt-adaptersuitegeneric::test_normalized_fqn_*tests still pass, which is what pins thequotedoverride to SQL Server onlyThe workspace count drops 46 → 44 here:
relation/factory.rsandadapter/adapter_factory.rs. The rest areadapter_impl.rs(35),sql_types.rs(5),column_builder.rs(2),metadata/get_relation.rs(1) — and one outside this crate,dbt-df-providersseed_io.rsinfer_seed_column_name_strategy, which no issue in the series claims. Earlier PRs in this stack said 45, all indbt-adapter; that came from per-crate checks that never reacheddbt-df-providers.Collation
normalize_componentis left on the lowercase arm above, which is correct under SQL Server's default_CI_collation and wrong under a_CS_one, whereMODELandmodelare two objects that fold to onesemantic_fqn. Reachable only when a project turnsquotingoff —DEFAULT_RESOLVED_QUOTINGis all-true, and the quoted path skips the fold entirely.dbt-sqlserver v1 has the same defect and knows it:
TestCachingUppercaseModelis@pytest.mark.skipped with "Fails because of case sensitivity. MODEL is coereced to model which fails the test as it sees conflicting naming." So this matches v1 rather than regressing against it, but nothing in dbt-core models a collation at all, and SQL Server is the only adapter where case behavior is configurable per database and per column. MarkedTODOat the arm.Closes #4