Skip to content

Commit 525fe95

Browse files
authored
V1.8.8 PR (#246)
* Security Foundation Initiative to make fabric adapter safer. * Configuring integration tests with Open Connection ID as part of Security Foundation Initiative. * configuring local tests to run on user credentials. Dropping a relation correctly based on its type * Addressing #243, #221, #228, #229, #232, #235 issues. * Updated test helper with ephemeral. * Updated unit tests * Updated get_pyodbc_attrs_before_credentials method * include only lines that start with order by -- dbt show
1 parent bc772ac commit 525fe95

File tree

14 files changed

+148
-155
lines changed

14 files changed

+148
-155
lines changed

.github/workflows/integration-tests-azure.yml

+37-15
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,56 @@
1-
---
2-
name: Integration tests on Azure
3-
on: # yamllint disable-line rule:truthy
1+
name: Integration tests on Fabric DW
2+
on: # yamllint disable-line rule:truthy
43
workflow_dispatch:
54
pull_request:
65
branches:
76
- main
87

98
jobs:
10-
integration-tests-azure:
9+
integration-tests-fabric-dw:
1110
name: Regular
1211
strategy:
1312
fail-fast: false
1413
max-parallel: 1
1514
matrix:
16-
profile: ["ci_azure_auto"]
15+
profile: ["integration_tests"]
1716
python_version: ["3.11"]
18-
msodbc_version: ["17", "18"]
17+
msodbc_version: ["18"]
1918

2019
runs-on: ubuntu-latest
20+
permissions:
21+
contents: read # Required to access repository files
22+
packages: read # Grant explicit read access to packages
23+
id-token: write # Needed if using OIDC authentication
2124
container:
2225
image: ghcr.io/${{ github.repository }}:CI-${{ matrix.python_version }}-msodbc${{ matrix.msodbc_version }}
2326
steps:
24-
- name: AZ CLI login
25-
run: az login --service-principal --username="${AZURE_CLIENT_ID}" --password="${AZURE_CLIENT_SECRET}" --tenant="${AZURE_TENANT_ID}"
26-
env:
27-
AZURE_CLIENT_ID: ${{ secrets.DBT_AZURE_SP_NAME }}
28-
AZURE_CLIENT_SECRET: ${{ secrets.DBT_AZURE_SP_SECRET }}
29-
AZURE_TENANT_ID: ${{ secrets.DBT_AZURE_TENANT }}
27+
# Azure login using federated credentials
28+
- name: Azure login with OIDC
29+
uses: azure/login@v2
30+
with:
31+
client-id: ${{ secrets.DBT_AZURE_SP_NAME }}
32+
tenant-id: ${{ secrets.DBT_AZURE_TENANT }}
33+
allow-no-subscriptions: true
34+
federated-token: true
35+
36+
- name: Test Connection To Fabric Data Warehouse
37+
id: fetch_token
38+
run: |
39+
pip install azure-identity pyodbc azure-core
40+
41+
python - <<EOF
42+
from azure.core.credentials import AccessToken
43+
from azure.identity import DefaultAzureCredential
44+
import pyodbc
45+
import logging
46+
import struct
47+
try:
48+
credential = DefaultAzureCredential()
49+
token = credential.get_token("https://database.windows.net/.default")
50+
print(f"::set-output name=access_token::{token.token}")
51+
except pyodbc.Error as e:
52+
logging.error("Error occurred while connecting to the database.", exc_info=True)
53+
EOF
3054
3155
- uses: actions/checkout@v4
3256

@@ -37,9 +61,7 @@ jobs:
3761
env:
3862
DBT_AZURESQL_SERVER: ${{ secrets.DBT_AZURESQL_SERVER }}
3963
DBT_AZURESQL_DB: ${{ secrets.DBT_AZURESQL_DB }}
40-
AZURE_CLIENT_ID: ${{ secrets.DBT_AZURE_SP_NAME }}
41-
AZURE_CLIENT_SECRET: ${{ secrets.DBT_AZURE_SP_SECRET }}
42-
AZURE_TENANT_ID: ${{ secrets.DBT_AZURE_TENANT }}
64+
FABRIC_INTEGRATION_TESTS_TOKEN: ${{ steps.fetch_token.outputs.access_token }}
4365
FABRIC_TEST_DRIVER: 'ODBC Driver ${{ matrix.msodbc_version }} for SQL Server'
4466
DBT_TEST_USER_1: dbo
4567
DBT_TEST_USER_2: dbo

.github/workflows/publish-docker.yml

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
name: Publish Docker images for CI/CD
33
on: # yamllint disable-line rule:truthy
4+
workflow_dispatch:
45
push:
56
paths:
67
- 'devops/**'
@@ -40,3 +41,6 @@ jobs:
4041
platforms: linux/amd64
4142
target: ${{ matrix.docker_target }}
4243
tags: ghcr.io/${{ github.repository }}:CI-${{ matrix.python_version }}-${{ matrix.docker_target }}
44+
45+
- name: List Docker images
46+
run: docker images ghcr.io/${{ github.repository }}

.github/workflows/unit-tests.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
name: Unit tests
1717
strategy:
1818
matrix:
19-
python_version: ["3.8", "3.9", "3.10", "3.11"]
19+
python_version: ["3.10", "3.11"]
2020
runs-on: ubuntu-latest
2121
permissions:
2222
contents: read

dbt/adapters/fabric/fabric_connection_manager.py

+41-4
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def get_environment_access_token(credentials: FabricCredentials) -> AccessToken:
180180
}
181181

