sqlmesh 0.209.1.dev65__py3-none-any.whl → 0.209.1.dev66__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlmesh/_version.py +2 -2
- sqlmesh/cli/main.py +8 -0
- sqlmesh/core/config/model.py +5 -0
- sqlmesh/core/console.py +39 -13
- sqlmesh/core/context.py +13 -0
- sqlmesh/core/engine_adapter/base.py +24 -10
- sqlmesh/core/engine_adapter/bigquery.py +31 -39
- sqlmesh/core/engine_adapter/clickhouse.py +5 -3
- sqlmesh/core/engine_adapter/mixins.py +65 -38
- sqlmesh/core/model/kind.py +51 -7
- sqlmesh/core/model/meta.py +6 -0
- sqlmesh/core/plan/builder.py +40 -17
- sqlmesh/core/plan/definition.py +3 -0
- sqlmesh/core/plan/evaluator.py +3 -0
- sqlmesh/core/scheduler.py +7 -0
- sqlmesh/core/schema_diff.py +256 -192
- sqlmesh/core/snapshot/definition.py +10 -0
- sqlmesh/core/snapshot/evaluator.py +101 -20
- sqlmesh/dbt/model.py +26 -12
- sqlmesh/migrations/v0091_on_additive_change.py +5 -0
- sqlmesh/utils/errors.py +76 -11
- {sqlmesh-0.209.1.dev65.dist-info → sqlmesh-0.209.1.dev66.dist-info}/METADATA +1 -1
- {sqlmesh-0.209.1.dev65.dist-info → sqlmesh-0.209.1.dev66.dist-info}/RECORD +27 -26
- {sqlmesh-0.209.1.dev65.dist-info → sqlmesh-0.209.1.dev66.dist-info}/WHEEL +0 -0
- {sqlmesh-0.209.1.dev65.dist-info → sqlmesh-0.209.1.dev66.dist-info}/entry_points.txt +0 -0
- {sqlmesh-0.209.1.dev65.dist-info → sqlmesh-0.209.1.dev66.dist-info}/licenses/LICENSE +0 -0
- {sqlmesh-0.209.1.dev65.dist-info → sqlmesh-0.209.1.dev66.dist-info}/top_level.txt +0 -0
sqlmesh/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.209.1.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 209, 1, '
|
|
31
|
+
__version__ = version = '0.209.1.dev66'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 209, 1, 'dev66')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
sqlmesh/cli/main.py
CHANGED
|
@@ -450,6 +450,12 @@ def diff(ctx: click.Context, environment: t.Optional[str] = None) -> None:
|
|
|
450
450
|
multiple=True,
|
|
451
451
|
help="Allow destructive forward-only changes to models whose names match the expression.",
|
|
452
452
|
)
|
|
453
|
+
@click.option(
|
|
454
|
+
"--allow-additive-model",
|
|
455
|
+
type=str,
|
|
456
|
+
multiple=True,
|
|
457
|
+
help="Allow additive forward-only changes to models whose names match the expression.",
|
|
458
|
+
)
|
|
453
459
|
@click.option(
|
|
454
460
|
"--effective-from",
|
|
455
461
|
type=str,
|
|
@@ -548,6 +554,7 @@ def plan(
|
|
|
548
554
|
restate_models = kwargs.pop("restate_model") or None
|
|
549
555
|
select_models = kwargs.pop("select_model") or None
|
|
550
556
|
allow_destructive_models = kwargs.pop("allow_destructive_model") or None
|
|
557
|
+
allow_additive_models = kwargs.pop("allow_additive_model") or None
|
|
551
558
|
backfill_models = kwargs.pop("backfill_model") or None
|
|
552
559
|
ignore_cron = kwargs.pop("ignore_cron") or None
|
|
553
560
|
setattr(get_console(), "verbosity", Verbosity(verbose))
|
|
@@ -557,6 +564,7 @@ def plan(
|
|
|
557
564
|
restate_models=restate_models,
|
|
558
565
|
select_models=select_models,
|
|
559
566
|
allow_destructive_models=allow_destructive_models,
|
|
567
|
+
allow_additive_models=allow_additive_models,
|
|
560
568
|
backfill_models=backfill_models,
|
|
561
569
|
ignore_cron=ignore_cron,
|
|
562
570
|
**kwargs,
|
sqlmesh/core/config/model.py
CHANGED
|
@@ -10,6 +10,8 @@ from sqlmesh.core.model.kind import (
|
|
|
10
10
|
OnDestructiveChange,
|
|
11
11
|
model_kind_validator,
|
|
12
12
|
on_destructive_change_validator,
|
|
13
|
+
on_additive_change_validator,
|
|
14
|
+
OnAdditiveChange,
|
|
13
15
|
)
|
|
14
16
|
from sqlmesh.core.model.meta import FunctionCall
|
|
15
17
|
from sqlmesh.core.node import IntervalUnit
|
|
@@ -34,6 +36,7 @@ class ModelDefaultsConfig(BaseConfig):
|
|
|
34
36
|
storage_format: The storage format used to store the physical table, only applicable in certain engines.
|
|
35
37
|
(eg. 'parquet', 'orc')
|
|
36
38
|
on_destructive_change: What should happen when a forward-only model requires a destructive schema change.
|
|
39
|
+
on_additive_change: What should happen when a forward-only model requires an additive schema change.
|
|
37
40
|
physical_properties: A key-value mapping of arbitrary properties that are applied to the model table / view in the physical layer.
|
|
38
41
|
virtual_properties: A key-value mapping of arbitrary properties that are applied to the model view in the virtual layer.
|
|
39
42
|
session_properties: A key-value mapping of properties specific to the target engine that are applied to the engine session.
|
|
@@ -56,6 +59,7 @@ class ModelDefaultsConfig(BaseConfig):
|
|
|
56
59
|
table_format: t.Optional[str] = None
|
|
57
60
|
storage_format: t.Optional[str] = None
|
|
58
61
|
on_destructive_change: t.Optional[OnDestructiveChange] = None
|
|
62
|
+
on_additive_change: t.Optional[OnAdditiveChange] = None
|
|
59
63
|
physical_properties: t.Optional[t.Dict[str, t.Any]] = None
|
|
60
64
|
virtual_properties: t.Optional[t.Dict[str, t.Any]] = None
|
|
61
65
|
session_properties: t.Optional[t.Dict[str, t.Any]] = None
|
|
@@ -71,6 +75,7 @@ class ModelDefaultsConfig(BaseConfig):
|
|
|
71
75
|
|
|
72
76
|
_model_kind_validator = model_kind_validator
|
|
73
77
|
_on_destructive_change_validator = on_destructive_change_validator
|
|
78
|
+
_on_additive_change_validator = on_additive_change_validator
|
|
74
79
|
|
|
75
80
|
@field_validator("audits", mode="before")
|
|
76
81
|
def _audits_validator(cls, v: t.Any) -> t.Any:
|
sqlmesh/core/console.py
CHANGED
|
@@ -27,6 +27,7 @@ from rich.table import Table
|
|
|
27
27
|
from rich.tree import Tree
|
|
28
28
|
from sqlglot import exp
|
|
29
29
|
|
|
30
|
+
from sqlmesh.core.schema_diff import TableAlterOperation
|
|
30
31
|
from sqlmesh.core.test.result import ModelTextTestResult
|
|
31
32
|
from sqlmesh.core.environment import EnvironmentNamingInfo, EnvironmentSummary
|
|
32
33
|
from sqlmesh.core.linter.rule import RuleViolation
|
|
@@ -47,6 +48,7 @@ from sqlmesh.utils.errors import (
|
|
|
47
48
|
PythonModelEvalError,
|
|
48
49
|
NodeAuditsErrors,
|
|
49
50
|
format_destructive_change_msg,
|
|
51
|
+
format_additive_change_msg,
|
|
50
52
|
)
|
|
51
53
|
from sqlmesh.utils.rich import strip_ansi_codes
|
|
52
54
|
|
|
@@ -327,13 +329,22 @@ class PlanBuilderConsole(BaseConsole, abc.ABC):
|
|
|
327
329
|
def log_destructive_change(
|
|
328
330
|
self,
|
|
329
331
|
snapshot_name: str,
|
|
330
|
-
|
|
331
|
-
alter_expressions: t.List[exp.Alter],
|
|
332
|
+
alter_operations: t.List[TableAlterOperation],
|
|
332
333
|
dialect: str,
|
|
333
334
|
error: bool = True,
|
|
334
335
|
) -> None:
|
|
335
336
|
"""Display a destructive change error or warning to the user."""
|
|
336
337
|
|
|
338
|
+
@abc.abstractmethod
|
|
339
|
+
def log_additive_change(
|
|
340
|
+
self,
|
|
341
|
+
snapshot_name: str,
|
|
342
|
+
alter_operations: t.List[TableAlterOperation],
|
|
343
|
+
dialect: str,
|
|
344
|
+
error: bool = True,
|
|
345
|
+
) -> None:
|
|
346
|
+
"""Display an additive change error or warning to the user."""
|
|
347
|
+
|
|
337
348
|
|
|
338
349
|
class UnitTestConsole(abc.ABC):
|
|
339
350
|
@abc.abstractmethod
|
|
@@ -759,8 +770,16 @@ class NoopConsole(Console):
|
|
|
759
770
|
def log_destructive_change(
|
|
760
771
|
self,
|
|
761
772
|
snapshot_name: str,
|
|
762
|
-
|
|
763
|
-
|
|
773
|
+
alter_operations: t.List[TableAlterOperation],
|
|
774
|
+
dialect: str,
|
|
775
|
+
error: bool = True,
|
|
776
|
+
) -> None:
|
|
777
|
+
pass
|
|
778
|
+
|
|
779
|
+
def log_additive_change(
|
|
780
|
+
self,
|
|
781
|
+
snapshot_name: str,
|
|
782
|
+
alter_operations: t.List[TableAlterOperation],
|
|
764
783
|
dialect: str,
|
|
765
784
|
error: bool = True,
|
|
766
785
|
) -> None:
|
|
@@ -2202,22 +2221,29 @@ class TerminalConsole(Console):
|
|
|
2202
2221
|
def log_destructive_change(
|
|
2203
2222
|
self,
|
|
2204
2223
|
snapshot_name: str,
|
|
2205
|
-
|
|
2206
|
-
alter_expressions: t.List[exp.Alter],
|
|
2224
|
+
alter_operations: t.List[TableAlterOperation],
|
|
2207
2225
|
dialect: str,
|
|
2208
2226
|
error: bool = True,
|
|
2209
2227
|
) -> None:
|
|
2210
2228
|
if error:
|
|
2211
|
-
self._print(
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
)
|
|
2229
|
+
self._print(format_destructive_change_msg(snapshot_name, alter_operations, dialect))
|
|
2230
|
+
else:
|
|
2231
|
+
self.log_warning(
|
|
2232
|
+
format_destructive_change_msg(snapshot_name, alter_operations, dialect, error)
|
|
2215
2233
|
)
|
|
2234
|
+
|
|
2235
|
+
def log_additive_change(
|
|
2236
|
+
self,
|
|
2237
|
+
snapshot_name: str,
|
|
2238
|
+
alter_operations: t.List[TableAlterOperation],
|
|
2239
|
+
dialect: str,
|
|
2240
|
+
error: bool = True,
|
|
2241
|
+
) -> None:
|
|
2242
|
+
if error:
|
|
2243
|
+
self._print(format_additive_change_msg(snapshot_name, alter_operations, dialect))
|
|
2216
2244
|
else:
|
|
2217
2245
|
self.log_warning(
|
|
2218
|
-
|
|
2219
|
-
snapshot_name, dropped_column_names, alter_expressions, dialect, error
|
|
2220
|
-
)
|
|
2246
|
+
format_additive_change_msg(snapshot_name, alter_operations, dialect, error)
|
|
2221
2247
|
)
|
|
2222
2248
|
|
|
2223
2249
|
def log_error(self, message: str) -> None:
|
sqlmesh/core/context.py
CHANGED
|
@@ -1279,6 +1279,7 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
1279
1279
|
empty_backfill: t.Optional[bool] = None,
|
|
1280
1280
|
forward_only: t.Optional[bool] = None,
|
|
1281
1281
|
allow_destructive_models: t.Optional[t.Collection[str]] = None,
|
|
1282
|
+
allow_additive_models: t.Optional[t.Collection[str]] = None,
|
|
1282
1283
|
no_prompts: t.Optional[bool] = None,
|
|
1283
1284
|
auto_apply: t.Optional[bool] = None,
|
|
1284
1285
|
no_auto_categorization: t.Optional[bool] = None,
|
|
@@ -1322,6 +1323,7 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
1322
1323
|
empty_backfill: Like skip_backfill, but also records processed intervals.
|
|
1323
1324
|
forward_only: Whether the purpose of the plan is to make forward only changes.
|
|
1324
1325
|
allow_destructive_models: Models whose forward-only changes are allowed to be destructive.
|
|
1326
|
+
allow_additive_models: Models whose forward-only changes are allowed to be additive.
|
|
1325
1327
|
no_prompts: Whether to disable interactive prompts for the backfill time range. Please note that
|
|
1326
1328
|
if this flag is set to true and there are uncategorized changes the plan creation will
|
|
1327
1329
|
fail. Default: False.
|
|
@@ -1360,6 +1362,7 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
1360
1362
|
empty_backfill=empty_backfill,
|
|
1361
1363
|
forward_only=forward_only,
|
|
1362
1364
|
allow_destructive_models=allow_destructive_models,
|
|
1365
|
+
allow_additive_models=allow_additive_models,
|
|
1363
1366
|
no_auto_categorization=no_auto_categorization,
|
|
1364
1367
|
effective_from=effective_from,
|
|
1365
1368
|
include_unmodified=include_unmodified,
|
|
@@ -1411,6 +1414,7 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
1411
1414
|
empty_backfill: t.Optional[bool] = None,
|
|
1412
1415
|
forward_only: t.Optional[bool] = None,
|
|
1413
1416
|
allow_destructive_models: t.Optional[t.Collection[str]] = None,
|
|
1417
|
+
allow_additive_models: t.Optional[t.Collection[str]] = None,
|
|
1414
1418
|
no_auto_categorization: t.Optional[bool] = None,
|
|
1415
1419
|
effective_from: t.Optional[TimeLike] = None,
|
|
1416
1420
|
include_unmodified: t.Optional[bool] = None,
|
|
@@ -1480,6 +1484,9 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
1480
1484
|
"allow_destructive_models": list(allow_destructive_models)
|
|
1481
1485
|
if allow_destructive_models is not None
|
|
1482
1486
|
else None,
|
|
1487
|
+
"allow_additive_models": list(allow_additive_models)
|
|
1488
|
+
if allow_additive_models is not None
|
|
1489
|
+
else None,
|
|
1483
1490
|
"no_auto_categorization": no_auto_categorization,
|
|
1484
1491
|
"effective_from": effective_from,
|
|
1485
1492
|
"include_unmodified": include_unmodified,
|
|
@@ -1533,6 +1540,11 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
1533
1540
|
else:
|
|
1534
1541
|
expanded_destructive_models = None
|
|
1535
1542
|
|
|
1543
|
+
if allow_additive_models:
|
|
1544
|
+
expanded_additive_models = model_selector.expand_model_selections(allow_additive_models)
|
|
1545
|
+
else:
|
|
1546
|
+
expanded_additive_models = None
|
|
1547
|
+
|
|
1536
1548
|
if backfill_models:
|
|
1537
1549
|
backfill_models = model_selector.expand_model_selections(backfill_models)
|
|
1538
1550
|
else:
|
|
@@ -1642,6 +1654,7 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
1642
1654
|
is_dev=is_dev,
|
|
1643
1655
|
forward_only=forward_only,
|
|
1644
1656
|
allow_destructive_models=expanded_destructive_models,
|
|
1657
|
+
allow_additive_models=expanded_additive_models,
|
|
1645
1658
|
environment_ttl=environment_ttl,
|
|
1646
1659
|
environment_suffix_target=self.config.environment_suffix_target,
|
|
1647
1660
|
environment_catalog_mapping=self.environment_catalog_mapping,
|
|
@@ -39,7 +39,7 @@ from sqlmesh.core.engine_adapter.shared import (
|
|
|
39
39
|
set_catalog,
|
|
40
40
|
)
|
|
41
41
|
from sqlmesh.core.model.kind import TimeColumn
|
|
42
|
-
from sqlmesh.core.schema_diff import SchemaDiffer
|
|
42
|
+
from sqlmesh.core.schema_diff import SchemaDiffer, TableAlterOperation
|
|
43
43
|
from sqlmesh.utils import (
|
|
44
44
|
CorrelationId,
|
|
45
45
|
columns_to_types_all_known,
|
|
@@ -376,6 +376,13 @@ class EngineAdapter:
|
|
|
376
376
|
target_columns_to_types = columns_to_types_from_df(t.cast(pd.DataFrame, query_or_df))
|
|
377
377
|
if not source_columns and target_columns_to_types:
|
|
378
378
|
source_columns = list(target_columns_to_types)
|
|
379
|
+
# source columns should only contain columns that are defined in the target. If there are extras then
|
|
380
|
+
# that means they are intended to be ignored and will be excluded
|
|
381
|
+
source_columns = (
|
|
382
|
+
[x for x in source_columns if x in target_columns_to_types]
|
|
383
|
+
if source_columns and target_columns_to_types
|
|
384
|
+
else None
|
|
385
|
+
)
|
|
379
386
|
return target_columns_to_types, source_columns
|
|
380
387
|
|
|
381
388
|
def recycle(self) -> None:
|
|
@@ -1074,32 +1081,39 @@ class EngineAdapter:
|
|
|
1074
1081
|
"""
|
|
1075
1082
|
self.execute(exp.Drop(this=exp.to_table(name), kind=kind, exists=exists, **drop_args))
|
|
1076
1083
|
|
|
1077
|
-
def
|
|
1084
|
+
def get_alter_operations(
|
|
1078
1085
|
self,
|
|
1079
1086
|
current_table_name: TableName,
|
|
1080
1087
|
target_table_name: TableName,
|
|
1081
1088
|
*,
|
|
1082
1089
|
ignore_destructive: bool = False,
|
|
1083
|
-
|
|
1090
|
+
ignore_additive: bool = False,
|
|
1091
|
+
) -> t.List[TableAlterOperation]:
|
|
1084
1092
|
"""
|
|
1085
1093
|
Determines the alter statements needed to change the current table into the structure of the target table.
|
|
1086
1094
|
"""
|
|
1087
|
-
return
|
|
1088
|
-
|
|
1089
|
-
self.
|
|
1090
|
-
|
|
1091
|
-
|
|
1095
|
+
return t.cast(
|
|
1096
|
+
t.List[TableAlterOperation],
|
|
1097
|
+
self.SCHEMA_DIFFER.compare_columns(
|
|
1098
|
+
current_table_name,
|
|
1099
|
+
self.columns(current_table_name),
|
|
1100
|
+
self.columns(target_table_name),
|
|
1101
|
+
ignore_destructive=ignore_destructive,
|
|
1102
|
+
ignore_additive=ignore_additive,
|
|
1103
|
+
),
|
|
1092
1104
|
)
|
|
1093
1105
|
|
|
1094
1106
|
def alter_table(
|
|
1095
1107
|
self,
|
|
1096
|
-
alter_expressions: t.List[exp.Alter],
|
|
1108
|
+
alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]],
|
|
1097
1109
|
) -> None:
|
|
1098
1110
|
"""
|
|
1099
1111
|
Performs the alter statements to change the current table into the structure of the target table.
|
|
1100
1112
|
"""
|
|
1101
1113
|
with self.transaction():
|
|
1102
|
-
for alter_expression in
|
|
1114
|
+
for alter_expression in [
|
|
1115
|
+
x.expression if isinstance(x, TableAlterOperation) else x for x in alter_expressions
|
|
1116
|
+
]:
|
|
1103
1117
|
self.execute(alter_expression)
|
|
1104
1118
|
|
|
1105
1119
|
def create_view(
|
|
@@ -12,6 +12,7 @@ from sqlmesh.core.engine_adapter.mixins import (
|
|
|
12
12
|
InsertOverwriteWithMergeMixin,
|
|
13
13
|
ClusteredByMixin,
|
|
14
14
|
RowDiffMixin,
|
|
15
|
+
TableAlterClusterByOperation,
|
|
15
16
|
)
|
|
16
17
|
from sqlmesh.core.engine_adapter.shared import (
|
|
17
18
|
CatalogSupport,
|
|
@@ -21,7 +22,7 @@ from sqlmesh.core.engine_adapter.shared import (
|
|
|
21
22
|
set_catalog,
|
|
22
23
|
)
|
|
23
24
|
from sqlmesh.core.node import IntervalUnit
|
|
24
|
-
from sqlmesh.core.schema_diff import SchemaDiffer
|
|
25
|
+
from sqlmesh.core.schema_diff import SchemaDiffer, TableAlterOperation
|
|
25
26
|
from sqlmesh.utils import optional_import, get_source_columns_to_types
|
|
26
27
|
from sqlmesh.utils.date import to_datetime
|
|
27
28
|
from sqlmesh.utils.errors import SQLMeshError
|
|
@@ -51,9 +52,6 @@ bigframes_pd = optional_import("bigframes.pandas")
|
|
|
51
52
|
NestedField = t.Tuple[str, str, t.List[str]]
|
|
52
53
|
NestedFieldsDict = t.Dict[str, t.List[NestedField]]
|
|
53
54
|
|
|
54
|
-
# used to tag AST nodes to be specially handled in alter_table()
|
|
55
|
-
_CLUSTERING_META_KEY = "__sqlmesh_update_table_clustering"
|
|
56
|
-
|
|
57
55
|
|
|
58
56
|
@set_catalog()
|
|
59
57
|
class BigQueryEngineAdapter(InsertOverwriteWithMergeMixin, ClusteredByMixin, RowDiffMixin):
|
|
@@ -394,29 +392,31 @@ class BigQueryEngineAdapter(InsertOverwriteWithMergeMixin, ClusteredByMixin, Row
|
|
|
394
392
|
|
|
395
393
|
def alter_table(
|
|
396
394
|
self,
|
|
397
|
-
alter_expressions: t.List[exp.Alter],
|
|
395
|
+
alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]],
|
|
398
396
|
) -> None:
|
|
399
397
|
"""
|
|
400
398
|
Performs the alter statements to change the current table into the structure of the target table,
|
|
401
399
|
and uses the API to add columns to structs, where SQL is not supported.
|
|
402
400
|
"""
|
|
401
|
+
if not alter_expressions:
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
cluster_by_operations, alter_statements = [], []
|
|
405
|
+
for e in alter_expressions:
|
|
406
|
+
if isinstance(e, TableAlterClusterByOperation):
|
|
407
|
+
cluster_by_operations.append(e)
|
|
408
|
+
elif isinstance(e, TableAlterOperation):
|
|
409
|
+
alter_statements.append(e.expression)
|
|
410
|
+
else:
|
|
411
|
+
alter_statements.append(e)
|
|
403
412
|
|
|
404
|
-
|
|
413
|
+
for op in cluster_by_operations:
|
|
414
|
+
self._update_clustering_key(op)
|
|
405
415
|
|
|
406
|
-
|
|
407
|
-
self._update_table_schema_nested_fields(nested_fields, alter_expressions[0].this)
|
|
416
|
+
nested_fields, non_nested_expressions = self._split_alter_expressions(alter_statements)
|
|
408
417
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
clustering_change_operations = [
|
|
412
|
-
e for e in non_nested_expressions if _CLUSTERING_META_KEY in e.meta
|
|
413
|
-
]
|
|
414
|
-
for op in clustering_change_operations:
|
|
415
|
-
non_nested_expressions.remove(op)
|
|
416
|
-
table, cluster_by = op.meta[_CLUSTERING_META_KEY]
|
|
417
|
-
assert isinstance(table, str) or isinstance(table, exp.Table)
|
|
418
|
-
|
|
419
|
-
self._update_clustering_key(table, cluster_by)
|
|
418
|
+
if nested_fields:
|
|
419
|
+
self._update_table_schema_nested_fields(nested_fields, alter_statements[0].this)
|
|
420
420
|
|
|
421
421
|
if non_nested_expressions:
|
|
422
422
|
super().alter_table(non_nested_expressions)
|
|
@@ -1179,35 +1179,27 @@ class BigQueryEngineAdapter(InsertOverwriteWithMergeMixin, ClusteredByMixin, Row
|
|
|
1179
1179
|
for row in df.itertuples()
|
|
1180
1180
|
]
|
|
1181
1181
|
|
|
1182
|
-
def
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
expr = super()._change_clustering_key_expr(table=table, cluster_by=cluster_by)
|
|
1186
|
-
expr.meta[_CLUSTERING_META_KEY] = (table, cluster_by)
|
|
1187
|
-
return expr
|
|
1182
|
+
def _update_clustering_key(self, operation: TableAlterClusterByOperation) -> None:
|
|
1183
|
+
cluster_key_expressions = getattr(operation, "cluster_key_expressions", [])
|
|
1184
|
+
bq_table = self._get_table(operation.target_table)
|
|
1188
1185
|
|
|
1189
|
-
|
|
1190
|
-
expr = super()._drop_clustering_key_expr(table=table)
|
|
1191
|
-
expr.meta[_CLUSTERING_META_KEY] = (table, None)
|
|
1192
|
-
return expr
|
|
1193
|
-
|
|
1194
|
-
def _update_clustering_key(
|
|
1195
|
-
self, table_name: TableName, cluster_by: t.Optional[t.List[exp.Expression]]
|
|
1196
|
-
) -> None:
|
|
1197
|
-
cluster_by = cluster_by or []
|
|
1198
|
-
bq_table = self._get_table(table_name)
|
|
1199
|
-
|
|
1200
|
-
rendered_columns = [c.sql(dialect=self.dialect) for c in cluster_by]
|
|
1186
|
+
rendered_columns = [c.sql(dialect=self.dialect) for c in cluster_key_expressions]
|
|
1201
1187
|
bq_table.clustering_fields = (
|
|
1202
1188
|
rendered_columns or None
|
|
1203
1189
|
) # causes a drop of the key if cluster_by is empty or None
|
|
1204
1190
|
|
|
1205
1191
|
self._db_call(self.client.update_table, table=bq_table, fields=["clustering_fields"])
|
|
1206
1192
|
|
|
1207
|
-
if
|
|
1193
|
+
if cluster_key_expressions:
|
|
1208
1194
|
# BigQuery only applies new clustering going forward, so this rewrites the columns to apply the new clustering to historical data
|
|
1209
1195
|
# ref: https://cloud.google.com/bigquery/docs/creating-clustered-tables#modifying-cluster-spec
|
|
1210
|
-
self.execute(
|
|
1196
|
+
self.execute(
|
|
1197
|
+
exp.update(
|
|
1198
|
+
operation.target_table,
|
|
1199
|
+
{c: c for c in cluster_key_expressions},
|
|
1200
|
+
where=exp.true(),
|
|
1201
|
+
)
|
|
1202
|
+
)
|
|
1211
1203
|
|
|
1212
1204
|
def _normalize_decimal_value(self, col: exp.Expression, precision: int) -> exp.Expression:
|
|
1213
1205
|
return exp.func("FORMAT", exp.Literal.string(f"%.{precision}f"), col)
|
|
@@ -15,7 +15,7 @@ from sqlmesh.core.engine_adapter.shared import (
|
|
|
15
15
|
CommentCreationView,
|
|
16
16
|
InsertOverwriteStrategy,
|
|
17
17
|
)
|
|
18
|
-
from sqlmesh.core.schema_diff import SchemaDiffer
|
|
18
|
+
from sqlmesh.core.schema_diff import SchemaDiffer, TableAlterOperation
|
|
19
19
|
from sqlmesh.utils import get_source_columns_to_types
|
|
20
20
|
|
|
21
21
|
if t.TYPE_CHECKING:
|
|
@@ -596,13 +596,15 @@ class ClickhouseEngineAdapter(EngineAdapterWithIndexSupport, LogicalMergeMixin):
|
|
|
596
596
|
|
|
597
597
|
def alter_table(
|
|
598
598
|
self,
|
|
599
|
-
alter_expressions: t.List[exp.Alter],
|
|
599
|
+
alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]],
|
|
600
600
|
) -> None:
|
|
601
601
|
"""
|
|
602
602
|
Performs the alter statements to change the current table into the structure of the target table.
|
|
603
603
|
"""
|
|
604
604
|
with self.transaction():
|
|
605
|
-
for alter_expression in
|
|
605
|
+
for alter_expression in [
|
|
606
|
+
x.expression if isinstance(x, TableAlterOperation) else x for x in alter_expressions
|
|
607
|
+
]:
|
|
606
608
|
if self.engine_run_mode.is_cluster:
|
|
607
609
|
alter_expression.set(
|
|
608
610
|
"cluster", exp.OnCluster(this=exp.to_identifier(self.cluster))
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import abc
|
|
3
4
|
import logging
|
|
4
5
|
import typing as t
|
|
6
|
+
from dataclasses import dataclass
|
|
5
7
|
|
|
6
8
|
from sqlglot import exp, parse_one
|
|
7
9
|
from sqlglot.helper import seq_get
|
|
@@ -10,6 +12,7 @@ from sqlmesh.core.engine_adapter.base import EngineAdapter
|
|
|
10
12
|
from sqlmesh.core.engine_adapter.shared import InsertOverwriteStrategy, SourceQuery
|
|
11
13
|
from sqlmesh.core.node import IntervalUnit
|
|
12
14
|
from sqlmesh.core.dialect import schema_
|
|
15
|
+
from sqlmesh.core.schema_diff import TableAlterOperation
|
|
13
16
|
from sqlmesh.utils.errors import SQLMeshError
|
|
14
17
|
|
|
15
18
|
if t.TYPE_CHECKING:
|
|
@@ -340,35 +343,74 @@ class VarcharSizeWorkaroundMixin(EngineAdapter):
|
|
|
340
343
|
return statement
|
|
341
344
|
|
|
342
345
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
**kwargs: t.Any,
|
|
348
|
-
) -> t.Optional[exp.Cluster]:
|
|
349
|
-
return exp.Cluster(expressions=[c.copy() for c in clustered_by])
|
|
346
|
+
@dataclass(frozen=True)
|
|
347
|
+
class TableAlterClusterByOperation(TableAlterOperation, abc.ABC):
|
|
348
|
+
pass
|
|
349
|
+
|
|
350
350
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
351
|
+
@dataclass(frozen=True)
|
|
352
|
+
class TableAlterChangeClusterKeyOperation(TableAlterClusterByOperation):
|
|
353
|
+
clustering_key: str
|
|
354
|
+
dialect: str
|
|
354
355
|
|
|
356
|
+
@property
|
|
357
|
+
def is_additive(self) -> bool:
|
|
358
|
+
return False
|
|
359
|
+
|
|
360
|
+
@property
|
|
361
|
+
def is_destructive(self) -> bool:
|
|
362
|
+
return False
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def _alter_actions(self) -> t.List[exp.Expression]:
|
|
366
|
+
return [exp.Cluster(expressions=self.cluster_key_expressions)]
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def cluster_key_expressions(self) -> t.List[exp.Expression]:
|
|
355
370
|
# Note: Assumes `clustering_key` as a string like:
|
|
356
371
|
# - "(col_a)"
|
|
357
372
|
# - "(col_a, col_b)"
|
|
358
373
|
# - "func(col_a, transform(col_b))"
|
|
359
|
-
parsed_cluster_key = parse_one(clustering_key, dialect=self.dialect)
|
|
360
|
-
|
|
374
|
+
parsed_cluster_key = parse_one(self.clustering_key, dialect=self.dialect)
|
|
361
375
|
return parsed_cluster_key.expressions or [parsed_cluster_key.this]
|
|
362
376
|
|
|
363
|
-
|
|
377
|
+
|
|
378
|
+
@dataclass(frozen=True)
|
|
379
|
+
class TableAlterDropClusterKeyOperation(TableAlterClusterByOperation):
|
|
380
|
+
@property
|
|
381
|
+
def is_additive(self) -> bool:
|
|
382
|
+
return False
|
|
383
|
+
|
|
384
|
+
@property
|
|
385
|
+
def is_destructive(self) -> bool:
|
|
386
|
+
return False
|
|
387
|
+
|
|
388
|
+
@property
|
|
389
|
+
def _alter_actions(self) -> t.List[exp.Expression]:
|
|
390
|
+
return [exp.Command(this="DROP", expression="CLUSTERING KEY")]
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class ClusteredByMixin(EngineAdapter):
|
|
394
|
+
def _build_clustered_by_exp(
|
|
395
|
+
self,
|
|
396
|
+
clustered_by: t.List[exp.Expression],
|
|
397
|
+
**kwargs: t.Any,
|
|
398
|
+
) -> t.Optional[exp.Cluster]:
|
|
399
|
+
return exp.Cluster(expressions=[c.copy() for c in clustered_by])
|
|
400
|
+
|
|
401
|
+
def get_alter_operations(
|
|
364
402
|
self,
|
|
365
403
|
current_table_name: TableName,
|
|
366
404
|
target_table_name: TableName,
|
|
367
405
|
*,
|
|
368
406
|
ignore_destructive: bool = False,
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
407
|
+
ignore_additive: bool = False,
|
|
408
|
+
) -> t.List[TableAlterOperation]:
|
|
409
|
+
operations = super().get_alter_operations(
|
|
410
|
+
current_table_name,
|
|
411
|
+
target_table_name,
|
|
412
|
+
ignore_destructive=ignore_destructive,
|
|
413
|
+
ignore_additive=ignore_additive,
|
|
372
414
|
)
|
|
373
415
|
|
|
374
416
|
# check for a change in clustering
|
|
@@ -390,32 +432,17 @@ class ClusteredByMixin(EngineAdapter):
|
|
|
390
432
|
if target_table_info.clustering_key and (
|
|
391
433
|
current_table_info.clustering_key != target_table_info.clustering_key
|
|
392
434
|
):
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
current_table,
|
|
396
|
-
|
|
435
|
+
operations.append(
|
|
436
|
+
TableAlterChangeClusterKeyOperation(
|
|
437
|
+
target_table=current_table,
|
|
438
|
+
clustering_key=target_table_info.clustering_key,
|
|
439
|
+
dialect=self.dialect,
|
|
397
440
|
)
|
|
398
441
|
)
|
|
399
442
|
elif current_table_info.is_clustered:
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
return expressions
|
|
443
|
+
operations.append(TableAlterDropClusterKeyOperation(target_table=current_table))
|
|
403
444
|
|
|
404
|
-
|
|
405
|
-
self, table: exp.Table, cluster_by: t.List[exp.Expression]
|
|
406
|
-
) -> exp.Alter:
|
|
407
|
-
return exp.Alter(
|
|
408
|
-
this=table,
|
|
409
|
-
kind="TABLE",
|
|
410
|
-
actions=[exp.Cluster(expressions=cluster_by)],
|
|
411
|
-
)
|
|
412
|
-
|
|
413
|
-
def _drop_clustering_key_expr(self, table: exp.Table) -> exp.Alter:
|
|
414
|
-
return exp.Alter(
|
|
415
|
-
this=table,
|
|
416
|
-
kind="TABLE",
|
|
417
|
-
actions=[exp.Command(this="DROP", expression="CLUSTERING KEY")],
|
|
418
|
-
)
|
|
445
|
+
return operations
|
|
419
446
|
|
|
420
447
|
|
|
421
448
|
def logical_merge(
|