Skip to content

Commit 6f4820a

Browse files
sdebruynclaude
andcommitted
Add Microsoft Fabric Data Warehouse support
Fabric uses T-SQL with BIT booleans and has several SQL dialect differences from PostgreSQL/Snowflake/BigQuery. This commit makes the package cross-platform compatible with Fabric DW by: - Adding fabric__recursive_dag (loop-based, no recursive CTEs) - Adding fabric__get_dbtreplace_directory_pattern (no regexp_replace) - Adding fabric__type_string_dpe (varchar(8000)) - Adding quote_identifier/bool_literal dispatch macros - Replacing boolean expressions in SELECT with CASE WHEN - Replacing bare booleans in WHERE with explicit comparisons - Replacing GROUP BY ordinals with column names - Replacing || with dbt.concat() - Replacing 'where false' with 'where 1=0' - Replacing cast(True/False as ...) with cast(1/0 as ...) - Adding 'fabric' to target.type conditionals - Guarding ORDER BY in CTEs (invalid in T-SQL without TOP) Relates to #229 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d6ea261 commit 6f4820a

51 files changed

Lines changed: 352 additions & 190 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dbt_project.yml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,17 @@ dispatch:
2626

2727
models:
2828
dbt_project_evaluator:
29-
+materialized: "{{ 'table' if target.type in ['duckdb'] else 'view' }}"
29+
+materialized: "{{ 'table' if target.type in ['duckdb', 'fabric'] else 'view' }}"
3030
marts:
3131
core:
3232
int_all_graph_resources:
3333
+materialized: table
3434
int_direct_relationships:
35-
# required for BigQuery and Redshift for performance/memory reasons
36-
+materialized: "{{ 'table' if target.type in ['bigquery', 'redshift', 'databricks'] else 'view' }}"
35+
# required for BigQuery, Redshift, Databricks, and Fabric for performance/memory reasons
36+
+materialized: "{{ 'table' if target.type in ['bigquery', 'redshift', 'databricks', 'fabric'] else 'view' }}"
3737
int_all_dag_relationships:
38-
# required for BigQuery, Redshift, and Databricks for performance/memory reasons
39-
+materialized: "{{ 'table' if target.type in ['bigquery', 'redshift', 'databricks', 'clickhouse'] else 'view' }}"
38+
# required for BigQuery, Redshift, Databricks, Clickhouse, and Fabric for performance/memory reasons
39+
+materialized: "{{ 'table' if target.type in ['bigquery', 'redshift', 'databricks', 'clickhouse', 'fabric'] else 'view' }}"
4040
dag:
4141
+materialized: table
4242
staging:
@@ -45,11 +45,11 @@ models:
4545
+materialized: table
4646
variables:
4747
stg_naming_convention_folders:
48-
# required for Redshift because listagg runs only on tables
49-
+materialized: "{{ 'table' if target.type == 'redshift' else 'view' }}"
48+
# required for Redshift and Fabric because listagg runs only on tables
49+
+materialized: "{{ 'table' if target.type in ['redshift', 'fabric'] else 'view' }}"
5050
stg_naming_convention_prefixes:
51-
# required for Redshift because listagg runs only on tables
52-
+materialized: "{{ 'table' if target.type == 'redshift' else 'view' }}"
51+
# required for Redshift and Fabric because listagg runs only on tables
52+
+materialized: "{{ 'table' if target.type in ['redshift', 'fabric'] else 'view' }}"
5353

5454

5555
vars:
@@ -89,7 +89,7 @@ vars:
8989

9090
# -- Execution variables --
9191
insert_batch_size: "{{ 500 if target.type in ['athena', 'bigquery'] else 10000 }}"
92-
max_depth_dag: "{{ 9 if target.type in ['bigquery', 'spark', 'databricks'] else 4 if target.type in ['athena', 'trino', 'clickhouse'] else -1 }}"
92+
max_depth_dag: "{{ 9 if target.type in ['bigquery', 'spark', 'databricks', 'fabric'] else 4 if target.type in ['athena', 'trino', 'clickhouse'] else -1 }}"
9393

