Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dbt/adapters/sqlserver/relation_configs/policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class SQLServerRelationType(StrEnum):
Table = "table"
View = "view"
CTE = "cte"
Function = "function"


class SQLServerIncludePolicy(Policy):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% macro sqlserver__function_execute_build_sql(build_sql, existing_relation, target_relation) %}
{% set grant_config = config.get('grants') %}

{% call statement(name="use_database") %}
{{ get_use_database_sql(target_relation.database) }}
{% endcall %}

{% call statement(name="main") %}
{{ build_sql }}
{% endcall %}

{% set should_revoke = should_revoke(existing_relation, full_refresh_mode=True) %}
{% do apply_grants(target_relation, grant_config, should_revoke=should_revoke) %}

{% do persist_docs(target_relation, model) %}

{{ adapter.commit() }}

{% endmacro %}
42 changes: 42 additions & 0 deletions dbt/include/sqlserver/macros/materializations/functions/scalar.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{% macro sqlserver__scalar_function_sql(target_relation) %}
{{ sqlserver__scalar_function_create_replace_signature_sql(target_relation) }}
{{ sqlserver__scalar_function_body_sql() }};
{% endmacro %}

{% macro sqlserver__scalar_function_create_replace_signature_sql(target_relation) %}
CREATE OR ALTER FUNCTION {{ target_relation.include(database=False) }} ({{ sqlserver__formatted_scalar_function_args_sql() }})
RETURNS {{ model.returns.data_type }}
{{ sqlserver__scalar_function_volatility_sql() }}
AS
{% endmacro %}

{% macro sqlserver__formatted_scalar_function_args_sql() %}
{% set args = [] %}
{% for arg in model.arguments -%}
{%- set arg_def = '@' ~ arg.name ~ ' ' ~ arg.data_type -%}
{%- set default_val = arg.default_value | default(none) -%}
{%- if default_val is not none and default_val | string | trim != '' and default_val | string | lower != 'none' -%}
{%- set arg_def = arg_def ~ ' = ' ~ default_val -%}
{%- endif -%}
{%- do args.append(arg_def) -%}
{%- endfor %}
{{ return(args | join(', ')) }}
{% endmacro %}

{% macro sqlserver__scalar_function_body_sql() %}
BEGIN
RETURN {{ model.compiled_code }}
END
{% endmacro %}

{% macro sqlserver__scalar_function_volatility_sql() %}
{% set volatility = model.config.get('volatility') %}
{% if volatility != none %}
{% do sqlserver__unsupported_volatility_warning(volatility) %}
{% endif %}
{% endmacro %}

