Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c09407a
Added to Ignore .vscode/ on .gitignore
axellpadilla Oct 17, 2024
9f2473c
Adding tests for new feature to use config for indexes
axellpadilla Oct 17, 2024
293294f
Added Basic Index configuration using dbt-postgresql reference
axellpadilla Oct 17, 2024
0770d67
Refactor index and include tests for included columns
axellpadilla Oct 22, 2024
ee93529
Refactor index macro to include optional columns in SQL Server indexes
axellpadilla Oct 22, 2024
866c3b1
fixed typing for python >=3.8, optimized with inmutables, added unit …
axellpadilla Oct 30, 2024
76f8f92
test: characterize index name churn across table rebuilds
Benjamin-Knight Jun 4, 2026
f182231
feat: deterministic dbt_idx_ index names, drop timestamp salt
Benjamin-Knight Jun 4, 2026
513b8cc
feat: idempotent create-index macro with bracket quoting
Benjamin-Knight Jun 4, 2026
8fe7aae
feat: data_compression and sort_in_tempdb index config options
Benjamin-Knight Jun 4, 2026
31b0622
feat: index reconciliation diff engine
Benjamin-Knight Jun 4, 2026
7f1c3a6
feat: index reconciliation on persistent-table paths
Benjamin-Knight Jun 4, 2026
da44288
feat: cross-config index validation
Benjamin-Knight Jun 4, 2026
c358d4c
chore: add pytest-cov; fix dead from_dict override found by coverage
Benjamin-Knight Jun 4, 2026
c21022c
chore: disable dbt telemetry and version check in test env
Benjamin-Knight Jun 4, 2026
8af3951
feat: full definition-option coverage for the indexes config
Benjamin-Knight Jun 4, 2026
1dbe6ed
feat: build_options passthrough for CREATE INDEX build-time options
Benjamin-Knight Jun 4, 2026
dc7384f
feat: emit full option surface in create-index macro; richer describe
Benjamin-Knight Jun 4, 2026
396b22e
fix: review findings - atomic reconcile, early validation, dead rule
Benjamin-Knight Jun 4, 2026
b18575f
docs: changelog for the indexes feature set; SQL Server 2017 floor note
Benjamin-Knight Jun 4, 2026
201a1c2
fix: lock-free index introspection across all catalog views
Benjamin-Knight Jun 11, 2026
add49ed
fix: address review findings on indexes feature
Benjamin-Knight Jun 16, 2026
35cce49
Fix hash collision between key and included columns
axellpadilla Jun 19, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ target/
*.swp
*.swo

# vscode
.vscode/

# Mypy cache
.mypy_cache/

Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@