9494
# -- Code complexity variables --
9595
comment_chars: ["--"]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{% macro fabric__escape_single_quotes(expression) -%}
2+
{{ expression | replace("'","''") }}
3+
{%- endmacro %}
4+
5+
{% macro quote_identifier(name) %}
6+
{{ return(adapter.dispatch('quote_identifier', 'dbt_project_evaluator')(name)) }}
7+
{% endmacro %}
8+
9+
{% macro default__quote_identifier(name) %}{{ name }}{% endmacro %}
10+
11+
{% macro fabric__quote_identifier(name) %}[{{ name }}]{% endmacro %}
12+
13+
{# Convert a Python boolean to a SQL boolean literal appropriate for the target adapter #}
14+
{% macro bool_literal(value) %}
15+
{{ return(adapter.dispatch('bool_literal', 'dbt_project_evaluator')(value)) }}
16+
{% endmacro %}
17+
18+
{% macro default__bool_literal(value) %}{{ value | trim }}{% endmacro %}
19+
20+
{% macro fabric__bool_literal(value) %}{% if value %}1{% else %}0{% endif %}{% endmacro %}

macros/cross_db_shim/type_string.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@
99
{%- macro redshift__type_string_dpe() -%}
1010
{{ return(api.Column.string_type(600)) }}
1111
{%- endmacro -%}
12+
13+
{%- macro fabric__type_string_dpe() -%}
14+
{{ return("varchar(8000)") }}
15+
{%- endmacro -%}

macros/get_directory_pattern.sql

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,15 @@
2424

2525
{% macro get_dbtreplace_directory_pattern() %}
2626
{% if execute %}
27-
{%- set on_mac_or_linux = dbt_project_evaluator.is_os_mac_or_linux() -%}
28-
{%- if on_mac_or_linux -%}
29-
{{ dbt.replace("file_path", "regexp_replace(file_path,'.*/','')", "''") }}
30-
{% else %}
31-
{{ dbt.replace("file_path", "regexp_replace(file_path,'.*\\\\\\\\','')", "''") }}
32-
{% endif %}
27+
{%- if target.type == 'fabric' -%}
28+
left(file_path, len(file_path) - charindex('/', reverse(file_path)))
29+
{%- else -%}
30+
{%- set on_mac_or_linux = dbt_project_evaluator.is_os_mac_or_linux() -%}
31+
{%- if on_mac_or_linux -%}
32+
{{ dbt.replace("file_path", "regexp_replace(file_path,'.*/','')", "''") }}
33+
{% else %}
34+
{{ dbt.replace("file_path", "regexp_replace(file_path,'.*\\\\\\\\','')", "''") }}
35+
{% endif %}
36+
{%- endif -%}
3337
{% endif %}
34-
{% endmacro %}
38+
{% endmacro %}

macros/is_not_empty_string.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@
1010
{{ false }}
1111
{% endif %}
1212

13+
{% endmacro %}
14+
15+
{% macro fabric__is_not_empty_string(str) %}
16+
{% if str %}1{% else %}0{% endif %}
1317
{% endmacro %}

macros/recursive_dag.sql

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,125 @@ with direct_relationships as (
257257
{% endmacro %}
258258

259259

260+
{% macro fabric__recursive_dag() %}
261+
262+
{% set max_depth = var('max_depth_dag') | int %}
263+
{% if max_depth < 2 or max_depth < var('chained_views_threshold') | int %}
264+
{% do exceptions.raise_compiler_error(
265+
'Variable max_depth_dag must be at least 2 and must be greater or equal to than chained_views_threshold.'
266+
) %}
267+
{% endif %}
268+
269+
with direct_relationships as (
270+
select
271+
*
272+
from {{ ref('int_direct_relationships') }}
273+
where resource_type <> 'test'
274+
)
275+
276+
, get_distinct as (
277+
select distinct
278+
resource_id as parent_id,
279+
resource_id as child_id,
280+
resource_name,
281+
materialized as child_materialized,
282+
is_public as child_is_public,
283+
access as child_access,
284+
is_excluded as child_is_excluded
285+
286+
from direct_relationships
287+
)
288+
289+
, cte_0 as (
290+
select
291+
parent_id,
292+
child_id,
293+
child_materialized,
294+
child_is_public,
295+
child_access,
296+
child_is_excluded,
297+
0 as distance,
298+
cast({{ dbt.array_construct(['resource_name']) }} as varchar(max)) as path,
299+
cast(null as {{ dbt.type_boolean() }}) as is_dependent_on_chain_of_views
300+
from get_distinct
301+
)
302+
303+
{% for i in range(1, max_depth) %}
304+
{% set prev_cte_path %}cte_{{ i - 1 }}.path{% endset %}
305+
, cte_{{ i }} as (
306+
select
307+
cte_{{ i - 1 }}.parent_id as parent_id,
308+
direct_relationships.resource_id as child_id,
309+
direct_relationships.materialized as child_materialized,
310+
direct_relationships.is_public as child_is_public,
311+
direct_relationships.access as child_access,
312+
direct_relationships.is_excluded as child_is_excluded,
313+
cte_{{ i - 1 }}.distance+1 as distance,
314+
cast({{ dbt.array_append(prev_cte_path, 'direct_relationships.resource_name') }} as varchar(max)) as path,
315+
case
316+
when
317+
cte_{{ i - 1 }}.child_materialized in ('view', 'ephemeral')
318+
and coalesce(cte_{{ i - 1 }}.is_dependent_on_chain_of_views, cast(1 as bit)) = cast(1 as bit)
319+
then cast(1 as bit)
320+
else cast(0 as bit)
321+
end as is_dependent_on_chain_of_views
322+
323+
from direct_relationships
324+
inner join cte_{{ i - 1 }}
325+
on cte_{{ i - 1 }}.child_id = direct_relationships.direct_parent_id
326+
)
327+
{% endfor %}
328+
329+
, all_relationships_unioned as (
330+
{% for i in range(max_depth) %}
331+
select * from cte_{{ i }}
332+
{% if not loop.last %}union all{% endif %}
333+
{% endfor %}
334+
)
335+
336+
, resource_info as (
337+
select * from {{ ref('int_all_graph_resources') }}
338+
)
339+
340+
, all_relationships as (
341+
select
342+
parent.resource_id as parent_id,
343+
parent.resource_name as parent,
344+
parent.resource_type as parent_resource_type,
345+
parent.model_type as parent_model_type,
346+
parent.materialized as parent_materialized,
347+
parent.is_public as parent_is_public,
348+
parent.access as parent_access,
349+
parent.source_name as parent_source_name,
350+
parent.file_path as parent_file_path,
351+
parent.directory_path as parent_directory_path,
352+
parent.file_name as parent_file_name,
353+
parent.is_excluded as parent_is_excluded,
354+
child.resource_id as child_id,
355+
child.resource_name as child,
356+
child.resource_type as child_resource_type,
357+
child.model_type as child_model_type,
358+
child.materialized as child_materialized,
359+
child.is_public as child_is_public,
360+
child.access as child_access,
361+
child.source_name as child_source_name,
362+
child.file_path as child_file_path,
363+
child.directory_path as child_directory_path,
364+
child.file_name as child_file_name,
365+
child.is_excluded as child_is_excluded,
366+
cast(all_relationships_unioned.distance as {{ dbt.type_int() }}) as distance,
367+
all_relationships_unioned.path,
368+
case when all_relationships_unioned.is_dependent_on_chain_of_views = cast(1 as bit) then cast(1 as bit) else cast(0 as bit) end as is_dependent_on_chain_of_views
369+
from all_relationships_unioned
370+
left join resource_info as parent
371+
on all_relationships_unioned.parent_id = parent.resource_id
372+
left join resource_info as child
373+
on all_relationships_unioned.child_id = child.resource_id
374+
)
375+
376+
{% endmacro %}
377+
378+
260379
{% macro clickhouse__recursive_dag() %}
261380
{{ return(bigquery__recursive_dag()) }}
262381
{% endmacro %}

macros/unpack/get_column_values.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
wrap_string_with_quotes(dbt.escape_single_quotes(column.description | replace("\\","\\\\"))),
2626
wrap_string_with_quotes(dbt.escape_single_quotes(column.data_type)),
2727
wrap_string_with_quotes(dbt.escape_single_quotes(tojson(column.constraints))),
28-
column.constraints | selectattr('type', 'equalto', 'not_null') | list | length > 0,
28+
"cast(" ~ dbt_project_evaluator.bool_literal(column.constraints | selectattr('type', 'equalto', 'not_null') | list | length > 0) | trim ~ " as " ~ dbt.type_boolean() ~ ")",
2929
column.constraints | length,
3030
wrap_string_with_quotes(dbt.escape_single_quotes(column.quote))
3131
]

macros/unpack/get_node_values.sql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@
2323
wrap_string_with_quotes(node.name),
2424
wrap_string_with_quotes(node.resource_type),
2525
wrap_string_with_quotes(node.original_file_path | replace("\\","\\\\")),
26-
"cast(" ~ node.config.enabled | trim ~ " as " ~ dbt.type_boolean() ~ ")",
26+
"cast(" ~ dbt_project_evaluator.bool_literal(node.config.enabled) | trim ~ " as " ~ dbt.type_boolean() ~ ")",
2727
wrap_string_with_quotes(node.config.materialized),
2828
wrap_string_with_quotes(node.config.on_schema_change),
2929
wrap_string_with_quotes(node.group),
3030
wrap_string_with_quotes(node.access),
3131
wrap_string_with_quotes(node.latest_version),
3232
wrap_string_with_quotes(node.version),
3333
wrap_string_with_quotes(node.deprecation_date),
34-
"cast(" ~ contract | trim ~ " as " ~ dbt.type_boolean() ~ ")",
34+
"cast(" ~ dbt_project_evaluator.bool_literal(contract) | trim ~ " as " ~ dbt.type_boolean() ~ ")",
3535
node.columns.values() | list | length,
3636
node.columns.values() | list | selectattr('description') | list | length,
3737
wrap_string_with_quotes(node.database),
@@ -46,7 +46,7 @@
4646
sql_complexity,
4747
wrap_string_with_quotes(node.get('depends_on',{}).get('macros',[]) | tojson),
4848
"cast(" ~ dbt_project_evaluator.is_not_empty_string(node.test_metadata) | trim ~ " as " ~ dbt.type_boolean() ~ ")",
49-
"cast(" ~ exclude_node ~ " as " ~ dbt.type_boolean() ~ ")",
49+
"cast(" ~ dbt_project_evaluator.bool_literal(exclude_node) | trim ~ " as " ~ dbt.type_boolean() ~ ")",
5050
]
5151
%}
5252