{% macro sqlserver__unsupported_volatility_warning(volatility) %}
{% set msg = "Found `" ~ volatility ~ "` volatility specified on function `" ~ model.name ~ "`. SQL Server does not support volatility specifiers; this will be ignored." %}
{% do exceptions.warn(msg) %}
{% endmacro %}
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ classifiers = [
dependencies = [
"dbt-core>=1.11.0,<2.0",
"dbt-common>=1.22.0,<2.0",
"dbt-adapters>=1.15.2,<2.0",
"dbt-adapters>=1.24.1,<2.0",
"pyodbc>=5.2.0",
]
dynamic = ["version"]
Expand All @@ -48,7 +48,7 @@ mssql = [

[dependency-groups]
dev = [
"dbt-tests-adapter>=1.15.0,<2.0",
"dbt-tests-adapter>=1.20.0,<2.0",
"azure-identity>=1.12.0",
"pyodbc>=5.2.0",
"mssql-python>=1.7.1",
Expand Down
243 changes: 243 additions & 0 deletions tests/functional/adapter/dbt/test_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import pytest
from dbt_common.events.base_types import EventMsg

from dbt.artifacts.schemas.results import RunStatus
from dbt.contracts.graph.nodes import FunctionNode
from dbt.tests.adapter.functions import files
from dbt.tests.adapter.functions.test_udafs import (
BasicPythonUDAF,
BasicSQLUDAF,
)
from dbt.tests.adapter.functions.test_udfs import (
ErrorForUnsupportedType,
PythonUDFNotSupported,
SqlUDFDefaultArgSupport,
UDFsBasic,
)
from dbt.tests.util import run_dbt

SQLSERVER_UDF_SQL = """
@price * 2
""".strip()

SQLSERVER_UDF_WITH_DEFAULT_SQL = """
@price * 2
""".strip()

SQLSERVER_UDF_YML = files.MY_UDF_YML

SQLSERVER_UDF_WITH_DEFAULT_ARG_YML = """
functions:
- name: price_for_xlarge
description: Calculate the price for the xlarge version of a standard item
arguments:
- name: price
data_type: float
description: The price of the standard item
default_value: 100
returns:
data_type: float
description: The resulting xlarge price
"""

SQLSERVER_MODEL_USING_FUNCTION_SQL = """
SELECT {{ function('price_for_xlarge') }}(100) AS result
"""


class TestUDFsBasic(UDFsBasic):
@pytest.fixture(scope="class")
def functions(self):
return {
"price_for_xlarge.sql": SQLSERVER_UDF_SQL,
"price_for_xlarge.yml": SQLSERVER_UDF_YML,
}

def is_function_create_event(self, event: EventMsg) -> bool:
return (
event.data.node_info.node_name == "price_for_xlarge"
and "CREATE OR ALTER FUNCTION" in event.data.sql
)

def check_function_volatility(self, sql: str):
assert "VOLATILE" not in sql
assert "STABLE" not in sql
assert "IMMUTABLE" not in sql


class TestDeterministicUDF(TestUDFsBasic):
@pytest.fixture(scope="class")
def project_config_update(self):
return {
"functions": {"+volatility": "deterministic"},
}


class TestStableUDF(TestUDFsBasic):
@pytest.fixture(scope="class")
def project_config_update(self):
return {
"functions": {"+volatility": "stable"},
}


class TestNonDeterministicUDF(TestUDFsBasic):
@pytest.fixture(scope="class")
def project_config_update(self):
return {
"functions": {"+volatility": "non-deterministic"},
}


class TestErrorForUnsupportedType(ErrorForUnsupportedType):
@pytest.fixture(scope="class")
def functions(self):
return {
"price_for_xlarge.sql": SQLSERVER_UDF_SQL,
"price_for_xlarge.yml": SQLSERVER_UDF_YML,
}


class TestPythonUDFNotSupported(PythonUDFNotSupported):
@pytest.fixture(scope="class")
def functions(self):
return {
"price_for_xlarge.py": files.MY_UDF_PYTHON,
"price_for_xlarge.yml": files.MY_UDF_PYTHON_YML,
}


class TestSqlUDFDefaultArgSupport(SqlUDFDefaultArgSupport):
expect_default_arg_support = True

@pytest.fixture(scope="class")
def functions(self):
return {
"price_for_xlarge.sql": SQLSERVER_UDF_WITH_DEFAULT_SQL,
"price_for_xlarge.yml": SQLSERVER_UDF_WITH_DEFAULT_ARG_YML,
}

def is_function_create_event(self, event: EventMsg) -> bool:
return (
event.data.node_info.node_name == "price_for_xlarge"
and "CREATE OR ALTER FUNCTION" in event.data.sql
)

def test_udfs(self, project, sql_event_catcher):
result = run_dbt(["build", "--debug"], callbacks=[sql_event_catcher.catch])
assert len(result.results) == 1

assert "= 100" in sql_event_catcher.caught_events[0].data.sql

result = run_dbt(
["show", "--inline", "SELECT {{ function('price_for_xlarge') }}(DEFAULT)"]
)
assert len(result.results) == 1
assert result.results[0].agate_table.rows[0].values()[0] == 200


class TestUDFParse:
@pytest.fixture(scope="class")
def functions(self):
return {
"price_for_xlarge.sql": SQLSERVER_UDF_SQL,
"price_for_xlarge.yml": SQLSERVER_UDF_YML,
}

def test_parse(self, project):
result = run_dbt(["parse"])
assert result is not None


class TestUDFList:
@pytest.fixture(scope="class")
def functions(self):
return {
"price_for_xlarge.sql": SQLSERVER_UDF_SQL,
"price_for_xlarge.yml": SQLSERVER_UDF_YML,
}

def test_list(self, project):
result = run_dbt(["list", "--select", "resource_type:function"])
assert len(result) == 1


class TestUDFBuild:
@pytest.fixture(scope="class")
def functions(self):
return {
"price_for_xlarge.sql": SQLSERVER_UDF_SQL,
"price_for_xlarge.yml": SQLSERVER_UDF_YML,
}

def test_build(self, project):
result = run_dbt(["build", "--select", "resource_type:function"])
assert len(result.results) == 1
assert result.results[0].status == RunStatus.Success
assert isinstance(result.results[0].node, FunctionNode)


class TestUDFModelRef:
@pytest.fixture(scope="class")
def functions(self):
return {
"price_for_xlarge.sql": SQLSERVER_UDF_SQL,
"price_for_xlarge.yml": SQLSERVER_UDF_YML,
}

@pytest.fixture(scope="class")
def models(self):
return {
"model_using_function.sql": SQLSERVER_MODEL_USING_FUNCTION_SQL,
}

def test_model_ref(self, project):
result = run_dbt(["build"])
assert len(result.results) == 2

result = run_dbt(["show", "--inline", "SELECT * FROM {{ ref('model_using_function') }}"])
assert len(result.results) == 1
select_value = int(result.results[0].agate_table.rows[0].values()[0])
assert select_value == 200


class TestUDFSchemaCreation:
@pytest.fixture(scope="class")
def functions(self):
return {
"price_for_xlarge.sql": SQLSERVER_UDF_SQL,
"price_for_xlarge.yml": SQLSERVER_UDF_YML,
}

def test_schema_created_before_function(self, project):
result = run_dbt(["build"])
assert len(result.results) == 1
assert result.results[0].status == RunStatus.Success


class TestUDFRebuild:
@pytest.fixture(scope="class")
def functions(self):
return {
"price_for_xlarge.sql": SQLSERVER_UDF_SQL,
"price_for_xlarge.yml": SQLSERVER_UDF_YML,
}

def test_rebuild(self, project):
first_result = run_dbt(["build"])
assert len(first_result.results) == 1
assert first_result.results[0].status == RunStatus.Success

second_result = run_dbt(["build"])
assert len(second_result.results) == 1
assert second_result.results[0].status == RunStatus.Success


@pytest.mark.skip(reason="SQL Server does not support user-defined aggregate functions")
class TestBasicSQLUDAF(BasicSQLUDAF):
pass


@pytest.mark.skip(reason="SQL Server does not support user-defined aggregate functions")
class TestBasicPythonUDAF(BasicPythonUDAF):
pass