182182

183-
def get_pyodbc_attrs_before(credentials: FabricCredentials) -> Dict:
183+
def get_pyodbc_attrs_before_credentials(credentials: FabricCredentials) -> Dict:
184184
"""
185185
Get the pyodbc attrs before.
186186
@@ -220,6 +220,36 @@ def get_pyodbc_attrs_before(credentials: FabricCredentials) -> Dict:
220220
return attrs_before
221221

222222

223+
def get_pyodbc_attrs_before_accesstoken(accessToken: str) -> Dict:
224+
"""
225+
Get the pyodbc attrs before.
226+
227+
Parameters
228+
----------
229+
credentials : Access Token for Integration Tests
230+
Credentials.
231+
232+
Returns
233+
-------
234+
out : Dict
235+
The pyodbc attrs before.
236+
237+
Source
238+
------
239+
Authentication for SQL server with an access token:
240+
https://docs.microsoft.com/en-us/sql/connect/odbc/using-azure-active-directory?view=sql-server-ver15#authenticating-with-an-access-token
241+
"""
242+
243+
access_token_utf16 = accessToken.encode("utf-16-le")
244+
token_struct = struct.pack(
245+
f"<I{len(access_token_utf16)}s", len(access_token_utf16), access_token_utf16
246+
)
247+
sql_copt_ss_access_token = 1256 # see source in docstring
248+
attrs_before = {sql_copt_ss_access_token: token_struct}
249+
250+
return attrs_before
251+
252+
223253
def bool_to_connection_string_arg(key: str, value: bool) -> str:
224254
"""
225255
Convert a boolean to a connection string argument.
@@ -323,15 +353,18 @@ def open(cls, connection: Connection) -> Connection:
323353

324354
con_str.append(f"Database={credentials.database}")
325355

326-
#Enabling trace flag
356+
# Enabling trace flag
327357
if credentials.trace_flag:
328358
con_str.append("SQL_ATTR_TRACE=SQL_OPT_TRACE_ON")
329359
else:
330360
con_str.append("SQL_ATTR_TRACE=SQL_OPT_TRACE_OFF")
331361

332362
assert credentials.authentication is not None
333363

334-
if "ActiveDirectory" in credentials.authentication:
364+
if (
365+
"ActiveDirectory" in credentials.authentication
366+
and credentials.authentication != "ActiveDirectoryAccessToken"
367+
):
335368
con_str.append(f"Authentication={credentials.authentication}")
336369

337370
if credentials.authentication == "ActiveDirectoryPassword":
@@ -395,7 +428,11 @@ def open(cls, connection: Connection) -> Connection:
395428
def connect():
396429
logger.debug(f"Using connection string: {con_str_display}")
397430

