Skip to content

Commit d6f4d7e

Browse files
author
Alieu
committed
Merge branch 'master' into token_timeout_fix
2 parents b201795 + f2a4806 commit d6f4d7e

File tree

16 files changed

+258
-85
lines changed

16 files changed

+258
-85
lines changed

.circleci/config.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ jobs:
4242
- checkout
4343
- run: &install-dbt-sqlserver
4444
name: "install dbt-sqlserver"
45-
command: pip install .
45+
command: |
46+
pip install .
4647
- run:
4748
name: wait for SQL Server container to set up
4849
command: sleep 30

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,4 @@ env/
9696
venv/
9797
ENV/
9898
env.bak/
99-
venv.bak/
99+
venv.bak/

CHANGELOG.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,47 @@
11
# Changelog
22

3-
### v0.19.0.3
3+
### v0.20.0
4+
5+
#### features:
6+
7+
- dbt-sqlserver will now work with dbt `v0.20.0`. Please see dbt's [upgrading to `v0.20.0` docs](https://docs.getdbt.com/docs/guides/migration-guide/upgrading-to-0-20-0) for more info.
8+
- users can now declare a custom `max_batch_size` in the project configuration to set the batch size used by the seed file loader. [#127](https://github.com/dbt-msft/dbt-sqlserver/issues/127) and [#151](https://github.com/dbt-msft/dbt-sqlserver/pull/151) thanks [@jacobm001](https://github.com/jacobm001)
9+
10+
#### under the hood
11+
12+
- `sqlserver__load_csv_rows` now has a safety provided by `calc_batch_size()` to ensure the insert statements won't exceed SQL Server's 2100 parameter limit. [#127](https://github.com/dbt-msft/dbt-sqlserver/issues/127) and [#151](https://github.com/dbt-msft/dbt-sqlserver/pull/151) thanks [@jacobm001](https://github.com/jacobm001)
13+
- switched to using a `MANIFEST.in` to declare which files should be included
14+
- updated `pyodbc` and `azure-identity` dependencies to their latest versions
15+
### v0.19.2
16+
17+
#### fixes
18+
19+
- fixing and issue with empty seed table that dbt-redshift already addressed with [fishtown-analytics/dbt#2255](https://github.com/fishtown-analytics/dbt/pull/2255) [#147](https://github.com/dbt-msft/dbt-sqlserver/pull/147)
20+
- drop unneeded debugging code that only was run when "Active Directory integrated" was given as the auth method [#149](https://github.com/dbt-msft/dbt-sqlserver/pull/149)
21+
- hotfix for regression introduced by [#126](https://github.com/dbt-msft/dbt-sqlserver/issues/126) that wouldn't surface syntax errors from the SQL engine [#140](https://github.com/dbt-msft/dbt-sqlserver/issues/140) thanks [@jeroen-mostert](https://github.com/jeroen-mostert)!
22+
23+
#### under the hood:
24+
25+
- ensure that macros are not recreated for incremental models [#116](https://github.com/dbt-msft/dbt-sqlserver/issues/116) thanks [@infused-kim](https://github.com/infused-kim)
26+
- authentication now is case-insensitive and accepts both `CLI` and `cli` as options. [#100](https://github.com/dbt-msft/dbt-sqlserver/issues/100) thanks (@JCZuurmond)[https://github.com/JCZuurmond]
27+
- add unit tests for azure-identity related token fetching
28+
29+
### v0.19.1
30+
31+
#### features:
32+
33+
- users can now delcare a model's database to be other than the one specified in the profile. This will only work for on-premise SQL Server and Azure SQL Managed Instance. [#126](https://github.com/dbt-msft/dbt-sqlserver/issues/126) thanks [@semcha](https://github.com/semcha)!
434

535
#### under the hood
36+
37+
- abandon four-part version names (`v0.19.0.2`) in favor of three-part version names because it isn't [SemVer](https://semver.org/) and it causes problems with the `~=` pip operator used dbt-synapse, a pacakge that depends on dbt-sqlserver
638
- allow CI to work with the lower-cost serverless Azure SQL [#132](https://github.com/dbt-msft/dbt-sqlserver/pull/132)
39+
740
### v0.19.0.2
841

942
#### fixes
1043
- solved a bug in snapshots introduced in v0.19.0. Fixes: [#108](https://github.com/dbt-msft/dbt-sqlserver/issues/108), [#117](https://github.com/dbt-msft/dbt-sqlserver/issues/117).
1144

12-
1345
### v0.19.0.1
1446

1547
#### fixes

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
recursive-include dbt/include *.sql *.yml *.md

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,15 @@ client_secret: clientsecret
137137

138138
### Seeds
139139

140+
By default, dbt-sqlserver will attempt to insert seed files in batches of 400 rows. If this exceeds SQL Server's 2100 parameter limit, the adapter will automatically limit to the highest safe value possible.
141+
142+
To set a different default seed value, you can set the variable `max_batch_size` in your project configuration.
143+
144+
```yaml
145+
vars:
146+
max_batch_size: 200 # Any integer less than or equal to 2100 will do.
147+
```
148+
140149
### Hooks
141150
142151
### Custom schemas

dbt/adapters/sqlserver/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version = '0.19.0.2'
1+
version = '0.20.0'

dbt/adapters/sqlserver/connections.py

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import time
66
import struct
77
from itertools import chain, repeat
8-
from typing import Callable, Mapping
8+
from typing import Callable, Dict, Mapping, Optional
99

1010
import dbt.exceptions
1111
from dbt.adapters.base import Credentials
@@ -22,6 +22,7 @@
2222

2323

2424
AZURE_CREDENTIAL_SCOPE = "https://database.windows.net//.default"
25+
_TOKEN: Optional[AccessToken] = None
2526

2627

2728
@dataclass
@@ -162,12 +163,47 @@ def get_sp_access_token(credentials: SQLServerCredentials) -> AccessToken:
162163
token = DefaultAzureCredential().get_token(AZURE_CREDENTIAL_SCOPE)
163164
return token
164165

165-
MAX_QUERY_TIME_IN_SECONDS = 3300
166-
AZURE_AUTH_FUNCTION_TYPE = Callable[[SQLServerCredentials], AccessToken]
167-
AZURE_AUTH_FUNCTIONS: Mapping[str, AZURE_AUTH_FUNCTION_TYPE] = {
168-
"ServicePrincipal": get_sp_access_token,
169-
"CLI": get_cli_access_token,
170-
}
166+
167+
def get_pyodbc_attrs_before(credentials: SQLServerCredentials) -> Dict:
168+
"""
169+
Get the pyodbc attrs before.
170+
171+
Parameters
172+
----------
173+
credentials : SQLServerCredentials
174+
Credentials.
175+
176+
Returns
177+
-------
178+
out : Dict
179+
The pyodbc attrs before.
180+
181+
Source
182+
------
183+
Authentication for SQL server with an access token:
184+
https://docs.microsoft.com/en-us/sql/connect/odbc/using-azure-active-directory?view=sql-server-ver15#authenticating-with-an-access-token
185+
"""
186+
global _TOKEN
187+
attrs_before: Dict
188+
MAX_QUERY_TIME_IN_SECONDS = 3300
189+
190+
azure_auth_function_type = Callable[[SQLServerCredentials], AccessToken]
191+
azure_auth_functions: Mapping[str, azure_auth_function_type] = {
192+
"serviceprincipal": get_sp_access_token,
193+
"cli": get_cli_access_token,
194+
}
195+
196+
authentication = credentials.authentication.lower()
197+
if authentication not in azure_auth_functions:
198+
attrs_before = {}
199+
elif _TOKEN is None or (_TOKEN.expires_on - time.time()) < MAX_QUERY_TIME_IN_SECONDS:
200+
azure_auth_function = azure_auth_functions[authentication]
201+
_TOKEN = _TOKEN or azure_auth_function(credentials)
202+
token_bytes = convert_access_token_to_mswindows_byte_string(_TOKEN)
203+
204+
sql_copt_ss_access_token = 1256 # see source in docstring
205+
attrs_before = {sql_copt_ss_access_token: token_bytes}
206+
return attrs_before
171207

172208

173209
class SQLServerConnectionManager(SQLConnectionManager):
@@ -235,9 +271,6 @@ def open(cls, connection):
235271
con_str.append(f"PWD={{{credentials.PWD}}}")
236272
elif type_auth == "ActiveDirectoryInteractive":
237273
con_str.append(f"UID={{{credentials.UID}}}")
238-
elif type_auth == "ActiveDirectoryIntegrated":
239-
# why is this necessary???
240-
con_str.remove("UID={None}")
241274
elif type_auth == "ActiveDirectoryMsi":
242275
raise ValueError("ActiveDirectoryMsi is not supported yet")
243276

@@ -276,25 +309,9 @@ def open(cls, connection):
276309

277310
con_str_display = ';'.join(con_str)
278311

279-
logger.debug(f'Using connection string: {con_str_display}')
280-
281-
if type_auth in AZURE_AUTH_FUNCTIONS.keys():
282-
# create token if it does not exist
283-
if cls.TOKEN is None or (cls.TOKEN.expires_on - time.time()) < MAX_QUERY_TIME_IN_SECONDS:
284-
azure_auth_function = AZURE_AUTH_FUNCTIONS[type_auth]
285-
token = azure_auth_function(credentials)
286-
cls.TOKEN = convert_access_token_to_mswindows_byte_string(
287-
token
288-
)
289-
290-
# Source:
291-
# https://docs.microsoft.com/en-us/sql/connect/odbc/using-azure-active-directory?view=sql-server-ver15#authenticating-with-an-access-token
292-
SQL_COPT_SS_ACCESS_TOKEN = 1256
293-
294-
attrs_before = {SQL_COPT_SS_ACCESS_TOKEN: cls.TOKEN}
295-
else:
296-
attrs_before = {}
312+
logger.debug(f"Using connection string: {con_str_display}")
297313

314+
attrs_before = get_pyodbc_attrs_before(credentials)
298315
handle = pyodbc.connect(
299316
con_str_concat,
300317
attrs_before=attrs_before,
@@ -356,7 +373,7 @@ def add_query(self, sql, auto_begin=True, bindings=None, abridge_sql_log=False):
356373
self.get_response(cursor), (time.time() - pre)
357374
)
358375
)
359-
376+
360377
return connection, cursor
361378

362379
@classmethod
@@ -383,9 +400,16 @@ def get_response(cls, cursor) -> AdapterResponse:
383400

384401
def execute(self, sql, auto_begin=True, fetch=False):
385402
_, cursor = self.add_query(sql, auto_begin)
386-
status = self.get_response(cursor)
403+
response = self.get_response(cursor)
387404
if fetch:
405+
# Get the result of the first non-empty result set (if any)
406+
while cursor.description is None:
407+
if not cursor.nextset():
408+
break
388409
table = self.get_result_from_cursor(cursor)
389410
else:
390411
table = dbt.clients.agate_helper.empty_table()
391-
return status, table
412+
# Step through all result sets so we process all errors
413+
while cursor.nextset():
414+
pass
415+
return response, table

dbt/adapters/sqlserver/impl.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ def date_function(cls):
1818
@classmethod
1919
def convert_text_type(cls, agate_table, col_idx):
2020
column = agate_table.columns[col_idx]
21-
lens = (len(d.encode("utf-8")) for d in column.values_without_nulls())
21+
# see https://github.com/fishtown-analytics/dbt/pull/2255
22+
lens = [len(d.encode("utf-8")) for d in column.values_without_nulls()]
2223
max_len = max(lens) if lens else 64
2324
length = max_len if max_len > 16 else 16
2425
return "varchar({})".format(length)

dbt/include/sqlserver/macros/adapters.sql

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@
2626
else table_type
2727
end as table_type
2828

29-
from information_schema.tables
29+
from [{{ schema_relation.database }}].information_schema.tables
3030
where table_schema like '{{ schema_relation.schema }}'
31-
and table_catalog like '{{ schema_relation.database }}'
3231
{% endcall %}
3332
{{ return(load_result('list_relations_without_caching').table) }}
3433
{% endmacro %}
3534

3635
{% macro sqlserver__list_schemas(database) %}
3736
{% call statement('list_schemas', fetch_result=True, auto_begin=False) -%}
37+
USE {{ database }};
3838
select name as [schema]
3939
from sys.schemas
4040
{% endcall %}
@@ -43,7 +43,7 @@
4343

4444
{% macro sqlserver__create_schema(relation) -%}
4545
{% call statement('create_schema') -%}
46-
USE [{{ relation.database }}]
46+
USE [{{ relation.database }}];
4747
IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '{{ relation.without_identifier().schema }}')
4848
BEGIN
4949
EXEC('CREATE SCHEMA {{ relation.without_identifier().schema }}')
@@ -72,17 +72,8 @@
7272
{% endmacro %}
7373

7474
{% macro sqlserver__drop_relation(relation) -%}
75-
{% if relation.type == 'view' -%}
76-
{% set object_id_type = 'V' %}
77-
{% elif relation.type == 'table'%}
78-
{% set object_id_type = 'U' %}
79-
{%- else -%} invalid target name
80-
{% endif %}
8175
{% call statement('drop_relation', auto_begin=False) -%}
82-
if object_id ('{{ relation.include(database=False) }}','{{ object_id_type }}') is not null
83-
begin
84-
drop {{ relation.type }} {{ relation.include(database=False) }}
85-
end
76+
{{ sqlserver__drop_relation_script(relation) }}
8677
{%- endcall %}
8778
{% endmacro %}
8879

@@ -93,6 +84,7 @@
9384
{% set object_id_type = 'U' %}
9485
{%- else -%} invalid target name
9586
{% endif %}
87+
USE [{{ relation.database }}];
9688
if object_id ('{{ relation.include(database=False) }}','{{ object_id_type }}') is not null
9789
begin
9890
drop {{ relation.type }} {{ relation.include(database=False) }}
@@ -107,14 +99,24 @@
10799
{{ return(load_result('check_schema_exists').table) }}
108100
{% endmacro %}
109101

102+
103+
{% macro sqlserver__create_view_exec(relation, sql) -%}
104+
{%- set temp_view_sql = sql.replace("'", "''") -%}
105+
execute('create view {{ relation.include(database=False) }} as
106+
{{ temp_view_sql }}
107+
');
108+
{% endmacro %}
109+
110+
110111
{% macro sqlserver__create_view_as(relation, sql) -%}
111-
create view {{ relation.schema }}.{{ relation.identifier }} as
112-
{{ sql }}
112+
USE [{{ relation.database }}];
113+
{{ sqlserver__create_view_exec(relation, sql) }}
113114
{% endmacro %}
114115

115116

116117
{% macro sqlserver__rename_relation(from_relation, to_relation) -%}
117118
{% call statement('rename_relation') -%}
119+
USE [{{ to_relation.database }}];
118120
EXEC sp_rename '{{ from_relation.schema }}.{{ from_relation.identifier }}', '{{ to_relation.identifier }}'
119121
IF EXISTS(
120122
SELECT *
@@ -128,6 +130,7 @@
128130
{%- set cci_name = relation.schema ~ '_' ~ relation.identifier ~ '_cci' -%}
129131
{%- set relation_name = relation.schema ~ '_' ~ relation.identifier -%}
130132
{%- set full_relation = relation.schema ~ '.' ~ relation.identifier -%}
133+
use [{{ relation.database }}];
131134
if EXISTS (
132135
SELECT * FROM
133136
sys.indexes WHERE name = '{{cci_name}}'
@@ -149,12 +152,13 @@
149152

150153
{{ sqlserver__drop_relation_script(relation) }}
151154

152-
EXEC('create view {{ tmp_relation.schema }}.{{ tmp_relation.identifier }} as
155+
USE [{{ relation.database }}];
156+
EXEC('create view {{ tmp_relation.include(database=False) }} as
153157
{{ temp_view_sql }}
154158
');
155159

156-
SELECT * INTO {{ relation.schema }}.{{ relation.identifier }} FROM
157-
{{ tmp_relation.schema }}.{{ tmp_relation.identifier }}
160+
SELECT * INTO {{ relation }} FROM
161+
{{ tmp_relation }}
158162

159163
{{ sqlserver__drop_relation_script(tmp_relation) }}
160164

@@ -165,11 +169,7 @@
165169
{% endmacro %}_
166170

167171
{% macro sqlserver__insert_into_from(to_relation, from_relation) -%}
168-
{%- set full_to_relation = to_relation.schema ~ '.' ~ to_relation.identifier -%}
169-
{%- set full_from_relation = from_relation.schema ~ '.' ~ from_relation.identifier -%}
170-
171-
SELECT * INTO {{full_to_relation}} FROM {{full_from_relation}}
172-
172+
SELECT * INTO {{ to_relation }} FROM {{ from_relation }}
173173
{% endmacro %}
174174

175175
{% macro sqlserver__current_timestamp() -%}
@@ -192,7 +192,7 @@
192192
character_maximum_length,
193193
numeric_precision,
194194
numeric_scale
195-
from INFORMATION_SCHEMA.COLUMNS
195+
from [{{ relation.database }}].INFORMATION_SCHEMA.COLUMNS
196196
where table_name = '{{ relation.identifier }}'
197197
and table_schema = '{{ relation.schema }}'
198198
UNION ALL

0 commit comments

Comments
 (0)