Skip to content

Commit 43303a0

Browse files
authored
Feat: Enable previews by default for native projects and dbt projects if the target engine suports cloning (#3771)
1 parent dc613d0 commit 43303a0

6 files changed

Lines changed: 82 additions & 44 deletions

File tree

docs/reference/configuration.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,15 @@ Global variable values may be any of the data types in the table below or lists
6161

6262
Configuration for the `sqlmesh plan` command.
6363

64-
| Option | Description | Type | Required |
65-
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------: | :------: |
66-
| `auto_categorize_changes` | Indicates whether SQLMesh should attempt to automatically [categorize](../concepts/plans.md#change-categories) model changes during plan creation per each model source type ([additional details](../guides/configuration.md#auto-categorize-changes)) | dict[string, string] | N |
67-
| `include_unmodified` | Indicates whether to create views for all models in the target development environment or only for modified ones (Default: False) | boolean | N |
68-
| `auto_apply` | Indicates whether to automatically apply a new plan after creation (Default: False) | boolean | N |
69-
| `forward_only` | Indicates whether the plan should be [forward-only](../concepts/plans.md#forward-only-plans) (Default: False) | boolean | N |
70-
| `enable_preview` | Indicates whether to enable [data preview](../concepts/plans.md#data-preview) for forward-only models when targeting a development environment (Default: False) | boolean | N |
71-
| `no_diff` | Don't show diffs for changed models (Default: False) | boolean | N |
72-
| `no_prompts` | Disables interactive prompts in CLI (Default: True) | boolean | N |
64+
| Option | Description | Type | Required |
65+
|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------:|:--------:|
66+
| `auto_categorize_changes` | Indicates whether SQLMesh should attempt to automatically [categorize](../concepts/plans.md#change-categories) model changes during plan creation per each model source type ([additional details](../guides/configuration.md#auto-categorize-changes)) | dict[string, string] | N |
67+
| `include_unmodified` | Indicates whether to create views for all models in the target development environment or only for modified ones (Default: False) | boolean | N |
68+
| `auto_apply` | Indicates whether to automatically apply a new plan after creation (Default: False) | boolean | N |
69+
| `forward_only` | Indicates whether the plan should be [forward-only](../concepts/plans.md#forward-only-plans) (Default: False) | boolean | N |
70+
| `enable_preview` | Indicates whether to enable [data preview](../concepts/plans.md#data-preview) for forward-only models when targeting a development environment (Default: True, except for dbt projects where the target engine does not support cloning) | Boolean | N |
71+
| `no_diff` | Don't show diffs for changed models (Default: False) | boolean | N |
72+
| `no_prompts` | Disables interactive prompts in CLI (Default: True) | boolean | N |
7373

7474
## Run
7575

sqlmesh/core/config/plan.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import typing as t
4+
35
from sqlmesh.core.config.base import BaseConfig
46
from sqlmesh.core.config.categorizer import CategorizerConfig
57

@@ -23,7 +25,7 @@ class PlanConfig(BaseConfig):
2325
forward_only: bool = False
2426
auto_categorize_changes: CategorizerConfig = CategorizerConfig()
2527
include_unmodified: bool = False
26-
enable_preview: bool = False
28+
enable_preview: t.Optional[bool] = None
2729
no_diff: bool = False
2830
no_prompts: bool = True
2931
auto_apply: bool = False

sqlmesh/core/context.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -596,13 +596,8 @@ def load(self, update_schemas: bool = True) -> GenericContext[C]:
596596
self.default_dialect or ""
597597
}
598598

599-
project_types = {
600-
c.DBT if loader.config.loader.__name__.lower().startswith(c.DBT) else c.NATIVE
601-
for loader in self._loaders
602-
}
603-
604599
analytics.collector.on_project_loaded(
605-
project_type=c.HYBRID if len(project_types) > 1 else first(project_types),
600+
project_type=self._project_type,
606601
models_count=len(self._models),
607602
audits_count=len(self._audits),
608603
standalone_audits_count=len(self._standalone_audits),
@@ -1390,7 +1385,7 @@ def plan_builder(
13901385
default_start=default_start,
13911386
default_end=default_end,
13921387
enable_preview=(
1393-
enable_preview if enable_preview is not None else self.config.plan.enable_preview
1388+
enable_preview if enable_preview is not None else self._plan_preview_enabled
13941389
),
13951390
end_bounded=not run,
13961391
ensure_finalized_snapshots=self.config.plan.use_finalized_state,
@@ -2279,6 +2274,23 @@ def _select_models_for_run(
22792274
result = set(dag.subdag(*result))
22802275
return result
22812276

2277+
@cached_property
2278+
def _project_type(self) -> str:
2279+
project_types = {
2280+
c.DBT if loader.__class__.__name__.lower().startswith(c.DBT) else c.NATIVE
2281+
for loader in self._loaders
2282+
}
2283+
return c.HYBRID if len(project_types) > 1 else first(project_types)
2284+
2285+
@property
2286+
def _plan_preview_enabled(self) -> bool:
2287+
if self.config.plan.enable_preview is not None:
2288+
return self.config.plan.enable_preview
2289+
# It is dangerous to enable preview by default for dbt projects that rely on engines that don’t support cloning.
2290+
# Enabling previews in such cases can result in unintended full refreshes because dbt incremental models rely on
2291+
# the maximum timestamp value in the target table.
2292+
return self._project_type == c.NATIVE or self.engine_adapter.SUPPORTS_CLONING
2293+
22822294

22832295
class Context(GenericContext[Config]):
22842296
CONFIG_TYPE = Config

sqlmesh/core/model/kind.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def is_symbolic(self) -> bool:
120120

121121
@property
122122
def is_materialized(self) -> bool:
123-
return not (self.is_symbolic or self.is_view)
123+
return self.model_kind_name is not None and not (self.is_symbolic or self.is_view)
124124

125125
@property
126126
def only_execution_time(self) -> bool:

tests/core/test_context.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,3 +1265,11 @@ def test_rendered_diff():
12651265
)
12661266
-DROP VIEW "test"
12671267
+DROP VIEW IF EXISTS "test"''' in plan.context_diff.text_diff('"test"')
1268+
1269+
1270+
def test_plan_enable_preview_default(sushi_context: Context, sushi_dbt_context: Context):
1271+
assert sushi_context._plan_preview_enabled
1272+
assert not sushi_dbt_context._plan_preview_enabled
1273+
1274+
sushi_dbt_context.engine_adapter.SUPPORTS_CLONING = True
1275+
assert sushi_dbt_context._plan_preview_enabled

tests/core/test_integration.py

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ def test_forward_only_model_regular_plan(init_and_plan_context: t.Callable):
232232
snapshot = context.get_snapshot(model, raise_if_missing=True)
233233
top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True)
234234

235-
plan = context.plan_builder("dev", skip_tests=True).build()
235+
plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build()
236236
assert len(plan.new_snapshots) == 2
237237
assert (
238238
plan.context_diff.snapshots[snapshot.snapshot_id].change_category
@@ -253,7 +253,9 @@ def test_forward_only_model_regular_plan(init_and_plan_context: t.Callable):
253253
assert not dev_df["event_date"].tolist()
254254

255255
# Run a restatement plan to preview changes
256-
plan_builder = context.plan_builder("dev", skip_tests=True, restate_models=[model_name])
256+
plan_builder = context.plan_builder(
257+
"dev", skip_tests=True, restate_models=[model_name], enable_preview=False
258+
)
257259
plan_builder.set_start("2023-01-06")
258260
assert plan_builder.build().missing_intervals == [
259261
SnapshotIntervals(
@@ -397,7 +399,7 @@ def test_forward_only_model_restate_full_history_in_dev(init_and_plan_context: t
397399
assert model.kind.full_history_restatement_only
398400
context.upsert_model(model)
399401

400-
context.plan("prod", skip_tests=True, auto_apply=True)
402+
context.plan("prod", skip_tests=True, auto_apply=True, enable_preview=False)
401403

402404
model_kwargs = {
403405
**model.dict(),
@@ -407,7 +409,7 @@ def test_forward_only_model_restate_full_history_in_dev(init_and_plan_context: t
407409
context.upsert_model(SqlModel.parse_obj(model_kwargs))
408410

409411
# Apply the model change in dev
410-
plan = context.plan_builder("dev", skip_tests=True).build()
412+
plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build()
411413
assert not plan.missing_intervals
412414
context.apply(plan)
413415

@@ -425,7 +427,7 @@ def test_forward_only_model_restate_full_history_in_dev(init_and_plan_context: t
425427
assert df["cnt"][0] == 1
426428

427429
# Apply a restatement plan in dev
428-
plan = context.plan("dev", restate_models=[model.name], auto_apply=True)
430+
plan = context.plan("dev", restate_models=[model.name], auto_apply=True, enable_preview=False)
429431
assert len(plan.missing_intervals) == 1
430432

431433
# Check that the dummy value is not present
@@ -833,7 +835,7 @@ def test_forward_only_parent_created_in_dev_child_created_in_prod(
833835
)
834836
top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True)
835837

836-
plan = context.plan_builder("dev", skip_tests=True).build()
838+
plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build()
837839
assert len(plan.new_snapshots) == 2
838840
assert (
839841
plan.context_diff.snapshots[waiter_revenue_by_day_snapshot.snapshot_id].change_category
@@ -855,7 +857,7 @@ def test_forward_only_parent_created_in_dev_child_created_in_prod(
855857

856858
top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True)
857859

858-
plan = context.plan_builder("prod", skip_tests=True).build()
860+
plan = context.plan_builder("prod", skip_tests=True, enable_preview=False).build()
859861
assert len(plan.new_snapshots) == 1
860862
assert (
861863
plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category
@@ -869,7 +871,7 @@ def test_forward_only_parent_created_in_dev_child_created_in_prod(
869871
def test_new_forward_only_model(init_and_plan_context: t.Callable):
870872
context, _ = init_and_plan_context("examples/sushi")
871873

872-
context.plan("dev", skip_tests=True, no_prompts=True, auto_apply=True)
874+
context.plan("dev", skip_tests=True, no_prompts=True, auto_apply=True, enable_preview=False)
873875

874876
snapshot = context.get_snapshot("sushi.marketing")
875877

@@ -1156,7 +1158,7 @@ def test_indirect_non_breaking_change_after_forward_only_in_dev(init_and_plan_co
11561158
context.upsert_model(model)
11571159
snapshot = context.get_snapshot(model, raise_if_missing=True)
11581160

1159-
plan = context.plan_builder("dev", skip_tests=True).build()
1161+
plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build()
11601162
assert (
11611163
plan.context_diff.snapshots[snapshot.snapshot_id].change_category
11621164
== SnapshotChangeCategory.FORWARD_ONLY
@@ -1169,7 +1171,7 @@ def test_indirect_non_breaking_change_after_forward_only_in_dev(init_and_plan_co
11691171
context.upsert_model(add_projection_to_model(t.cast(SqlModel, model)))
11701172
top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True)
11711173

1172-
plan = context.plan_builder("dev", skip_tests=True).build()
1174+
plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build()
11731175
assert len(plan.new_snapshots) == 1
11741176
assert (
11751177
plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category
@@ -1202,7 +1204,7 @@ def test_indirect_non_breaking_change_after_forward_only_in_dev(init_and_plan_co
12021204
)
12031205
top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True)
12041206

1205-
plan = context.plan_builder("dev", skip_tests=True).build()
1207+
plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build()
12061208
assert len(plan.new_snapshots) == 2
12071209
assert (
12081210
plan.context_diff.snapshots[waiter_revenue_by_day_snapshot.snapshot_id].change_category
@@ -1233,7 +1235,7 @@ def test_indirect_non_breaking_change_after_forward_only_in_dev(init_and_plan_co
12331235
assert not context.plan_builder("dev", skip_tests=True).build().requires_backfill
12341236

12351237
# Deploy everything to prod.
1236-
plan = context.plan_builder("prod", skip_tests=True).build()
1238+
plan = context.plan_builder("prod", skip_tests=True, enable_preview=False).build()
12371239
assert plan.start == to_timestamp("2023-01-01")
12381240
assert plan.missing_intervals == [
12391241
SnapshotIntervals(
@@ -1263,7 +1265,11 @@ def test_indirect_non_breaking_change_after_forward_only_in_dev(init_and_plan_co
12631265
]
12641266

12651267
context.apply(plan)
1266-
assert not context.plan_builder("prod", skip_tests=True).build().requires_backfill
1268+
assert (
1269+
not context.plan_builder("prod", skip_tests=True, enable_preview=False)
1270+
.build()
1271+
.requires_backfill
1272+
)
12671273

12681274

12691275
@time_machine.travel("2023-01-08 15:00:00 UTC")
@@ -1285,7 +1291,7 @@ def test_forward_only_precedence_over_indirect_non_breaking(init_and_plan_contex
12851291
non_breaking_snapshot = context.get_snapshot(non_breaking_model, raise_if_missing=True)
12861292
top_waiter_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True)
12871293

1288-
plan = context.plan_builder("dev", skip_tests=True).build()
1294+
plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build()
12891295
assert (
12901296
plan.context_diff.snapshots[forward_only_snapshot.snapshot_id].change_category
12911297
== SnapshotChangeCategory.FORWARD_ONLY
@@ -1315,7 +1321,11 @@ def test_forward_only_precedence_over_indirect_non_breaking(init_and_plan_contex
13151321
]
13161322

13171323
context.apply(plan)
1318-
assert not context.plan_builder("dev", skip_tests=True).build().requires_backfill
1324+
assert (
1325+
not context.plan_builder("dev", skip_tests=True, enable_preview=False)
1326+
.build()
1327+
.requires_backfill
1328+
)
13191329

13201330
# Deploy everything to prod.
13211331
plan = context.plan_builder("prod", skip_tests=True).build()
@@ -1336,7 +1346,11 @@ def test_forward_only_precedence_over_indirect_non_breaking(init_and_plan_contex
13361346
]
13371347

13381348
context.apply(plan)
1339-
assert not context.plan_builder("prod", skip_tests=True).build().requires_backfill
1349+
assert (
1350+
not context.plan_builder("prod", skip_tests=True, enable_preview=False)
1351+
.build()
1352+
.requires_backfill
1353+
)
13401354

13411355

13421356
@time_machine.travel("2023-01-08 15:00:00 UTC")
@@ -1579,12 +1593,6 @@ def test_select_models_for_backfill(init_and_plan_context: t.Callable):
15791593
context, _ = init_and_plan_context("examples/sushi")
15801594

15811595
expected_intervals = [
1582-
(to_timestamp("2023-01-01"), to_timestamp("2023-01-02")),
1583-
(to_timestamp("2023-01-02"), to_timestamp("2023-01-03")),
1584-
(to_timestamp("2023-01-03"), to_timestamp("2023-01-04")),
1585-
(to_timestamp("2023-01-04"), to_timestamp("2023-01-05")),
1586-
(to_timestamp("2023-01-05"), to_timestamp("2023-01-06")),
1587-
(to_timestamp("2023-01-06"), to_timestamp("2023-01-07")),
15881596
(to_timestamp("2023-01-07"), to_timestamp("2023-01-08")),
15891597
]
15901598

@@ -1620,7 +1628,7 @@ def test_select_models_for_backfill(init_and_plan_context: t.Callable):
16201628
dev_df = context.engine_adapter.fetchdf(
16211629
"SELECT DISTINCT event_date FROM sushi__dev.waiter_revenue_by_day ORDER BY event_date"
16221630
)
1623-
assert len(dev_df) == 7
1631+
assert len(dev_df) == 1
16241632

16251633
schema_objects = context.engine_adapter.get_data_objects("sushi__dev")
16261634
assert {o.name for o in schema_objects} == {
@@ -1854,7 +1862,7 @@ def test_indirect_non_breaking_view_model_non_representative_snapshot(
18541862
full_downstream_model_2_snapshot_id = context.get_snapshot(
18551863
view_downstream_model_name
18561864
).snapshot_id
1857-
dev_plan = context.plan("dev", auto_apply=True, no_prompts=True)
1865+
dev_plan = context.plan("dev", auto_apply=True, no_prompts=True, enable_preview=False)
18581866
assert (
18591867
dev_plan.snapshots[forward_only_model_snapshot_id].change_category
18601868
== SnapshotChangeCategory.FORWARD_ONLY
@@ -1887,7 +1895,11 @@ def test_indirect_non_breaking_view_model_non_representative_snapshot(
18871895
view_downstream_model_name
18881896
).snapshot_id
18891897
dev_plan = context.plan(
1890-
"dev", categorizer_config=CategorizerConfig.all_full(), auto_apply=True, no_prompts=True
1898+
"dev",
1899+
categorizer_config=CategorizerConfig.all_full(),
1900+
auto_apply=True,
1901+
no_prompts=True,
1902+
enable_preview=False,
18911903
)
18921904
assert (
18931905
dev_plan.snapshots[full_downstream_model_snapshot_id].change_category
@@ -1917,7 +1929,11 @@ def test_indirect_non_breaking_view_model_non_representative_snapshot(
19171929
view_downstream_model_name
19181930
).snapshot_id
19191931
dev_plan = context.plan(
1920-
"dev", categorizer_config=CategorizerConfig.all_full(), auto_apply=True, no_prompts=True
1932+
"dev",
1933+
categorizer_config=CategorizerConfig.all_full(),
1934+
auto_apply=True,
1935+
no_prompts=True,
1936+
enable_preview=False,
19211937
)
19221938
assert (
19231939
dev_plan.snapshots[full_downstream_model_snapshot_id].change_category

0 commit comments

Comments
 (0)