398-
attrs_before = get_pyodbc_attrs_before(credentials)
431+
if credentials.authentication == "ActiveDirectoryAccessToken":
432+
attrs_before = get_pyodbc_attrs_before_accesstoken(credentials.access_token)
433+
else:
434+
attrs_before = get_pyodbc_attrs_before_credentials(credentials)
435+
399436
handle = pyodbc.connect(
400437
con_str_concat,
401438
attrs_before=attrs_before,

dbt/adapters/fabric/fabric_credentials.py

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class FabricCredentials(Credentials):
1717
tenant_id: Optional[str] = None
1818
client_id: Optional[str] = None
1919
client_secret: Optional[str] = None
20+
access_token: Optional[str] = None
2021
authentication: Optional[str] = "ActiveDirectoryServicePrincipal"
2122
encrypt: Optional[bool] = True # default value in MS ODBC Driver 18 as well
2223
trust_cert: Optional[bool] = False # default value in MS ODBC Driver 18 as well

dbt/include/fabric/macros/adapters/catalog.sql

+2-2
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
c.column_id as column_index,
9797
t.name as column_type
9898
from sys.columns as c {{ information_schema_hints() }}
99-
left join sys.types as t on c.system_type_id = t.system_type_id {{ information_schema_hints() }}
99+
left join sys.types as t on c.system_type_id = t.system_type_id
100100
)
101101

102102
select
@@ -223,7 +223,7 @@
223223
c.column_id as column_index,
224224
t.name as column_type
225225
from sys.columns as c {{ information_schema_hints() }}
226-
left join sys.types as t on c.system_type_id = t.system_type_id {{ information_schema_hints() }}
226+
left join sys.types as t on c.system_type_id = t.system_type_id
227227
)
228228

