dbt-sqlserver 1.11.0__tar.gz → 1.11.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/PKG-INFO +1 -1
  2. dbt_sqlserver-1.11.1/dbt/adapters/sqlserver/__version__.py +1 -0
  3. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/adapters/sqlserver/sqlserver_adapter.py +206 -10
  4. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/adapters/sqlserver/sqlserver_connections.py +58 -0
  5. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/adapters/sqlserver/sqlserver_runtime.py +5 -0
  6. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/models/view/view.sql +24 -4
  7. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt_sqlserver.egg-info/PKG-INFO +1 -1
  8. dbt_sqlserver-1.11.0/dbt/adapters/sqlserver/__version__.py +0 -1
  9. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/LICENSE +0 -0
  10. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/README.md +0 -0
  11. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/__init__.py +0 -0
  12. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/adapters/sqlserver/__init__.py +0 -0
  13. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/adapters/sqlserver/py.typed +0 -0
  14. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/adapters/sqlserver/relation_configs/__init__.py +0 -0
  15. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/adapters/sqlserver/relation_configs/index.py +0 -0
  16. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/adapters/sqlserver/relation_configs/policies.py +0 -0
  17. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/adapters/sqlserver/sqlserver_auth.py +0 -0
  18. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/adapters/sqlserver/sqlserver_backend.py +0 -0
  19. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/adapters/sqlserver/sqlserver_column.py +0 -0
  20. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/adapters/sqlserver/sqlserver_configs.py +0 -0
  21. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/adapters/sqlserver/sqlserver_constants.py +0 -0
  22. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/adapters/sqlserver/sqlserver_credentials.py +0 -0
  23. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/adapters/sqlserver/sqlserver_helpers.py +0 -0
  24. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/adapters/sqlserver/sqlserver_mask.py +0 -0
  25. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/adapters/sqlserver/sqlserver_relation.py +0 -0
  26. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/__init__.py +0 -0
  27. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/dbt_project.yml +0 -0
  28. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/adapters/apply_grants.sql +0 -0
  29. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/adapters/apply_masks.sql +0 -0
  30. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/adapters/catalog.sql +0 -0
  31. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/adapters/columns.sql +0 -0
  32. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/adapters/indexes.sql +0 -0
  33. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/adapters/metadata.sql +0 -0
  34. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/adapters/persist_docs.sql +0 -0
  35. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/adapters/relation.sql +0 -0
  36. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/adapters/schema.sql +0 -0
  37. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/adapters/show.sql +0 -0
  38. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/adapters/validate_sql.sql +0 -0
  39. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/functions/helpers.sql +0 -0
  40. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/functions/scalar.sql +0 -0
  41. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/hooks.sql +0 -0
  42. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/models/incremental/incremental.sql +0 -0
  43. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/models/incremental/incremental_strategies.sql +0 -0
  44. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/models/incremental/merge.sql +0 -0
  45. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/models/table/columns_spec_ddl.sql +0 -0
  46. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/models/table/table.sql +0 -0
  47. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/models/table/table_dml_refresh.sql +0 -0
  48. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/models/unit_test/get_fixture_sql.sql +0 -0
  49. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/models/unit_test/unit_test_create_table_as.sql +0 -0
  50. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/models/view/create_view_as.sql +0 -0
  51. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/snapshots/helpers.sql +0 -0
  52. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/snapshots/snapshot.sql +0 -0
  53. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/snapshots/snapshot_merge.sql +0 -0
  54. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/snapshots/strategies.sql +0 -0
  55. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/tests.sql +0 -0
  56. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/materializations/unit_tests.sql +0 -0
  57. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/relations/seeds/helpers.sql +0 -0
  58. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/relations/table/clone.sql +0 -0
  59. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/relations/table/create.sql +0 -0
  60. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/relations/views/create.sql +0 -0
  61. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/utils/any_value.sql +0 -0
  62. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/utils/array_construct.sql +0 -0
  63. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/utils/cast_bool_to_text.sql +0 -0
  64. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/utils/concat.sql +0 -0
  65. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/utils/date_trunc.sql +0 -0
  66. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/utils/dateadd.sql +0 -0
  67. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/utils/get_tables_by_pattern.sql +0 -0
  68. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/utils/hash.sql +0 -0
  69. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/utils/last_day.sql +0 -0
  70. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/utils/length.sql +0 -0
  71. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/utils/listagg.sql +0 -0
  72. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/utils/position.sql +0 -0
  73. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/utils/safe_cast.sql +0 -0
  74. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/utils/split_part.sql +0 -0
  75. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/macros/utils/timestamps.sql +0 -0
  76. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt/include/sqlserver/profile_template.yml +0 -0
  77. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt_sqlserver.egg-info/SOURCES.txt +0 -0
  78. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt_sqlserver.egg-info/dependency_links.txt +0 -0
  79. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt_sqlserver.egg-info/requires.txt +0 -0
  80. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/dbt_sqlserver.egg-info/top_level.txt +0 -0
  81. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/pyproject.toml +0 -0
  82. {dbt_sqlserver-1.11.0 → dbt_sqlserver-1.11.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbt-sqlserver
3
- Version: 1.11.0
3
+ Version: 1.11.1
4
4
  Summary: A Microsoft SQL Server adapter plugin for dbt
5
5
  Author: Mikael Ene, Anders Swanson, Sam Debruyn, Cor Zuurmond, Cody Scott
6
6
  License: MIT
@@ -0,0 +1 @@
1
+ version = "1.11.1"
@@ -1,3 +1,4 @@
1
+ import re
1
2
  from typing import Any, Dict, List, Optional
2
3
 
3
4
  import agate
@@ -26,16 +27,118 @@ from dbt.adapters.sqlserver.relation_configs.index import (
26
27
  index_config_changes,
27
28
  normalize_drop_unmanaged,
28
29
  )
30
+ from dbt.adapters.sqlserver.sqlserver_auth import is_mssql_python_backend
29
31
  from dbt.adapters.sqlserver.sqlserver_column import SQLServerColumn, SQLServerColumnNative
30
32
  from dbt.adapters.sqlserver.sqlserver_configs import SQLServerConfigs
31
- from dbt.adapters.sqlserver.sqlserver_connections import SQLServerConnectionManager
33
+ from dbt.adapters.sqlserver.sqlserver_connections import (
34
+ SQLServerConnectionManager,
35
+ _discard_pending_results,
36
+ )
32
37
  from dbt.adapters.sqlserver.sqlserver_mask import ColumnMask
33
38
  from dbt.adapters.sqlserver.sqlserver_mask import mask_changes as _mask_changes
34
39
  from dbt.adapters.sqlserver.sqlserver_mask import resolve_masks as _resolve_masks
35
40
  from dbt.adapters.sqlserver.sqlserver_relation import SQLServerRelation
41
+ from dbt.adapters.sqlserver.sqlserver_runtime import _get_pyodbc
36
42
 
37
43
  logger = AdapterLogger("SQLServer")
38
44
 
45
+ # Mirrors sqlserver__select_starts_with_cte
46
+ # (dbt/include/sqlserver/macros/adapters/columns.sql): a query opening with a
47
+ # CTE cannot be neutered as ``select * from (...) where 1 = 0``, so it reaches
48
+ # get_column_schema_from_query unwrapped and would otherwise be executed in
49
+ # full just to read its column names.
50
+ _SQL_COMMENT = re.compile(r"(?s)/\*.*?\*/|--[^\n]*\n")
51
+
52
+ # sp_describe_first_result_set reports true SQL Server types; reading
53
+ # ``cursor.description`` reports Python classes, which collapse whole families
54
+ # (every integer width arrives as ``int``, every string type as ``varchar``).
55
+ # Contract comparison comes through this method either way, so the describe
56
+ # path is mapped back onto exactly the names the execute path yields via
57
+ # ``data_type_code_to_name``. TestCteProbeAvoidsExecution pins the two
58
+ # together; a type missing from this map falls back to executing rather than
59
+ # guessing.
60
+ #
61
+ # The names below are pyodbc's. The class a driver picks for a column is its
62
+ # own choice, not SQL Server's, and mssql-python decodes three of these
63
+ # differently -- see ``_MSSQL_PYTHON_TYPE_OVERRIDES``.
64
+ _SYSTEM_TYPE_TO_EXECUTED_NAME = {
65
+ "bigint": "int",
66
+ "int": "int",
67
+ "smallint": "int",
68
+ "tinyint": "int",
69
+ "bit": "bit",
70
+ "decimal": "decimal",
71
+ "numeric": "decimal",
72
+ "money": "decimal",
73
+ "smallmoney": "decimal",
74
+ "float": "float",
75
+ "real": "float",
76
+ "date": "date",
77
+ "time": "time",
78
+ "datetime": "datetime2(6)",
79
+ "smalldatetime": "datetime2(6)",
80
+ "datetime2": "datetime2(6)",
81
+ "char": "varchar",
82
+ "nchar": "varchar",
83
+ "varchar": "varchar",
84
+ "nvarchar": "varchar",
85
+ "text": "varchar",
86
+ "ntext": "varchar",
87
+ "xml": "varchar",
88
+ "uniqueidentifier": "varchar",
89
+ "binary": "varbinary",
90
+ "varbinary": "varbinary",
91
+ "image": "varbinary",
92
+ "timestamp": "varbinary",
93
+ "rowversion": "varbinary",
94
+ # datetimeoffset is deliberately absent *from this map*: add_query
95
+ # registers the -155 output converter after the execute that needed it,
96
+ # so pyodbc reports the column as bytearray on a connection's first query
97
+ # and as str on every one after. No fixed name mirrors that, so on pyodbc
98
+ # describing gives up and the query is executed -- which agrees with
99
+ # itself by construction. mssql-python needs no converter and is pinned
100
+ # in _MSSQL_PYTHON_TYPE_OVERRIDES.
101
+ "sql_variant": "varbinary",
102
+ "hierarchyid": "varbinary",
103
+ "geography": "varbinary",
104
+ "geometry": "varbinary",
105
+ }
106
+
107
+ # mssql-python decodes three types into richer Python objects than pyodbc
108
+ # does: uniqueidentifier as ``uuid.UUID`` (pyodbc: ``str``), datetimeoffset
109
+ # as ``datetime`` and sql_variant as ``str`` (pyodbc: ``bytearray`` for
110
+ # both). Executing reports those classes, so describing has to agree.
111
+ _MSSQL_PYTHON_TYPE_OVERRIDES = {
112
+ "uniqueidentifier": "uniqueidentifier",
113
+ "datetimeoffset": "datetime2(6)",
114
+ "sql_variant": "varchar",
115
+ }
116
+
117
+
118
+ def _executed_name_for_system_type(base_type: str, backend: Any) -> Optional[str]:
119
+ """The name the executed probe would report for a described column type.
120
+
121
+ None means "no confident answer" -- the caller then executes the query,
122
+ which is slower but cannot disagree with itself.
123
+ """
124
+ if is_mssql_python_backend(backend):
125
+ overridden = _MSSQL_PYTHON_TYPE_OVERRIDES.get(base_type)
126
+ if overridden is not None:
127
+ return overridden
128
+
129
+ elif base_type == "uniqueidentifier":
130
+ # pyodbc yields uuid.UUID or str for a GUID depending on its
131
+ # module-level ``native_uuid`` flag -- process-global state anything
132
+ # in the process can flip, so read it rather than assume a default.
133
+ try:
134
+ native_uuid = bool(_get_pyodbc().native_uuid)
135
+ except Exception as e: # pragma: no cover - pyodbc is present if in use
136
+ logger.debug(f"Could not read pyodbc.native_uuid, executing the query: {e}")
137
+ return None
138
+ return "uniqueidentifier" if native_uuid else "varchar"
139
+
140
+ return _SYSTEM_TYPE_TO_EXECUTED_NAME.get(base_type)
141
+
39
142
 
40
143
  class SQLServerAdapter(SQLAdapter):
41
144
  """
@@ -135,18 +238,108 @@ class SQLServerAdapter(SQLAdapter):
135
238
 
136
239
  @available.parse(lambda *a, **k: [])
137
240
  def get_column_schema_from_query(self, sql: str) -> List[BaseColumn]:
138
- """Get a list of the Columns with names and data types from the given sql."""
241
+ """Get a list of the Columns with names and data types from the given sql.
242
+
243
+ Only the result *shape* is wanted, but the query still runs, so the
244
+ cursor comes back holding the whole result set. Usually that set is
245
+ empty: dbt-core's ``get_column_schema_from_query`` macro wraps the
246
+ query first, and ``sqlserver__get_empty_subquery_sql`` renders that as
247
+ ``select * from (...) where 1 = 0``. A query that opens with a CTE
248
+ cannot be wrapped that way, though, and is passed through untouched
249
+ (dbt/include/sqlserver/macros/adapters/columns.sql), so snapshot
250
+ staging queries and CTE-headed contract models arrive here in full.
251
+
252
+ Either way the cursor must not be abandoned holding rows -- see
253
+ ``_discard_pending_results`` for what that costs.
254
+ """
255
+ if _SQL_COMMENT.sub("", sql).strip().lower().startswith("with"):
256
+ described = self._describe_result_set(sql)
257
+ if described is not None:
258
+ return described
259
+
139
260
  _, cursor = self.connections.add_select_query(sql)
140
261
 
141
- columns = [
142
- self.Column.create(
143
- column_name, self.connections.data_type_code_to_name(column_type_code)
144
- )
145
- # https://peps.python.org/pep-0249/#description
146
- for column_name, column_type_code, *_ in cursor.description
147
- ]
262
+ try:
263
+ columns = [
264
+ self.Column.create(
265
+ column_name, self.connections.data_type_code_to_name(column_type_code)
266
+ )
267
+ # https://peps.python.org/pep-0249/#description
268
+ for column_name, column_type_code, *_ in cursor.description
269
+ ]
270
+ finally:
271
+ _discard_pending_results(cursor)
272
+
148
273
  return columns
149
274
 
275
+ def _describe_result_set(self, sql: str) -> Optional[List[BaseColumn]]:
276
+ """Read a query's column shape without running it, or None to fall back.
277
+
278
+ ``sp_describe_first_result_set`` compiles the query and reports its
279
+ result shape, which is all this method ever wanted. It is already how
280
+ ``sqlserver__get_columns_in_query`` handles CTEs (#698).
281
+
282
+ Returns None -- deliberately, rather than raising -- whenever the
283
+ describe cannot be trusted to match what executing would have reported:
284
+ a query it refuses to describe (it cannot see through ``#temp`` tables,
285
+ where executing works), or a type this backend's driver has no known
286
+ executed name for. The caller then executes as before, which is
287
+ slower but never disagrees with itself.
288
+ """
289
+ credentials = self.connections.profile.credentials
290
+
291
+ # Inline rather than bound: mssql-python binds str as varchar and the
292
+ # procedure demands nvarchar(max). columns.sql:24 escapes it the same
293
+ # way for the same reason.
294
+ describe_sql = "exec sp_describe_first_result_set @tsql = N'{}'".format(
295
+ sql.replace("'", "''")
296
+ )
297
+
298
+ try:
299
+ _, cursor = self.connections.add_select_query(describe_sql)
300
+ except Exception as e:
301
+ logger.debug(f"Could not describe a CTE query, falling back to executing it: {e}")
302
+ return None
303
+
304
+ try:
305
+ fields = [description[0].lower() for description in cursor.description]
306
+ rows = cursor.fetchall()
307
+ except Exception as e:
308
+ logger.debug(f"Could not read a described result set, executing the query: {e}")
309
+ return None
310
+ finally:
311
+ _discard_pending_results(cursor)
312
+
313
+ try:
314
+ hidden, name, type_name = (
315
+ fields.index("is_hidden"),
316
+ fields.index("name"),
317
+ fields.index("system_type_name"),
318
+ )
319
+ except ValueError: # pragma: no cover - shape is fixed by SQL Server
320
+ return None
321
+
322
+ columns = []
323
+ for row in rows:
324
+ if row[hidden]:
325
+ continue
326
+ # "varchar(10)" / "decimal(10,2)" -> "varchar" / "decimal"
327
+ base_type = str(row[type_name]).split("(")[0].strip().lower()
328
+ executed_name = _executed_name_for_system_type(base_type, credentials.backend)
329
+ if executed_name is None or row[name] is None:
330
+ logger.debug(
331
+ f"Describing a CTE query reported {base_type!r}, which has no "
332
+ "equivalent in the executed path; executing it instead"
333
+ )
334
+ return None
335
+ columns.append(self.Column.create(row[name], executed_name))
336
+
337
+ # Every select has at least one column, so nothing described means
338
+ # sp_describe_first_result_set could not work the shape out. Returning
339
+ # an empty list would read as "this query has no columns" and surface
340
+ # as a baffling contract mismatch; execute instead.
341
+ return columns or None
342
+
150
343
  @classmethod
151
344
  def convert_boolean_type(cls, agate_table, col_idx):
152
345
  return "bit"
@@ -324,7 +517,10 @@ class SQLServerAdapter(SQLAdapter):
324
517
  """Return the number of rows in the given relation."""
325
518
  sql = f"SELECT COUNT_BIG(*) FROM {relation}"
326
519
  _, cursor = self.connections.add_select_query(sql)
327
- row = cursor.fetchone()
520
+ try:
521
+ row = cursor.fetchone()
522
+ finally:
523
+ _discard_pending_results(cursor)
328
524
  return int(row[0]) if row else 0
329
525
 
330
526
  def expand_column_types(self, goal, current, max_rows: int = 1000000):
@@ -61,6 +61,64 @@ from dbt.adapters.sqlserver.sqlserver_runtime import (
61
61
 
62
62
  logger = AdapterLogger("sqlserver")
63
63
 
64
+
65
+ def _try_drain_nextset(cursor: Any) -> bool:
66
+ """Drain one additional result set from *cursor*, if supported.
67
+
68
+ Returns ``True`` if a further result set was consumed, ``False`` if the
69
+ cursor has no more result sets.
70
+ """
71
+ return bool(cursor.nextset())
72
+
73
+
74
+ # Rows per round trip when shedding a result set nobody asked for. Large
75
+ # enough that discarding even a big one costs a handful of round trips.
76
+ _DISCARD_CHUNK_SIZE = 10000
77
+
78
+
79
+ def _discard_pending_results(cursor: Any) -> None:
80
+ """Consume and close *cursor*, leaving nothing for the driver to cancel.
81
+
82
+ Closing a cursor whose result set the server is still producing makes the
83
+ driver cancel the request, and the cancel reaches SQL Server as an
84
+ *attention*. Every connection opened here runs ``SET XACT_ABORT ON``
85
+ (``_apply_session_settings``, for dbt-msft/dbt-sqlserver#718), and SQL
86
+ Server answers an attention under ``XACT_ABORT ON`` by rolling back the
87
+ open transaction.
88
+
89
+ None of that raises -- an attention is not an error -- so a materialization
90
+ that abandons a cursor part-way through a build silently loses whatever it
91
+ had already created inside that transaction, then fails further on against
92
+ relations that no longer exist. Fetching the rows first makes the close an
93
+ ordinary one.
94
+
95
+ Failures while discarding are logged and swallowed: the caller already has
96
+ what it came for, and the connection is about to be reused for real work,
97
+ so trouble shedding rows nobody wanted must not become the error the user
98
+ sees.
99
+ """
100
+ try:
101
+ while True:
102
+ # ``description`` is None for statements that return no rows at
103
+ # all, where fetching would raise rather than yield nothing.
104
+ if cursor.description is not None:
105
+ while cursor.fetchmany(_DISCARD_CHUNK_SIZE):
106
+ pass
107
+ # nextset() only advances; whatever rows the set it lands on holds
108
+ # still have to be fetched, hence the outer loop.
109
+ if not _try_drain_nextset(cursor):
110
+ break
111
+ except Exception as e:
112
+ # AdapterLogger cannot serialize an exception as a log argument, so
113
+ # interpolate rather than passing ``e`` through.
114
+ logger.debug(f"Discarding a pending result set failed: {e}")
115
+
116
+ try:
117
+ cursor.close()
118
+ except Exception as e:
119
+ logger.debug(f"Closing a cursor failed: {e}")
120
+
121
+
64
122
  # Attribute used to stash the in-flight pyodbc / mssql-python cursor on a
65
123
  # Connection so cancel() can reach it from another thread. See cancel().
66
124
  _IN_FLIGHT_CURSOR_ATTR = "_dbt_sqlserver_in_flight_cursor"
@@ -45,6 +45,11 @@ class PyodbcModuleProtocol(Protocol):
45
45
  InterfaceError: type[Exception]
46
46
  DatabaseError: type[Exception]
47
47
  pooling: bool
48
+ # Whether pyodbc decodes a uniqueidentifier column as uuid.UUID rather
49
+ # than str. Read by the CTE probe, which has to report the same type
50
+ # executing the query would have. Module-global, so it can change under
51
+ # us; nothing here sets it.
52
+ native_uuid: bool
48
53
 
49
54
  def connect(self, *args: Any, **kwargs: Any) -> Any: ...
50
55
 
@@ -43,10 +43,30 @@
43
43
  {% elif existing_relation is not none and existing_relation.type == 'view' %}
44
44
  {% set current_view_definition_table = run_query(get_view_definition_sql(existing_relation)) %}
45
45
  {% if current_view_definition_table is not none and current_view_definition_table.rows | length > 0 %}
46
- {% set normalized_relation = target_relation.include(database=False) | lower | replace('\n', '') | replace('\r', '') | replace('\t', '') | replace(' ', '') | replace(';', '') %}
47
- {% set normalized_sql = sql | lower | replace('\n', '') | replace('\r', '') | replace('\t', '') | replace(' ', '') | replace(';', '') %}
48
- {% set normalized_definition = current_view_definition_table.rows[0][0] | lower | replace('\n', '') | replace('\r', '') | replace('\t', '') | replace(' ', '') | replace(';', '') %}
49
- {% set should_skip_view_update = normalized_definition.endswith(normalized_sql) %}
46
+ {#- Compare the view *body* exactly, not by suffix. The stored definition is
47
+ the whole statement (CREATE [OR ALTER] VIEW <name> AS <body>); the model is
48
+ only the body. The header ends at the separating ' AS ' - split there and
49
+ compare the remainder verbatim. A suffix test (endswith) would wrongly skip
50
+ any edit whose new body is a tail of the old one, e.g. deleting a leading
51
+ comment or CTE - it lands as PASS but never reaches the database, and
52
+ --full-refresh does not fix it. Do NOT lowercase or strip whitespace: both
53
+ make genuinely different bodies compare equal (a string literal differing
54
+ only in case, or any literal containing spaces). The asymmetry is deliberate - a skip that
55
+ fails to fire costs one rebuild; a skip that fires wrongly costs correctness -
56
+ so when we cannot be certain, we rebuild. -#}
57
+ {% set stored = current_view_definition_table.rows[0][0] %}
58
+ {#- First ' as ' is the header/body separator: CREATE [OR ALTER] VIEW <quoted
59
+ relation> AS has no other, the relation being quoted. -#}
60
+ {% set marker = (stored | lower).find(' as ') %}
61
+ {% if marker < 0 %}
62
+ {% set should_skip_view_update = false %}
63
+ {% else %}
64
+ {% set stored_body = stored[marker + 4:] | replace('\r\n', '\n') | trim %}
65
+ {% set stored_body = (stored_body[:-1] if stored_body.endswith(';') else stored_body) | trim %}
66
+ {% set model_body = sql | replace('\r\n', '\n') | trim %}
67
+ {% set model_body = (model_body[:-1] if model_body.endswith(';') else model_body) | trim %}
68
+ {% set should_skip_view_update = stored_body == model_body %}
69
+ {% endif %}
50
70
  {% endif %}
51
71
  {% if should_skip_view_update %}
52
72
  {% set build_sql = 'declare @dbt_sqlserver_noop int;' %}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbt-sqlserver
3
- Version: 1.11.0
3
+ Version: 1.11.1
4
4
  Summary: A Microsoft SQL Server adapter plugin for dbt
5
5
  Author: Mikael Ene, Anders Swanson, Sam Debruyn, Cor Zuurmond, Cody Scott
6
6
  License: MIT
@@ -1 +0,0 @@
1
- version = "1.11.0"
File without changes
File without changes
File without changes