Skip to content

Commit af20b9b

Browse files
committed
feat: add ignore destructive support
1 parent 5aacaa8 commit af20b9b

File tree

31 files changed

+3089
-232
lines changed

31 files changed

+3089
-232
lines changed

docs/concepts/models/overview.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,11 +507,15 @@ Some properties are only available in specific model kinds - see the [model conf
507507
: Set this to true to indicate that all changes to this model should be [forward-only](../plans.md#forward-only-plans).
508508

509509
### on_destructive_change
510-
: What should happen when a change to a [forward-only model](../../guides/incremental_time.md#forward-only-models) or incremental model in a [forward-only plan](../plans.md#forward-only-plans) causes a destructive modification to the table schema (i.e., requires dropping an existing column).
510+
: What should happen when a change to a [forward-only model](../../guides/incremental_time.md#forward-only-models) or incremental model in a [forward-only plan](../plans.md#forward-only-plans) causes a destructive modification to the table schema (i.e., requires dropping an existing column or modifying column constraints in ways that could cause data loss).
511511

512512
SQLMesh checks for destructive changes at plan time based on the model definition and run time based on the model's underlying physical tables.
513513

514-
Must be one of the following values: `allow`, `warn`, or `error` (default).
514+
Must be one of the following values: `allow`, `warn`, `error` (default), or `ignore`.
515+
516+
!!! warning "Ignore is Dangerous"
517+
518+
`ignore` is dangerous since it can result in error or data loss. It likely should never be used but could be useful as an "escape-hatch" or a way to workaround unexpected behavior.
515519

516520
### disable_restatement
517521
: Set this to true to indicate that [data restatement](../plans.md#restatement-plans) is disabled for this model.

docs/guides/incremental_time.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,12 @@ The check is performed at plan time based on the model definition. SQLMesh may n
171171

172172
A model's `on_destructive_change` [configuration setting](../reference/model_configuration.md#incremental-models) determines what happens when SQLMesh detects a destructive change.
173173

174-
By default, SQLMesh will error so no data is lost. You can set `on_destructive_change` to `warn` or `allow` in the model's `MODEL` block to allow destructive changes.
174+
By default, SQLMesh will error so no data is lost. You can set `on_destructive_change` to `warn` or `allow` in the model's `MODEL` block to allow destructive changes.
175+
`ignore` can be used to not perform the schema change and allow the table's definition to diverge from the model definition.
176+
177+
!!! warning "Ignore is Dangerous"
178+
179+
`ignore` is dangerous since it can result in error or data loss. It likely should never be used but could be useful as an "escape-hatch" or a way to workaround unexpected behavior.
175180

176181
This example configures a model to silently `allow` destructive changes:
177182

examples/custom_materializations/custom_materializations/custom_kind.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ def insert(
2424
query_or_df: QueryOrDF,
2525
model: Model,
2626
is_first_insert: bool,
27+
render_kwargs: t.Dict[str, t.Any],
2728
**kwargs: t.Any,
2829
) -> None:
2930
assert type(model.kind).__name__ == "ExtendedCustomKind"
3031

31-
self._replace_query_for_model(model, table_name, query_or_df)
32+
self._replace_query_for_model(model, table_name, query_or_df, render_kwargs)

examples/custom_materializations/custom_materializations/full.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def insert(
1717
query_or_df: QueryOrDF,
1818
model: Model,
1919
is_first_insert: bool,
20+
render_kwargs: t.Dict[str, t.Any],
2021
**kwargs: t.Any,
2122
) -> None:
22-
self._replace_query_for_model(model, table_name, query_or_df)
23+
self._replace_query_for_model(model, table_name, query_or_df, render_kwargs)

sqlmesh/core/dialect.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,31 +1134,47 @@ def select_from_values_for_batch_range(
11341134
batch_start: int,
11351135
batch_end: int,
11361136
alias: str = "t",
1137+
source_columns: t.Optional[t.Set[str]] = None,
11371138
) -> exp.Select:
1138-
casted_columns = [
1139-
exp.alias_(exp.cast(exp.column(column), to=kind), column, copy=False)
1140-
for column, kind in columns_to_types.items()
1141-
]
1139+
from sqlmesh import EngineAdapter
1140+
1141+
source_columns = source_columns or set(columns_to_types)
1142+
source_columns_to_types = EngineAdapter.get_source_columns_to_types(
1143+
columns_to_types, source_columns
1144+
)
11421145

11431146
if not values:
11441147
# Ensures we don't generate an empty VALUES clause & forces a zero-row output
11451148
where = exp.false()
1146-
expressions = [tuple(exp.cast(exp.null(), to=kind) for kind in columns_to_types.values())]
1149+
expressions = [
1150+
tuple(exp.cast(exp.null(), to=kind) for kind in source_columns_to_types.values())
1151+
]
11471152
else:
11481153
where = None
11491154
expressions = [
1150-
tuple(transform_values(v, columns_to_types)) for v in values[batch_start:batch_end]
1155+
tuple(transform_values(v, source_columns_to_types))
1156+
for v in values[batch_start:batch_end]
11511157
]
11521158

1153-
values_exp = exp.values(expressions, alias=alias, columns=columns_to_types)
1159+
values_exp = exp.values(expressions, alias=alias, columns=source_columns_to_types)
11541160
if values:
11551161
# BigQuery crashes on `SELECT CAST(x AS TIMESTAMP) FROM UNNEST([NULL]) AS x`, but not
11561162
# on `SELECT CAST(x AS TIMESTAMP) FROM UNNEST([CAST(NULL AS TIMESTAMP)]) AS x`. This
11571163
# ensures nulls under the `Values` expression are cast to avoid similar issues.
1158-
for value, kind in zip(values_exp.expressions[0].expressions, columns_to_types.values()):
1164+
for value, kind in zip(
1165+
values_exp.expressions[0].expressions, source_columns_to_types.values()
1166+
):
11591167
if isinstance(value, exp.Null):
11601168
value.replace(exp.cast(value, to=kind))
11611169

1170+
casted_columns = [
1171+
exp.alias_(
1172+
exp.cast(exp.column(column) if column in source_columns else exp.Null(), to=kind),
1173+
column,
1174+
copy=False,
1175+
)
1176+
for column, kind in columns_to_types.items()
1177+
]
11621178
return exp.select(*casted_columns).from_(values_exp, copy=False).where(where, copy=False)
11631179

11641180

sqlmesh/core/engine_adapter/_typing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
snowpark = optional_import("snowflake.snowpark")
1515

16-
Query = t.Union[exp.Query, exp.DerivedTable]
16+
Query = exp.Query
1717
PySparkSession = t.Union[pyspark.sql.SparkSession, pyspark.sql.connect.dataframe.SparkSession]
1818
PySparkDataFrame = t.Union[pyspark.sql.DataFrame, pyspark.sql.connect.dataframe.DataFrame]
1919

sqlmesh/core/engine_adapter/athena.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,7 @@ def replace_query(
434434
columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
435435
table_description: t.Optional[str] = None,
436436
column_descriptions: t.Optional[t.Dict[str, str]] = None,
437+
source_columns: t.Optional[t.Set[str]] = None,
437438
**kwargs: t.Any,
438439
) -> None:
439440
table = exp.to_table(table_name)
@@ -447,6 +448,7 @@ def replace_query(
447448
columns_to_types=columns_to_types,
448449
table_description=table_description,
449450
column_descriptions=column_descriptions,
451+
source_columns=source_columns,
450452
**kwargs,
451453
)
452454

0 commit comments

Comments
 (0)