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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
- Guard `run_hooks` commit with `@@trancount` check for autocommit safety when running post-hooks. [#444](https://github.com/dbt-msft/dbt-sqlserver/issues/444)
- Fix `columnstore IF EXISTS` guard to check `object_id('schema.table')` correctly.
- Gate the `optimize_for_sequential_key` and `resumable` index options on the detected engine version: they require SQL Server 2019+, so on 2017/2016 the index is now built without them (with a warning) instead of failing with "is not a recognized CREATE INDEX option".
- Cast the aggregated column names in `sqlserver__describe_indexes` to `nvarchar(max)` so index reconciliation no longer fails with `STRING_AGG` error 9829 on wide tables (e.g. a clustered columnstore index spanning enough columns to exceed the 8000-byte cap). [#735](https://github.com/dbt-msft/dbt-sqlserver/issues/735)
- Escape single quotes in `query_tag` before building `OPTION (LABEL)` clause.
- Map Python `float` to SQL Server `float`, not `bigint`.
- Set default port to `1433` (instead of Postgres `5432`) in `dbt init` profile template.
Expand Down
6 changes: 3 additions & 3 deletions dbt/include/sqlserver/macros/adapters/indexes.sql
Original file line number Diff line number Diff line change
Expand Up @@ -379,23 +379,23 @@
outer apply (
/* STRING_AGG ... WITHIN GROUP requires SQL Server 2017+, the floor
of this adapter's CI matrix */
select string_agg(col.[name], ', ') within group (order by ic.key_ordinal) as cols
select string_agg(cast(col.[name] as nvarchar(max)), ', ') within group (order by ic.key_ordinal) as cols
from sys.index_columns ic {{ information_schema_hints() }}
inner join sys.columns col {{ information_schema_hints() }}
on col.object_id = ic.object_id and col.column_id = ic.column_id
where ic.object_id = i.object_id and ic.index_id = i.index_id
and ic.is_included_column = 0
) key_cols
outer apply (
select string_agg(col.[name], ', ') as cols
select string_agg(cast(col.[name] as nvarchar(max)), ', ') as cols
from sys.index_columns ic {{ information_schema_hints() }}
inner join sys.columns col {{ information_schema_hints() }}
on col.object_id = ic.object_id and col.column_id = ic.column_id
where ic.object_id = i.object_id and ic.index_id = i.index_id
and ic.is_included_column = 1
) incl_cols
outer apply (
select string_agg(col.[name], ', ') as cols
select string_agg(cast(col.[name] as nvarchar(max)), ', ') as cols
from sys.index_columns ic {{ information_schema_hints() }}
inner join sys.columns col {{ information_schema_hints() }}
on col.object_id = ic.object_id and col.column_id = ic.column_id
Expand Down
51 changes: 51 additions & 0 deletions tests/functional/adapter/mssql/test_index_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,27 @@ def test_invalid_index_configs(self, project):

SET_B = "[{'columns': ['column_b'], 'type': 'nonclustered'}]"

# A wide table whose clustered columnstore index (as_columnstore defaults True)
# spans every column. With ~200 columns of ~45 chars each, the column-name list
# aggregated by sqlserver__describe_indexes is ~18 KB as nvarchar, past
# STRING_AGG's 8000-byte result cap.
WIDE_COLUMNSTORE_COLUMN_COUNT = 200
_wide_columnstore_columns = ",\n ".join(
f"{i} as wide_column_{i:03d}_with_a_reasonably_long_name"
for i in range(WIDE_COLUMNSTORE_COLUMN_COUNT)
)
models__wide_columnstore_sql = f"""
{{{{
config(
materialized = "incremental",
as_columnstore = True,
)
}}}}

select
{_wide_columnstore_columns}
"""


def get_index_rows(project, unique_schema, table_name):
sql = indexes_def.format(schema_name=unique_schema, table_name=table_name)
Expand Down Expand Up @@ -755,6 +776,36 @@ def test_dml_refresh_reconciles_definition_change(self, project, unique_schema):
assert index_summary(second) == [("column_b", "nonclustered")]


class TestSQLServerWideColumnstoreReconcile:
@pytest.fixture(scope="class")
def models(self):
return {"wide_columnstore.sql": models__wide_columnstore_sql}

def test_wide_columnstore_reconcile_does_not_overflow(self, project, unique_schema):
# First run creates the wide table plus its clustered columnstore index.
run_dbt(["run", "--models", "wide_columnstore"])

# The second (non-full-refresh) run reconciles indexes, describing the
# existing columnstore index over all its columns.
results = run_dbt(["run", "--models", "wide_columnstore"])
assert len(results) == 1

# The clustered columnstore index (type 5) survives reconciliation.
cci_count = project.run_sql(
f"""
select count(*)
from sys.indexes i
join sys.objects o on o.object_id = i.object_id
join sys.schemas s on s.schema_id = o.schema_id
where s.name = '{unique_schema}'
and o.name = 'wide_columnstore'
and i.[type] = 5
""",
fetch="one",
)[0]
assert cci_count == 1


class TestSQLServerDropUnmanagedIndexes:
@pytest.fixture(scope="class")
def models(self):
Expand Down