229229
select
+9-21
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,12 @@
11
{% macro fabric__get_limit_sql(sql, limit) %}
2-
3-
{% if limit == -1 or limit is none %}
4-
{% if sql.strip().lower().startswith('with') %}
5-
{{ sql }}
6-
{% else -%}
7-
select *
8-
from (
9-
{{ sql }}
10-
) as model_limit_subq
11-
{%- endif -%}
12-
{% else -%}
13-
{% if sql.strip().lower().startswith('with') %}
14-
{{ sql }} order by (select null)
15-
offset 0 rows fetch first {{ limit }} rows only
16-
{% else -%}
17-
select *
18-
from (
19-
{{ sql }}
20-
) as model_limit_subq order by (select null)
21-
offset 0 rows fetch first {{ limit }} rows only
22-
{%- endif -%}
2+
{%- if limit == -1 or limit is none -%}
3+
{{ sql }}
4+
{#- Special processing if the last non-blank line starts with order by -#}
5+
{%- elif sql.strip().splitlines()[-1].strip().lower().startswith('order by') -%}
6+
{{ sql }}
7+
offset 0 rows fetch first {{ limit }} rows only
8+
{%- else -%}
9+
{{ sql }}
10+
order by (select null) offset 0 rows fetch first {{ limit }} rows only
2311
{%- endif -%}
2412
{% endmacro %}

dbt/include/fabric/macros/materializations/models/table/table.sql

+15-10
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,25 @@
3535

3636
{% if existing_relation is not none and existing_relation.is_table %}
3737

38-
-- making a backup relation, this will come in use when contract is enforced or not
39-
{%- set backup_relation = make_backup_relation(existing_relation, 'table') -%}
38+
-- making a backup relation, this will come in use when contract is enforced or not
39+
{%- set set_backup_relation = adapter.get_relation(database=this.database, schema=this.schema, identifier=this.identifier) -%}
40+
{% if (set_backup_relation != none and set_backup_relation.type == "table") %}
41+
{%- set backup_relation = make_backup_relation(target_relation, 'table') -%}
42+
{% elif (set_backup_relation != none and set_backup_relation.type == "view") %}
43+
{%- set backup_relation = make_backup_relation(target_relation, 'view') -%}
44+
{% endif %}
4045

41-
-- Dropping a temp relation if it exists
42-
{{ adapter.drop_relation(backup_relation) }}
46+
-- Dropping a temp relation if it exists
47+
{{ adapter.drop_relation(backup_relation) }}
4348

44-
-- Rename existing relation to back up relation
45-
{{ adapter.rename_relation(existing_relation, backup_relation) }}
49+
-- Rename existing relation to back up relation
50+
{{ adapter.rename_relation(existing_relation, backup_relation) }}
4651

47-
-- Renaming temp relation as main relation
48-
{{ adapter.rename_relation(temp_relation, target_relation) }}
52+
-- Renaming temp relation as main relation
53+
{{ adapter.rename_relation(temp_relation, target_relation) }}
4954

50-
-- Drop backup relation
51-
{{ adapter.drop_relation(backup_relation) }}
55+
-- Drop backup relation
56+
{{ adapter.drop_relation(backup_relation) }}
5257

5358
{%- else %}
5459

dbt/include/fabric/macros/materializations/snapshots/helpers.sql

+5-49
Original file line numberDiff line numberDiff line change
@@ -3,56 +3,12 @@
33
{% do drop_relation_if_exists(staging_relation) %}
44
{% endmacro %}
55

6-
--Due to Alter not being supported, have to rely on this for temporarily
76
{% macro fabric__create_columns(relation, columns) %}
8-
{# default__ macro uses "add column"
9-
TSQL preferes just "add"
10-
#}
11-
12-
{% set columns %}
13-
{% for column in columns %}
14-
, CAST(NULL AS {{column.data_type}}) AS {{column.name}}
15-
{% endfor %}
16-
{% endset %}
17-
18-
{% set tempTableName %}
19-
[{{relation.database}}].[{{ relation.schema }}].[{{ relation.identifier }}_{{ range(1300, 19000) | random }}]
20-
{% endset %}
21-
{{ log("Creating new columns are not supported without dropping a table. Using random table as a temp table. - " ~ tempTableName) }}
22-
23-
{% set tempTable %}
24-
CREATE TABLE {{tempTableName}}
25-
AS SELECT * {{columns}} FROM [{{relation.database}}].[{{ relation.schema }}].[{{ relation.identifier }}] {{ information_schema_hints() }} {{ apply_label() }}
26-
{% endset %}
27-
28-
{% call statement('create_temp_table') -%}
29-
{{ tempTable }}
30-
{%- endcall %}
31-
32-
{% set dropTable %}
33-
DROP TABLE [{{relation.database}}].[{{ relation.schema }}].[{{ relation.identifier }}]
34-
{% endset %}
35-
36-
{% call statement('drop_table') -%}
37-
{{ dropTable }}
38-
{%- endcall %}
39-
40-
{% set createTable %}
41-
CREATE TABLE {{ relation }}
42-
AS SELECT * FROM {{tempTableName}} {{ information_schema_hints() }} {{ apply_label() }}
43-
{% endset %}
44-
45-
{% call statement('create_Table') -%}
46-
{{ createTable }}
47-
{%- endcall %}
48-
49-
{% set dropTempTable %}
50-
DROP TABLE {{tempTableName}}
51-
{% endset %}
52-
53-
{% call statement('drop_temp_table') -%}
54-
{{ dropTempTable }}
55-
{%- endcall %}
7+
{% for column in columns %}
8+
{% call statement() %}
9+
alter table {{ relation.render() }} add "{{ column.name }}" {{ column.data_type }} NULL;
10+
{% endcall %}
11+
{% endfor %}
5612
{% endmacro %}
5713

5814
{% macro fabric__get_true_sql() %}

dbt/include/fabric/macros/materializations/snapshots/snapshot.sql

+5-4
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@
3131

3232
-- Create a temporary view to manage if user SQl uses CTE
3333
{% set temp_snapshot_relation_sql = model['compiled_code'].replace("'", "''") %}
34-
{% call statement('create temp_snapshot_relation') %}
35-
EXEC('DROP VIEW IF EXISTS {{ temp_snapshot_relation.include(database=False) }};');
36-
EXEC('create view {{ temp_snapshot_relation.include(database=False) }} as {{ temp_snapshot_relation_sql }};');
37-
{% endcall %}
34+
{{ adapter.drop_relation(temp_snapshot_relation) }}
35+
36+
{% call statement('create temp_snapshot_relation') -%}
37+
{{ get_create_view_as_sql(temp_snapshot_relation, temp_snapshot_relation_sql) }}
38+
{%- endcall %}
3839

3940
{% if not target_relation_exists %}
4041

0 commit comments

Comments
 (0)