Skip to content

backport(1.11): two silent-correctness fixes for v1.11.1 - #813

Merged
axellpadilla merged 5 commits into
release/v1.11from
backport/1.11.1-critical-fixes
Aug 6, 2026
Merged

backport(1.11): two silent-correctness fixes for v1.11.1#813
axellpadilla merged 5 commits into
release/v1.11from
backport/1.11.1-critical-fixes

Conversation

@axellpadilla

@axellpadilla axellpadilla commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Backports two bugfixes from master to release/v1.11 for a v1.11.1 patch release.

What's included

What's excluded and why

Testing

Full unit suite passes (321 passed).

axellpadilla and others added 2 commits August 6, 2026 15:15
…efinition

sqlserver__view's skip test compared the stored view definition against the
model's compiled SQL with normalized_definition.endswith(normalized_sql). The
stored definition is the whole statement (CREATE [OR ALTER] VIEW <name> AS
<body>) while the model is only the body, so any edit whose new body is a tail
of the old one - most commonly deleting a leading comment or CTE - satisfied
endswith() and was silently skipped. dbt run reported PASS, the change never
reached the database, and --full-refresh did not fix it; once in that state
every subsequent run re-confirmed the skip.

Split the header off at its separating ' AS ' (the first one - the quoted
relation contains no other) and compare the remainder verbatim. Also drop the
| lower and whitespace stripping: both made genuinely different bodies compare
equal (where source = 'MAXIMS' vs 'maxims', or any literal containing spaces),
turning a missed rebuild into a correctness bug. The comparison follows the
asymmetry that a skip failing to fire costs one rebuild while a skip firing
wrongly costs correctness - an unparseable definition (no ' AS ') rebuilds
rather than guessing.

Also removes the dead normalized_relation local, which was computed but never
read.

Add regression tests: removing a leading comment now lands in the stored
definition, and a change confined to the case of a string literal rebuilds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 54e7623)
…he build