macros/unpack/get_relationship_values.sql

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@
2121

2222
{%- if node.get('depends_on',{}).get('nodes',[]) |length == 0 -%}
2323

24-
{%- set values_line =
24+
{%- set values_line =
2525
[
2626
"cast('" ~ node.unique_id ~ "' as " ~ dbt_project_evaluator.type_string_dpe() ~ ")",
2727
"cast(NULL as " ~ dbt_project_evaluator.type_string_dpe() ~ ")",
28-
"FALSE",
29-
]
28+
"cast(" ~ dbt_project_evaluator.bool_literal(false) | trim ~ " as " ~ dbt.type_boolean() ~ ")",
29+
]
3030
%}
3131

3232
{%- do values.append(values_line) -%}
@@ -42,7 +42,7 @@
4242
[
4343
"cast('" ~ node.unique_id ~ "' as " ~ dbt_project_evaluator.type_string_dpe() ~ ")",
4444
"cast('" ~ parent ~ "' as " ~ dbt_project_evaluator.type_string_dpe() ~ ")",
45-
"" ~ is_primary ~ "" if node.unique_id.split('.')[0] == 'test' else "FALSE"
45+
"cast(" ~ dbt_project_evaluator.bool_literal(is_primary if node.unique_id.split('.')[0] == 'test' else false) | trim ~ " as " ~ dbt.type_boolean() ~ ")"
4646
]
4747
%}
4848