- Fix unit tests with empty fixtures (`rows: []`) generating invalid `limit 0` syntax; emit `top 0` instead. Also fix `get_columns_in_query()` for queries starting with a CTE, which broke unit tests with an empty `expect` block; such queries are now described via `sp_describe_first_result_set` instead of being executed. [#698](https://github.com/dbt-msft/dbt-sqlserver/issues/698)

#### Features

- Add Postgres-style `indexes` model config for tables, incrementals, seeds and snapshots, covering most `CREATE INDEX` options. [#535](https://github.com/dbt-msft/dbt-sqlserver/issues/535)
- Index names are deterministic definition hashes (`dbt_idx_` prefix); creation is idempotent and unchanged definitions are never rebuilt.
- Reconcile indexes against the config on incremental, DML-refresh and snapshot runs, applied as one atomic batch. Constraint-backing, legacy post-hook and `as_columnstore` indexes are never dropped.
- Index introspection reads the catalog lock-free throughout (`NOLOCK` on every `sys` view, not just `sys.indexes`), so it no longer queues behind concurrent index DDL.
- Add `drop_unmanaged_indexes` config (`false` (default) / `warn` / `true`) for indexes dbt didn't create.
- Validate cross-index config conflicts (multiple clustered indexes, clustered vs `as_columnstore`).
- Document the minimum supported SQL Server version (2017). Partitioning, `XML_COMPRESSION` and ordered columnstore are not yet expressible in the `indexes` config.

### v1.10.0

#### Features
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
The adapter supports dbt-core 0.14 or newer and follows the same versioning scheme.
E.g. version 1.1.x of the adapter will be compatible with dbt-core 1.1.x.

The minimum supported SQL Server version is SQL Server 2017.

## Documentation

We've bundled all documentation on the dbt docs site:
Expand Down
8 changes: 8 additions & 0 deletions dbt/adapters/sqlserver/relation_configs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
from dbt.adapters.sqlserver.relation_configs.index import (
SQLServerIndexConfig,
SQLServerIndexConfigChange,
SQLServerIndexType,
)
from dbt.adapters.sqlserver.relation_configs.policies import (
MAX_CHARACTERS_IN_IDENTIFIER,
SQLServerIncludePolicy,
Expand All @@ -10,4 +15,7 @@
"SQLServerIncludePolicy",
"SQLServerQuotePolicy",
"SQLServerRelationType",
"SQLServerIndexType",
"SQLServerIndexConfig",
"SQLServerIndexConfigChange",
]
605 changes: 605 additions & 0 deletions dbt/adapters/sqlserver/relation_configs/index.py

Large diffs are not rendered by default.

91 changes: 90 additions & 1 deletion dbt/adapters/sqlserver/sqlserver_adapter.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Optional
from typing import Any, List, Optional

import agate
import dbt_common.exceptions
Expand All @@ -17,7 +17,14 @@
from dbt.adapters.capability import Capability, CapabilityDict, CapabilitySupport, Support
from dbt.adapters.events.types import SchemaCreation
from dbt.adapters.reference_keys import _make_ref_key_dict
from dbt.adapters.relation_configs import RelationConfigChangeAction
from dbt.adapters.sql.impl import CREATE_SCHEMA_MACRO_NAME, SQLAdapter
from dbt.adapters.sqlserver.relation_configs import SQLServerIndexConfig, SQLServerIndexType
from dbt.adapters.sqlserver.relation_configs.index import (
create_needs_own_batch,
index_config_changes,
normalize_drop_unmanaged,
)
from dbt.adapters.sqlserver.sqlserver_column import SQLServerColumn, SQLServerColumnNative
from dbt.adapters.sqlserver.sqlserver_configs import SQLServerConfigs
from dbt.adapters.sqlserver.sqlserver_connections import SQLServerConnectionManager
Expand Down Expand Up @@ -288,6 +295,88 @@ def render_model_constraint(cls, constraint: ModelLevelConstraint) -> Optional[s
else:
return None

@available
def parse_index(self, raw_index: Any) -> Optional[SQLServerIndexConfig]:
return SQLServerIndexConfig.parse(raw_index)

@available
def validate_indexes(
self, raw_indexes: Any, as_columnstore: Any = False, drop_unmanaged: Any = False
) -> None:
"""Cross-config checks that individual index validation can't see.
Also fail-fast validates drop_unmanaged_indexes so a bad value errors
on the first build, not only when reconciliation first runs."""
normalize_drop_unmanaged(drop_unmanaged)
configs = []
for raw_index in raw_indexes or []:
parsed = self.parse_index(raw_index)
if parsed:
configs.append(parsed)

clustered = [config for config in configs if config.type == SQLServerIndexType.clustered]
if len(clustered) > 1:
raise dbt_common.exceptions.DbtRuntimeError(
f"A table can have at most one clustered index; "
f"{len(clustered)} declared in the indexes config: "
f"{[list(config.columns) for config in clustered]}"
)
if clustered and as_columnstore:
raise dbt_common.exceptions.DbtRuntimeError(
"A clustered rowstore index in the indexes config conflicts with "
"as_columnstore=true (the default), which builds the table with a "
"clustered columnstore index. Set as_columnstore: false on the "
"model, or remove the clustered entry."
)

@available
def index_changes(
self,
existing_indexes: Any,
raw_indexes: Any,
relation: BaseRelation,
drop_unmanaged: Any = False,
) -> dict:
"""Diff existing indexes (agate table from sqlserver__describe_indexes)
against the model's `indexes` config. Returns plain lists for jinja:
drops (index names), creates (index config dicts to build inside the
reconcile transaction), creates_no_txn (ONLINE/RESUMABLE creates that
must run as standalone autocommitted statements), warnings (strings).
Drops must be applied before creates (a replacement clustered index
needs its predecessor gone first)."""
rows = []
if existing_indexes is not None:
column_names = existing_indexes.column_names
for row in existing_indexes.rows:
rows.append(dict(zip(column_names, row)))

expected = []
for raw_index in raw_indexes or []:
parsed = self.parse_index(raw_index)
if parsed:
expected.append(parsed)

changes, warnings = index_config_changes(rows, expected, relation, drop_unmanaged)

drops = []
creates = []
creates_no_txn = []
for change in changes:
if change.action == RelationConfigChangeAction.drop:
drops.append(change.context.name)
elif change.action == RelationConfigChangeAction.create:
node_config = change.context.as_node_config
if create_needs_own_batch(node_config.get("build_options")):
creates_no_txn.append(node_config)
else:
creates.append(node_config)

return {
"drops": drops,
"creates": creates,
"creates_no_txn": creates_no_txn,
"warnings": warnings,
}


COLUMNS_EQUAL_SQL = """
with diff_count as (
Expand Down
7 changes: 6 additions & 1 deletion dbt/adapters/sqlserver/sqlserver_configs.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
from dataclasses import dataclass
from typing import Optional
from typing import Any, Optional, Tuple

from dbt.adapters.protocol import AdapterConfig
from dbt.adapters.sqlserver.relation_configs import SQLServerIndexConfig


@dataclass
class SQLServerConfigs(AdapterConfig):
auto_provision_aad_principals: Optional[bool] = False
indexes: Optional[Tuple[SQLServerIndexConfig, ...]] = None
# false (default) | warn | true - how index reconciliation treats
# droppable indexes dbt didn't create (YAML may supply bool or str)
drop_unmanaged_indexes: Optional[Any] = False
Loading