get_column_schema_from_query wants a query's column shape and gets it by
executing the query, reading cursor.description, then returning without
fetching the rows or closing the cursor. Closing a cursor whose result set
the server is still producing makes the driver cancel the request, and the
cancel arrives as an attention. Every connection runs SET XACT_ABORT ON
(#718), under which SQL Server answers an attention by rolling back the open
transaction.

None of that raises, so the damage surfaces later and elsewhere: a snapshot
loses the staging table it built moments earlier in the same transaction and
fails with `Invalid object name '..._dbt_tmp'`, and a contract-enforced model
silently loses its in-transaction pre_hook writes.

Only queries opening with a CTE were exposed. Anything else is wrapped as
`select * from (...) where 1 = 0` by sqlserver__get_empty_subquery_sql and
returns nothing, which is why snapshot staging queries (`with snapshot_query
as ...`, both check and timestamp strategies) and CTE-headed contract models
were the ones that broke, while everything else stayed quiet.

The size of the result set is not the trigger; the timing is. The cancel only
raises an attention while the request is still in flight, so a few hundred
rows at zero client delay is enough, while ~10ms of client work before the
close makes even 20MB safe. Existing coverage exercised these paths with
tens of narrow rows and so never tripped it -- the snapshot fixtures in
test_transactions.py are now sized past the boundary, and the previously
untested contract + CTE combination is covered too.

_discard_pending_results lives next to _try_drain_nextset rather than
introducing a second draining idiom, and is applied to _get_row_count as
well, which leaked its cursor on every expand_column_types call.

Note: _try_drain_nextset and _discard_pending_results are backported without
the ADBC-specific NotSupportedError handling in bc92c53, since v1.11 has no
ADBC backend (added later in #783). Reapply that branch if ADBC is ever
backported here too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit bc92c53)
@axellpadilla axellpadilla changed the title backport(1.11): two silent-correctness fixes — view-skip suffix match, probe-cursor transaction rollback backport(1.11): three silent-correctness/reliability fixes for v1.11.1 Aug 6, 2026
@axellpadilla
axellpadilla force-pushed the backport/1.11.1-critical-fixes branch from 022fe34 to 36b9fc1 Compare August 6, 2026 22:16
axellpadilla and others added 3 commits August 6, 2026 16:27
sqlserver__get_empty_subquery_sql neuters a probe query as
`select * from (...) where 1 = 0`, but a query opening with a CTE cannot be
wrapped that way and is passed through untouched, so it ran in full purely to
have its column names read. A snapshot executed its whole staging query once
for the probe and again to build the staging table; a contract-enforced model
ran twice per build. Draining that result set (previous commit) makes it safe
but not cheap, and the cost tracks data volume.

sp_describe_first_result_set compiles the query and reports its shape without
scanning, which is all this method ever wanted. It is already how
sqlserver__get_columns_in_query handles CTEs (#698). The macro cannot be the
fix point: its output is also embedded in a CREATE VIEW body by dbt-core's
unit-test materialization, where only a bare SELECT is legal, and reading
cursor.description of an `exec sp_describe...` batch would describe the
procedure's own result shape rather than the query's.

Reported names and types are unchanged. Reading cursor.description reports
Python classes, which collapse whole type families -- every integer width
arrives as int, every string type as varchar -- so the describe path is
mapped back onto exactly those names and fed through Column.create as before,
leaving the dbt_sqlserver_use_native_string_types flag working without the
mapping knowing it exists.

Rather than guess, the describe path declines and lets the caller execute
whenever it cannot guarantee agreement: a type outside the mapping, a query
sp_describe_first_result_set refuses to describe such as one reading a #temp
table, or a describe that returns nothing. Every gap degrades to slower,
never to silently different.

The name a type maps back to is also backend-specific, not just SQL Server's:
mssql-python decodes uniqueidentifier as uuid.UUID, datetimeoffset as
datetime and sql_variant as str, where pyodbc hands back str, bytearray and
bytearray for the same three. A fixed table can't serve both, so
_executed_name_for_system_type follows the backend in use (folded in from
master's 3354428, minus its ADBC Arrow-map portion -- no ADBC backend on
this branch). datetimeoffset on pyodbc depends on whether add_query has
registered the -155 output converter yet, so it's left unmapped there and
executes, agreeing with itself by construction; uniqueidentifier on pyodbc
depends on the process-global pyodbc.native_uuid flag, read at probe time.

TestCteProbeAvoidsExecution pins both halves: that a CTE-headed probe no
longer executes its query, and that the two branches agree across every
probed type on the backend under test.

Note: dropped the ADBC-backend guard clause from master's version (checking
is_adbc_backend before trusting the describe path), since v1.11 has no ADBC
backend (added later in #783) to disagree with it. Reapply that guard if
ADBC is ever backported here too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7cee257, with the
backend-aware type resolution folded in from 3354428)
…v1.11.1

Both are backported bugfixes with no code changes on this branch beyond the
two cherry-picked commits, so they get one section rather than riding under
"Unreleased"/"v1.12.0" as on master. Kept as an isolated commit (touching only
this new section, no reflow of existing entries) so it cherry-picks cleanly
forward onto master's CHANGELOG once v1.12.0 also needs to record that these
two shipped early in v1.11.1.
@axellpadilla
axellpadilla force-pushed the backport/1.11.1-critical-fixes branch from 36b9fc1 to c3c014a Compare August 6, 2026 23:07
@axellpadilla axellpadilla changed the title backport(1.11): three silent-correctness/reliability fixes for v1.11.1 backport(1.11): two silent-correctness fixes for v1.11.1 Aug 6, 2026
@axellpadilla
axellpadilla merged commit 0086964 into release/v1.11 Aug 6, 2026
1 check passed
@axellpadilla
axellpadilla deleted the backport/1.11.1-critical-fixes branch August 6, 2026 23:14
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