macros/unpack/get_source_values.sql

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,23 @@
2222
wrap_string_with_quotes(node.source_name),
2323
"cast(" ~ dbt_project_evaluator.is_not_empty_string(node.source_description) | trim ~ " as " ~ dbt.type_boolean() ~ ")",
2424
"cast(" ~ dbt_project_evaluator.is_not_empty_string(node.description) | trim ~ " as " ~ dbt.type_boolean() ~ ")",
25-
"cast(" ~ node.config.enabled ~ " as " ~ dbt.type_boolean() ~ ")",
25+
"cast(" ~ dbt_project_evaluator.bool_literal(node.config.enabled) | trim ~ " as " ~ dbt.type_boolean() ~ ")",
2626
wrap_string_with_quotes(node.loaded_at_field | replace("'", "_")),
2727

28-
"cast(" ~ (
29-
((node.config.freshness != None) and (dbt_project_evaluator.is_not_empty_string(node.config.freshness.warn_after.count)
30-
or dbt_project_evaluator.is_not_empty_string(node.config.freshness.error_after.count)))
31-
or ((node.freshness != None) and (dbt_project_evaluator.is_not_empty_string(node.freshness.warn_after.count)
28+
"cast(" ~ dbt_project_evaluator.bool_literal(
29+
((node.config.freshness != None) and (dbt_project_evaluator.is_not_empty_string(node.config.freshness.warn_after.count)
30+
or dbt_project_evaluator.is_not_empty_string(node.config.freshness.error_after.count)))
31+
or ((node.freshness != None) and (dbt_project_evaluator.is_not_empty_string(node.freshness.warn_after.count)
3232
or dbt_project_evaluator.is_not_empty_string(node.freshness.error_after.count)))
33-
) | trim ~ " as boolean)",
33+
) | trim ~ " as " ~ dbt.type_boolean() ~ ")",
3434

3535
wrap_string_with_quotes(node.database),
3636
wrap_string_with_quotes(node.schema),
3737
wrap_string_with_quotes(node.package_name),
3838
wrap_string_with_quotes(node.loader),
3939
wrap_string_with_quotes(node.identifier),
4040
wrap_string_with_quotes(node.meta | tojson),
41-
"cast(" ~ exclude_source ~ " as " ~ dbt.type_boolean() ~ ")",
41+
"cast(" ~ dbt_project_evaluator.bool_literal(exclude_source) | trim ~ " as " ~ dbt.type_boolean() ~ ")",
4242
]
4343
%}
4444

0 commit comments

Comments
 (0)