sqlmesh 0.188.2.dev29__py3-none-any.whl → 0.188.2.dev31__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 +7 -1
- sqlmesh/core/context.py +19 -1
- sqlmesh/core/engine_adapter/duckdb.py +2 -9
- sqlmesh/core/environment.py +13 -0
- sqlmesh/core/plan/__init__.py +1 -0
- sqlmesh/core/plan/builder.py +4 -0
- sqlmesh/core/plan/definition.py +1 -0
- sqlmesh/core/plan/evaluator.py +52 -44
- sqlmesh/core/plan/explainer.py +281 -0
- sqlmesh/core/plan/stages.py +303 -0
- sqlmesh/core/scheduler.py +51 -9
- sqlmesh/core/state_sync/db/facade.py +1 -8
- {sqlmesh-0.188.2.dev29.dist-info → sqlmesh-0.188.2.dev31.dist-info}/METADATA +2 -2
- {sqlmesh-0.188.2.dev29.dist-info → sqlmesh-0.188.2.dev31.dist-info}/RECORD +19 -17
- {sqlmesh-0.188.2.dev29.dist-info → sqlmesh-0.188.2.dev31.dist-info}/WHEEL +0 -0
- {sqlmesh-0.188.2.dev29.dist-info → sqlmesh-0.188.2.dev31.dist-info}/entry_points.txt +0 -0
- {sqlmesh-0.188.2.dev29.dist-info → sqlmesh-0.188.2.dev31.dist-info}/licenses/LICENSE +0 -0
- {sqlmesh-0.188.2.dev29.dist-info → sqlmesh-0.188.2.dev31.dist-info}/top_level.txt +0 -0
sqlmesh/_version.py
CHANGED
|
@@ -17,5 +17,5 @@ __version__: str
|
|
|
17
17
|
__version_tuple__: VERSION_TUPLE
|
|
18
18
|
version_tuple: VERSION_TUPLE
|
|
19
19
|
|
|
20
|
-
__version__ = version = '0.188.2.
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 188, 2, '
|
|
20
|
+
__version__ = version = '0.188.2.dev31'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 188, 2, 'dev31')
|
sqlmesh/cli/main.py
CHANGED
|
@@ -457,7 +457,13 @@ def diff(ctx: click.Context, environment: t.Optional[str] = None) -> None:
|
|
|
457
457
|
@click.option(
|
|
458
458
|
"--diff-rendered",
|
|
459
459
|
is_flag=True,
|
|
460
|
-
help="Output text differences for the rendered versions of the models and standalone audits",
|
|
460
|
+
help="Output text differences for the rendered versions of the models and standalone audits.",
|
|
461
|
+
default=None,
|
|
462
|
+
)
|
|
463
|
+
@click.option(
|
|
464
|
+
"--explain",
|
|
465
|
+
is_flag=True,
|
|
466
|
+
help="Explain the plan instead of applying it.",
|
|
461
467
|
default=None,
|
|
462
468
|
)
|
|
463
469
|
@opt.verbose
|
sqlmesh/core/context.py
CHANGED
|
@@ -88,7 +88,7 @@ from sqlmesh.core.notification_target import (
|
|
|
88
88
|
NotificationTarget,
|
|
89
89
|
NotificationTargetManager,
|
|
90
90
|
)
|
|
91
|
-
from sqlmesh.core.plan import Plan, PlanBuilder, SnapshotIntervals
|
|
91
|
+
from sqlmesh.core.plan import Plan, PlanBuilder, SnapshotIntervals, PlanExplainer
|
|
92
92
|
from sqlmesh.core.plan.definition import UserProvidedFlags
|
|
93
93
|
from sqlmesh.core.reference import ReferenceGraph
|
|
94
94
|
from sqlmesh.core.scheduler import Scheduler, CompletionStatus
|
|
@@ -1211,6 +1211,7 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
1211
1211
|
run: t.Optional[bool] = None,
|
|
1212
1212
|
diff_rendered: t.Optional[bool] = None,
|
|
1213
1213
|
skip_linter: t.Optional[bool] = None,
|
|
1214
|
+
explain: t.Optional[bool] = None,
|
|
1214
1215
|
) -> Plan:
|
|
1215
1216
|
"""Interactively creates a plan.
|
|
1216
1217
|
|
|
@@ -1256,6 +1257,7 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
1256
1257
|
run: Whether to run latest intervals as part of the plan application.
|
|
1257
1258
|
diff_rendered: Whether the diff should compare raw vs rendered models
|
|
1258
1259
|
skip_linter: Linter runs by default so this will skip it if enabled
|
|
1260
|
+
explain: Whether to explain the plan instead of applying it.
|
|
1259
1261
|
|
|
1260
1262
|
Returns:
|
|
1261
1263
|
The populated Plan object.
|
|
@@ -1283,6 +1285,7 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
1283
1285
|
run=run,
|
|
1284
1286
|
diff_rendered=diff_rendered,
|
|
1285
1287
|
skip_linter=skip_linter,
|
|
1288
|
+
explain=explain,
|
|
1286
1289
|
)
|
|
1287
1290
|
|
|
1288
1291
|
plan = plan_builder.build()
|
|
@@ -1292,6 +1295,9 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
1292
1295
|
# or if there are any uncategorized snapshots in the plan
|
|
1293
1296
|
no_prompts = False
|
|
1294
1297
|
|
|
1298
|
+
if explain:
|
|
1299
|
+
auto_apply = True
|
|
1300
|
+
|
|
1295
1301
|
self.console.plan(
|
|
1296
1302
|
plan_builder,
|
|
1297
1303
|
auto_apply if auto_apply is not None else self.config.plan.auto_apply,
|
|
@@ -1328,6 +1334,7 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
1328
1334
|
run: t.Optional[bool] = None,
|
|
1329
1335
|
diff_rendered: t.Optional[bool] = None,
|
|
1330
1336
|
skip_linter: t.Optional[bool] = None,
|
|
1337
|
+
explain: t.Optional[bool] = None,
|
|
1331
1338
|
) -> PlanBuilder:
|
|
1332
1339
|
"""Creates a plan builder.
|
|
1333
1340
|
|
|
@@ -1544,6 +1551,7 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
1544
1551
|
interval_end_per_model=max_interval_end_per_model,
|
|
1545
1552
|
console=self.console,
|
|
1546
1553
|
user_provided_flags=user_provided_flags,
|
|
1554
|
+
explain=explain or False,
|
|
1547
1555
|
)
|
|
1548
1556
|
|
|
1549
1557
|
def apply(
|
|
@@ -1568,6 +1576,16 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
1568
1576
|
return
|
|
1569
1577
|
if plan.uncategorized:
|
|
1570
1578
|
raise UncategorizedPlanError("Can't apply a plan with uncategorized changes.")
|
|
1579
|
+
|
|
1580
|
+
if plan.explain:
|
|
1581
|
+
explainer = PlanExplainer(
|
|
1582
|
+
state_reader=self.state_reader,
|
|
1583
|
+
default_catalog=self.default_catalog,
|
|
1584
|
+
console=self.console,
|
|
1585
|
+
)
|
|
1586
|
+
explainer.evaluate(plan.to_evaluatable())
|
|
1587
|
+
return
|
|
1588
|
+
|
|
1571
1589
|
self.notification_target_manager.notify(
|
|
1572
1590
|
NotificationEvent.APPLY_START,
|
|
1573
1591
|
environment=plan.environment_naming_info.name,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import typing as t
|
|
4
|
-
from duckdb import __version__ as duckdb_version
|
|
5
4
|
from sqlglot import exp
|
|
6
5
|
|
|
7
6
|
from sqlmesh.core.engine_adapter.mixins import (
|
|
@@ -18,7 +17,6 @@ from sqlmesh.core.engine_adapter.shared import (
|
|
|
18
17
|
SourceQuery,
|
|
19
18
|
set_catalog,
|
|
20
19
|
)
|
|
21
|
-
from sqlmesh.utils import major_minor
|
|
22
20
|
from sqlmesh.core.schema_diff import SchemaDiffer
|
|
23
21
|
|
|
24
22
|
if t.TYPE_CHECKING:
|
|
@@ -35,13 +33,8 @@ class DuckDBEngineAdapter(LogicalMergeMixin, GetCurrentCatalogFromFunctionMixin,
|
|
|
35
33
|
exp.DataType.build("DECIMAL", dialect=DIALECT).this: [(18, 3), (0,)],
|
|
36
34
|
},
|
|
37
35
|
)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
COMMENT_CREATION_TABLE, COMMENT_CREATION_VIEW = (
|
|
41
|
-
(CommentCreationTable.UNSUPPORTED, CommentCreationView.UNSUPPORTED)
|
|
42
|
-
if major_minor(duckdb_version) < (0, 10)
|
|
43
|
-
else (CommentCreationTable.COMMENT_COMMAND_ONLY, CommentCreationView.COMMENT_COMMAND_ONLY)
|
|
44
|
-
)
|
|
36
|
+
COMMENT_CREATION_TABLE = CommentCreationTable.COMMENT_COMMAND_ONLY
|
|
37
|
+
COMMENT_CREATION_VIEW = CommentCreationView.COMMENT_COMMAND_ONLY
|
|
45
38
|
|
|
46
39
|
@property
|
|
47
40
|
def catalog_support(self) -> CatalogSupport:
|
sqlmesh/core/environment.py
CHANGED
|
@@ -229,6 +229,19 @@ class Environment(EnvironmentNamingInfo, EnvironmentSummary):
|
|
|
229
229
|
finalized_ts=self.finalized_ts,
|
|
230
230
|
)
|
|
231
231
|
|
|
232
|
+
def can_partially_promote(self, existing_environment: Environment) -> bool:
|
|
233
|
+
"""Returns True if the existing environment can be partially promoted to the current environment.
|
|
234
|
+
|
|
235
|
+
Partial promotion means that we don't need to re-create views for snapshots that are already promoted in the
|
|
236
|
+
target environment.
|
|
237
|
+
"""
|
|
238
|
+
return (
|
|
239
|
+
bool(existing_environment.finalized_ts)
|
|
240
|
+
and not existing_environment.expired
|
|
241
|
+
and existing_environment.gateway_managed == self.gateway_managed
|
|
242
|
+
and existing_environment.name == c.PROD
|
|
243
|
+
)
|
|
244
|
+
|
|
232
245
|
def _convert_list_to_models_and_store(
|
|
233
246
|
self, field: str, type_: t.Type[PydanticType]
|
|
234
247
|
) -> t.Optional[t.List[PydanticType]]:
|
sqlmesh/core/plan/__init__.py
CHANGED
sqlmesh/core/plan/builder.py
CHANGED
|
@@ -83,6 +83,7 @@ class PlanBuilder:
|
|
|
83
83
|
environment state, or to use whatever snapshots are in the current environment state even if
|
|
84
84
|
the environment is not finalized.
|
|
85
85
|
interval_end_per_model: The mapping from model FQNs to target end dates.
|
|
86
|
+
explain: Whether to explain the plan instead of applying it.
|
|
86
87
|
"""
|
|
87
88
|
|
|
88
89
|
def __init__(
|
|
@@ -112,6 +113,7 @@ class PlanBuilder:
|
|
|
112
113
|
enable_preview: bool = False,
|
|
113
114
|
end_bounded: bool = False,
|
|
114
115
|
ensure_finalized_snapshots: bool = False,
|
|
116
|
+
explain: bool = False,
|
|
115
117
|
interval_end_per_model: t.Optional[t.Dict[str, int]] = None,
|
|
116
118
|
console: t.Optional[PlanBuilderConsole] = None,
|
|
117
119
|
user_provided_flags: t.Optional[t.Dict[str, UserProvidedFlags]] = None,
|
|
@@ -142,6 +144,7 @@ class PlanBuilder:
|
|
|
142
144
|
self._console = console or get_console()
|
|
143
145
|
self._choices: t.Dict[SnapshotId, SnapshotChangeCategory] = {}
|
|
144
146
|
self._user_provided_flags = user_provided_flags
|
|
147
|
+
self._explain = explain
|
|
145
148
|
|
|
146
149
|
self._start = start
|
|
147
150
|
if not self._start and (
|
|
@@ -273,6 +276,7 @@ class PlanBuilder:
|
|
|
273
276
|
empty_backfill=self._empty_backfill,
|
|
274
277
|
no_gaps=self._no_gaps,
|
|
275
278
|
forward_only=self._forward_only,
|
|
279
|
+
explain=self._explain,
|
|
276
280
|
allow_destructive_models=t.cast(t.Set, self._allow_destructive_models),
|
|
277
281
|
include_unmodified=self._include_unmodified,
|
|
278
282
|
environment_ttl=self._environment_ttl,
|
sqlmesh/core/plan/definition.py
CHANGED
sqlmesh/core/plan/evaluator.py
CHANGED
|
@@ -36,7 +36,7 @@ from sqlmesh.core.snapshot import (
|
|
|
36
36
|
SnapshotCreationFailedError,
|
|
37
37
|
)
|
|
38
38
|
from sqlmesh.utils import CompletionStatus
|
|
39
|
-
from sqlmesh.core.state_sync import StateSync
|
|
39
|
+
from sqlmesh.core.state_sync import StateSync, StateReader
|
|
40
40
|
from sqlmesh.core.state_sync.base import PromotionResult
|
|
41
41
|
from sqlmesh.utils.concurrency import NodeExecutionFailedError
|
|
42
42
|
from sqlmesh.utils.errors import PlanError
|
|
@@ -284,23 +284,7 @@ class BuiltInPlanEvaluator(PlanEvaluator):
|
|
|
284
284
|
new_snapshots=plan.new_snapshots, plan_id=plan.plan_id
|
|
285
285
|
)
|
|
286
286
|
|
|
287
|
-
|
|
288
|
-
set(plan.environment.promoted_snapshot_ids)
|
|
289
|
-
if plan.environment.promoted_snapshot_ids is not None
|
|
290
|
-
else None
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
def _should_create(s: Snapshot) -> bool:
|
|
294
|
-
if not s.is_model or s.is_symbolic:
|
|
295
|
-
return False
|
|
296
|
-
# Only create tables for snapshots that we're planning to promote or that were selected for backfill
|
|
297
|
-
return (
|
|
298
|
-
plan.is_selected_for_backfill(s.name)
|
|
299
|
-
or promoted_snapshot_ids is None
|
|
300
|
-
or s.snapshot_id in promoted_snapshot_ids
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
snapshots_to_create = [s for s in snapshots.values() if _should_create(s)]
|
|
287
|
+
snapshots_to_create = get_snapshots_to_create(plan, snapshots)
|
|
304
288
|
|
|
305
289
|
completion_status = None
|
|
306
290
|
progress_stopped = False
|
|
@@ -573,32 +557,7 @@ class BuiltInPlanEvaluator(PlanEvaluator):
|
|
|
573
557
|
plan: EvaluatablePlan,
|
|
574
558
|
new_snapshots: t.Dict[SnapshotId, Snapshot],
|
|
575
559
|
) -> None:
|
|
576
|
-
|
|
577
|
-
metadata_snapshots = []
|
|
578
|
-
for snapshot in new_snapshots.values():
|
|
579
|
-
if not snapshot.is_metadata or not snapshot.is_model or not snapshot.evaluatable:
|
|
580
|
-
continue
|
|
581
|
-
|
|
582
|
-
metadata_snapshots.append(snapshot)
|
|
583
|
-
|
|
584
|
-
# Bulk load all the previous snapshots
|
|
585
|
-
previous_snapshots = self.state_sync.get_snapshots(
|
|
586
|
-
[
|
|
587
|
-
s.previous_version.snapshot_id(s.name)
|
|
588
|
-
for s in metadata_snapshots
|
|
589
|
-
if s.previous_version
|
|
590
|
-
]
|
|
591
|
-
).values()
|
|
592
|
-
|
|
593
|
-
# Check if any of the snapshots have modifications to the audits field by comparing the hashes
|
|
594
|
-
audit_snapshots = {}
|
|
595
|
-
for snapshot, previous_snapshot in zip(metadata_snapshots, previous_snapshots):
|
|
596
|
-
new_audits_hash = snapshot.model.audit_metadata_hash()
|
|
597
|
-
previous_audit_hash = previous_snapshot.model.audit_metadata_hash()
|
|
598
|
-
|
|
599
|
-
if snapshot.model.audits and previous_audit_hash != new_audits_hash:
|
|
600
|
-
audit_snapshots[snapshot.snapshot_id] = snapshot
|
|
601
|
-
|
|
560
|
+
audit_snapshots = get_audit_only_snapshots(new_snapshots, self.state_sync)
|
|
602
561
|
if not audit_snapshots:
|
|
603
562
|
return
|
|
604
563
|
|
|
@@ -636,3 +595,52 @@ def update_intervals_for_new_snapshots(
|
|
|
636
595
|
|
|
637
596
|
if snapshots_intervals:
|
|
638
597
|
state_sync.add_snapshots_intervals(snapshots_intervals)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def get_audit_only_snapshots(
|
|
601
|
+
new_snapshots: t.Dict[SnapshotId, Snapshot], state_reader: StateReader
|
|
602
|
+
) -> t.Dict[SnapshotId, Snapshot]:
|
|
603
|
+
metadata_snapshots = []
|
|
604
|
+
for snapshot in new_snapshots.values():
|
|
605
|
+
if not snapshot.is_metadata or not snapshot.is_model or not snapshot.evaluatable:
|
|
606
|
+
continue
|
|
607
|
+
|
|
608
|
+
metadata_snapshots.append(snapshot)
|
|
609
|
+
|
|
610
|
+
# Bulk load all the previous snapshots
|
|
611
|
+
previous_snapshots = state_reader.get_snapshots(
|
|
612
|
+
[s.previous_version.snapshot_id(s.name) for s in metadata_snapshots if s.previous_version]
|
|
613
|
+
).values()
|
|
614
|
+
|
|
615
|
+
# Check if any of the snapshots have modifications to the audits field by comparing the hashes
|
|
616
|
+
audit_snapshots = {}
|
|
617
|
+
for snapshot, previous_snapshot in zip(metadata_snapshots, previous_snapshots):
|
|
618
|
+
new_audits_hash = snapshot.model.audit_metadata_hash()
|
|
619
|
+
previous_audit_hash = previous_snapshot.model.audit_metadata_hash()
|
|
620
|
+
|
|
621
|
+
if snapshot.model.audits and previous_audit_hash != new_audits_hash:
|
|
622
|
+
audit_snapshots[snapshot.snapshot_id] = snapshot
|
|
623
|
+
|
|
624
|
+
return audit_snapshots
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def get_snapshots_to_create(
|
|
628
|
+
plan: EvaluatablePlan, snapshots: t.Dict[SnapshotId, Snapshot]
|
|
629
|
+
) -> t.List[Snapshot]:
|
|
630
|
+
promoted_snapshot_ids = (
|
|
631
|
+
set(plan.environment.promoted_snapshot_ids)
|
|
632
|
+
if plan.environment.promoted_snapshot_ids is not None
|
|
633
|
+
else None
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
def _should_create(s: Snapshot) -> bool:
|
|
637
|
+
if not s.is_model or s.is_symbolic:
|
|
638
|
+
return False
|
|
639
|
+
# Only create tables for snapshots that we're planning to promote or that were selected for backfill
|
|
640
|
+
return (
|
|
641
|
+
plan.is_selected_for_backfill(s.name)
|
|
642
|
+
or promoted_snapshot_ids is None
|
|
643
|
+
or s.snapshot_id in promoted_snapshot_ids
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
return [s for s in snapshots.values() if _should_create(s)]
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import typing as t
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from rich.console import Console as RichConsole
|
|
6
|
+
from rich.tree import Tree
|
|
7
|
+
from sqlglot.dialects.dialect import DialectType
|
|
8
|
+
from sqlmesh.core import constants as c
|
|
9
|
+
from sqlmesh.core.console import Console, TerminalConsole, get_console
|
|
10
|
+
from sqlmesh.core.environment import EnvironmentNamingInfo
|
|
11
|
+
from sqlmesh.core.plan.definition import EvaluatablePlan, SnapshotIntervals
|
|
12
|
+
from sqlmesh.core.plan import stages
|
|
13
|
+
from sqlmesh.core.plan.evaluator import (
|
|
14
|
+
PlanEvaluator,
|
|
15
|
+
)
|
|
16
|
+
from sqlmesh.core.state_sync import StateReader
|
|
17
|
+
from sqlmesh.core.snapshot.definition import (
|
|
18
|
+
SnapshotInfoMixin,
|
|
19
|
+
)
|
|
20
|
+
from sqlmesh.utils import Verbosity, rich as srich
|
|
21
|
+
from sqlmesh.utils.date import to_ts
|
|
22
|
+
from sqlmesh.utils.errors import SQLMeshError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PlanExplainer(PlanEvaluator):
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
state_reader: StateReader,
|
|
32
|
+
default_catalog: t.Optional[str],
|
|
33
|
+
console: t.Optional[Console] = None,
|
|
34
|
+
):
|
|
35
|
+
self.state_reader = state_reader
|
|
36
|
+
self.default_catalog = default_catalog
|
|
37
|
+
self.console = console or get_console()
|
|
38
|
+
|
|
39
|
+
def evaluate(
|
|
40
|
+
self, plan: EvaluatablePlan, circuit_breaker: t.Optional[t.Callable[[], bool]] = None
|
|
41
|
+
) -> None:
|
|
42
|
+
plan_stages = stages.build_plan_stages(plan, self.state_reader, self.default_catalog)
|
|
43
|
+
explainer_console = _get_explainer_console(
|
|
44
|
+
self.console, plan.environment, self.default_catalog
|
|
45
|
+
)
|
|
46
|
+
explainer_console.explain(plan_stages)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ExplainerConsole(abc.ABC):
|
|
50
|
+
@abc.abstractmethod
|
|
51
|
+
def explain(self, stages: t.List[stages.PlanStage]) -> None:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
MAX_TREE_LENGTH = 10
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RichExplainerConsole(ExplainerConsole):
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
environment_naming_info: EnvironmentNamingInfo,
|
|
62
|
+
dialect: DialectType,
|
|
63
|
+
default_catalog: t.Optional[str],
|
|
64
|
+
verbosity: Verbosity = Verbosity.DEFAULT,
|
|
65
|
+
console: t.Optional[RichConsole] = None,
|
|
66
|
+
):
|
|
67
|
+
self.environment_naming_info = environment_naming_info
|
|
68
|
+
self.dialect = dialect
|
|
69
|
+
self.default_catalog = default_catalog
|
|
70
|
+
self.verbosity = verbosity
|
|
71
|
+
self.console: RichConsole = console or srich.console
|
|
72
|
+
|
|
73
|
+
def explain(self, stages: t.List[stages.PlanStage]) -> None:
|
|
74
|
+
tree = Tree("[bold]Explained plan[/bold]")
|
|
75
|
+
for stage in stages:
|
|
76
|
+
handler_name = f"visit_{_to_snake_case(stage.__class__.__name__)}"
|
|
77
|
+
if not hasattr(self, handler_name):
|
|
78
|
+
logger.error("Unexpected stage: %s", stage.__class__.__name__)
|
|
79
|
+
continue
|
|
80
|
+
handler = getattr(self, handler_name)
|
|
81
|
+
result = handler(stage)
|
|
82
|
+
if result:
|
|
83
|
+
tree.add(self._limit_tree(result))
|
|
84
|
+
self.console.print(tree)
|
|
85
|
+
|
|
86
|
+
def visit_before_all_stage(self, stage: stages.BeforeAllStage) -> Tree:
|
|
87
|
+
return Tree("[bold]Execute before all statements[/bold]")
|
|
88
|
+
|
|
89
|
+
def visit_after_all_stage(self, stage: stages.AfterAllStage) -> Tree:
|
|
90
|
+
return Tree("[bold]Execute after all statements[/bold]")
|
|
91
|
+
|
|
92
|
+
def visit_physical_layer_update_stage(self, stage: stages.PhysicalLayerUpdateStage) -> Tree:
|
|
93
|
+
if not stage.snapshots:
|
|
94
|
+
return Tree("[bold]SKIP: No physical layer updates to perform[/bold]")
|
|
95
|
+
|
|
96
|
+
tree = Tree(
|
|
97
|
+
"[bold]Validate SQL and create physical layer tables and views if they do not exist[/bold]"
|
|
98
|
+
)
|
|
99
|
+
for snapshot in stage.snapshots:
|
|
100
|
+
is_deployable = (
|
|
101
|
+
stage.deployability_index.is_deployable(snapshot)
|
|
102
|
+
if self.environment_naming_info.name != c.PROD
|
|
103
|
+
else True
|
|
104
|
+
)
|
|
105
|
+
display_name = self._display_name(snapshot)
|
|
106
|
+
table_name = snapshot.table_name(is_deployable)
|
|
107
|
+
model_tree = Tree(f"{display_name} -> {table_name}")
|
|
108
|
+
|
|
109
|
+
if snapshot.is_model:
|
|
110
|
+
if snapshot.model.pre_statements:
|
|
111
|
+
model_tree.add("Run pre-statements")
|
|
112
|
+
if snapshot.model.annotated:
|
|
113
|
+
model_tree.add("Dry run model query without inserting results")
|
|
114
|
+
|
|
115
|
+
if snapshot.is_view:
|
|
116
|
+
create_tree = Tree("Create view if it doesn't exist")
|
|
117
|
+
elif snapshot.is_forward_only and snapshot.previous_versions:
|
|
118
|
+
prod_table = snapshot.table_name(True)
|
|
119
|
+
create_tree = Tree(
|
|
120
|
+
f"Clone {prod_table} into {table_name} and then update its schema if it doesn't exist"
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
create_tree = Tree("Create table if it doesn't exist")
|
|
124
|
+
|
|
125
|
+
if not is_deployable:
|
|
126
|
+
create_tree.add("[orange1]preview[/orange1]: data will NOT be reused in production")
|
|
127
|
+
model_tree.add(create_tree)
|
|
128
|
+
|
|
129
|
+
if snapshot.is_model and snapshot.model.post_statements:
|
|
130
|
+
model_tree.add("Run post-statements")
|
|
131
|
+
|
|
132
|
+
tree.add(model_tree)
|
|
133
|
+
return tree
|
|
134
|
+
|
|
135
|
+
def visit_audit_only_run_stage(self, stage: stages.AuditOnlyRunStage) -> Tree:
|
|
136
|
+
tree = Tree("[bold]Audit-only execution[/bold]")
|
|
137
|
+
for snapshot in stage.snapshots:
|
|
138
|
+
display_name = self._display_name(snapshot)
|
|
139
|
+
tree.add(display_name)
|
|
140
|
+
return tree
|
|
141
|
+
|
|
142
|
+
def visit_restatement_stage(self, stage: stages.RestatementStage) -> Tree:
|
|
143
|
+
tree = Tree("[bold]Invalidate data intervals as part of restatement[/bold]")
|
|
144
|
+
for snapshot_table_info, interval in stage.snapshot_intervals.items():
|
|
145
|
+
display_name = self._display_name(snapshot_table_info)
|
|
146
|
+
tree.add(f"{display_name} [{to_ts(interval[0])} - {to_ts(interval[1])}]")
|
|
147
|
+
return tree
|
|
148
|
+
|
|
149
|
+
def visit_backfill_stage(self, stage: stages.BackfillStage) -> Tree:
|
|
150
|
+
if not stage.snapshot_to_intervals:
|
|
151
|
+
return Tree("[bold]SKIP: No model batches to execute[/bold]")
|
|
152
|
+
|
|
153
|
+
tree = Tree(
|
|
154
|
+
"[bold]Backfill models by running their queries and run standalone audits[/bold]"
|
|
155
|
+
)
|
|
156
|
+
for snapshot, intervals in stage.snapshot_to_intervals.items():
|
|
157
|
+
display_name = self._display_name(snapshot)
|
|
158
|
+
if snapshot.is_model:
|
|
159
|
+
table_name = snapshot.table_name(stage.deployability_index.is_deployable(snapshot))
|
|
160
|
+
model_tree = Tree(f"{display_name} -> {table_name}")
|
|
161
|
+
|
|
162
|
+
for signal_name, _ in snapshot.model.signals:
|
|
163
|
+
model_tree.add(f"Check '{signal_name}' signal")
|
|
164
|
+
|
|
165
|
+
if snapshot.model.pre_statements:
|
|
166
|
+
model_tree.add("Run pre-statements")
|
|
167
|
+
|
|
168
|
+
if snapshot.is_incremental:
|
|
169
|
+
current_intervals = (
|
|
170
|
+
snapshot.intervals
|
|
171
|
+
if stage.deployability_index.is_deployable(snapshot)
|
|
172
|
+
else snapshot.dev_intervals
|
|
173
|
+
)
|
|
174
|
+
if current_intervals:
|
|
175
|
+
formatted_range = SnapshotIntervals(
|
|
176
|
+
snapshot_id=snapshot.snapshot_id, intervals=intervals
|
|
177
|
+
).format_intervals(snapshot.node.interval_unit)
|
|
178
|
+
model_tree.add(
|
|
179
|
+
f"Incrementally insert records within the range [{formatted_range}]"
|
|
180
|
+
)
|
|
181
|
+
else:
|
|
182
|
+
# If there are no intervals, the table will be fully refreshed
|
|
183
|
+
model_tree.add("Fully refresh table")
|
|
184
|
+
elif snapshot.is_view:
|
|
185
|
+
model_tree.add("Recreate view")
|
|
186
|
+
else:
|
|
187
|
+
model_tree.add("Fully refresh table")
|
|
188
|
+
|
|
189
|
+
if snapshot.model.post_statements:
|
|
190
|
+
model_tree.add("Run post-statements")
|
|
191
|
+
|
|
192
|
+
if snapshot.model.audits:
|
|
193
|
+
for audit_name, _ in snapshot.model.audits:
|
|
194
|
+
model_tree.add(f"Run '{audit_name}' audit")
|
|
195
|
+
|
|
196
|
+
tree.add(model_tree)
|
|
197
|
+
else:
|
|
198
|
+
tree.add(f"{display_name} \[standalone audit]")
|
|
199
|
+
return tree
|
|
200
|
+
|
|
201
|
+
def visit_migrate_schemas_stage(self, stage: stages.MigrateSchemasStage) -> Tree:
|
|
202
|
+
tree = Tree(
|
|
203
|
+
"[bold]Update schemas (add, drop, alter columns) of production physical tables to reflect forward-only changes[/bold]"
|
|
204
|
+
)
|
|
205
|
+
for snapshot in stage.snapshots:
|
|
206
|
+
display_name = self._display_name(snapshot)
|
|
207
|
+
table_name = snapshot.table_name(True)
|
|
208
|
+
tree.add(f"{display_name} -> {table_name}")
|
|
209
|
+
return tree
|
|
210
|
+
|
|
211
|
+
def visit_virtual_layer_update_stage(self, stage: stages.VirtualLayerUpdateStage) -> Tree:
|
|
212
|
+
tree = Tree(
|
|
213
|
+
f"[bold]Update the virtual layer for environment '{self.environment_naming_info.name}'[/bold]"
|
|
214
|
+
)
|
|
215
|
+
promote_tree = Tree(
|
|
216
|
+
"[bold]Create or update views in the virtual layer to point at new physical tables and views[/bold]"
|
|
217
|
+
)
|
|
218
|
+
for snapshot in stage.promoted_snapshots:
|
|
219
|
+
display_name = self._display_name(snapshot)
|
|
220
|
+
table_name = snapshot.table_name(stage.deployability_index.is_representative(snapshot))
|
|
221
|
+
promote_tree.add(f"{display_name} -> {table_name}")
|
|
222
|
+
|
|
223
|
+
demote_tree = Tree(
|
|
224
|
+
"[bold]Delete views in the virtual layer for models that were removed[/bold]"
|
|
225
|
+
)
|
|
226
|
+
for snapshot in stage.demoted_snapshots:
|
|
227
|
+
display_name = self._display_name(snapshot)
|
|
228
|
+
demote_tree.add(display_name)
|
|
229
|
+
|
|
230
|
+
if stage.promoted_snapshots:
|
|
231
|
+
tree.add(self._limit_tree(promote_tree))
|
|
232
|
+
if stage.demoted_snapshots:
|
|
233
|
+
tree.add(self._limit_tree(demote_tree))
|
|
234
|
+
return tree
|
|
235
|
+
|
|
236
|
+
def visit_environment_record_update_stage(
|
|
237
|
+
self, stage: stages.EnvironmentRecordUpdateStage
|
|
238
|
+
) -> t.Optional[Tree]:
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
def _display_name(self, snapshot: SnapshotInfoMixin) -> str:
|
|
242
|
+
return snapshot.display_name(
|
|
243
|
+
self.environment_naming_info,
|
|
244
|
+
self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
|
|
245
|
+
dialect=self.dialect,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def _limit_tree(self, tree: Tree) -> Tree:
|
|
249
|
+
tree_length = len(tree.children)
|
|
250
|
+
if tree_length <= MAX_TREE_LENGTH:
|
|
251
|
+
return tree
|
|
252
|
+
if self.verbosity < Verbosity.VERY_VERBOSE:
|
|
253
|
+
tree.children = [
|
|
254
|
+
tree.children[0],
|
|
255
|
+
Tree(f".... {tree_length - 2} more ...."),
|
|
256
|
+
tree.children[-1],
|
|
257
|
+
]
|
|
258
|
+
return tree
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _get_explainer_console(
|
|
262
|
+
console: t.Optional[Console],
|
|
263
|
+
environment_naming_info: EnvironmentNamingInfo,
|
|
264
|
+
default_catalog: t.Optional[str],
|
|
265
|
+
) -> ExplainerConsole:
|
|
266
|
+
console = console or get_console()
|
|
267
|
+
if not isinstance(console, TerminalConsole):
|
|
268
|
+
raise SQLMeshError("Plain explaination is only supported in the terminal.")
|
|
269
|
+
return RichExplainerConsole(
|
|
270
|
+
environment_naming_info=environment_naming_info,
|
|
271
|
+
dialect=console.dialect,
|
|
272
|
+
default_catalog=default_catalog,
|
|
273
|
+
verbosity=console.verbosity,
|
|
274
|
+
console=console.console,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _to_snake_case(name: str) -> str:
|
|
279
|
+
return "".join(
|
|
280
|
+
f"_{c.lower()}" if c.isupper() and idx != 0 else c.lower() for idx, c in enumerate(name)
|
|
281
|
+
)
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from sqlmesh.core.plan.definition import EvaluatablePlan
|
|
5
|
+
from sqlmesh.core.plan.evaluator import (
|
|
6
|
+
get_audit_only_snapshots,
|
|
7
|
+
get_snapshots_to_create,
|
|
8
|
+
)
|
|
9
|
+
from sqlmesh.core.state_sync import StateReader
|
|
10
|
+
from sqlmesh.core.scheduler import merged_missing_intervals, SnapshotToIntervals
|
|
11
|
+
from sqlmesh.core.snapshot.definition import (
|
|
12
|
+
DeployabilityIndex,
|
|
13
|
+
Snapshot,
|
|
14
|
+
SnapshotTableInfo,
|
|
15
|
+
SnapshotId,
|
|
16
|
+
Interval,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class BeforeAllStage:
|
|
22
|
+
statements: t.List[str]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class AfterAllStage:
|
|
27
|
+
statements: t.List[str]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class PhysicalLayerUpdateStage:
|
|
32
|
+
snapshots: t.List[Snapshot]
|
|
33
|
+
deployability_index: DeployabilityIndex
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class AuditOnlyRunStage:
|
|
38
|
+
snapshots: t.List[Snapshot]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class RestatementStage:
|
|
43
|
+
snapshot_intervals: t.Dict[SnapshotTableInfo, Interval]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class BackfillStage:
|
|
48
|
+
snapshot_to_intervals: SnapshotToIntervals
|
|
49
|
+
deployability_index: DeployabilityIndex
|
|
50
|
+
before_promote: bool = True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class MigrateSchemasStage:
|
|
55
|
+
snapshots: t.List[Snapshot]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class VirtualLayerUpdateStage:
|
|
60
|
+
promoted_snapshots: t.Set[SnapshotTableInfo]
|
|
61
|
+
demoted_snapshots: t.Set[SnapshotTableInfo]
|
|
62
|
+
deployability_index: DeployabilityIndex
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class EnvironmentRecordUpdateStage:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
PlanStage = t.Union[
|
|
71
|
+
BeforeAllStage,
|
|
72
|
+
AfterAllStage,
|
|
73
|
+
PhysicalLayerUpdateStage,
|
|
74
|
+
AuditOnlyRunStage,
|
|
75
|
+
RestatementStage,
|
|
76
|
+
BackfillStage,
|
|
77
|
+
MigrateSchemasStage,
|
|
78
|
+
VirtualLayerUpdateStage,
|
|
79
|
+
EnvironmentRecordUpdateStage,
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class PlanStagesBuilder:
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
state_reader: StateReader,
|
|
87
|
+
default_catalog: t.Optional[str],
|
|
88
|
+
):
|
|
89
|
+
self.state_reader = state_reader
|
|
90
|
+
self.default_catalog = default_catalog
|
|
91
|
+
|
|
92
|
+
def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]:
|
|
93
|
+
new_snapshots = {s.snapshot_id: s for s in plan.new_snapshots}
|
|
94
|
+
stored_snapshots = self.state_reader.get_snapshots(plan.environment.snapshots)
|
|
95
|
+
snapshots = {**new_snapshots, **stored_snapshots}
|
|
96
|
+
snapshots_by_name = {s.name: s for s in snapshots.values()}
|
|
97
|
+
|
|
98
|
+
all_selected_for_backfill_snapshots = {
|
|
99
|
+
s.snapshot_id for s in snapshots.values() if plan.is_selected_for_backfill(s.name)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
deployability_index = DeployabilityIndex.create(snapshots, start=plan.start)
|
|
103
|
+
deployability_index_for_creation = deployability_index
|
|
104
|
+
if plan.is_dev:
|
|
105
|
+
before_promote_snapshots = all_selected_for_backfill_snapshots
|
|
106
|
+
after_promote_snapshots = set()
|
|
107
|
+
snapshots_with_schema_migration = []
|
|
108
|
+
else:
|
|
109
|
+
before_promote_snapshots = {
|
|
110
|
+
s.snapshot_id
|
|
111
|
+
for s in snapshots.values()
|
|
112
|
+
if deployability_index.is_representative(s)
|
|
113
|
+
and plan.is_selected_for_backfill(s.name)
|
|
114
|
+
}
|
|
115
|
+
after_promote_snapshots = all_selected_for_backfill_snapshots - before_promote_snapshots
|
|
116
|
+
deployability_index = DeployabilityIndex.all_deployable()
|
|
117
|
+
|
|
118
|
+
snapshots_with_schema_migration = [
|
|
119
|
+
s
|
|
120
|
+
for s in snapshots.values()
|
|
121
|
+
if s.is_paused
|
|
122
|
+
and s.is_materialized
|
|
123
|
+
and not deployability_index_for_creation.is_representative(s)
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
snapshots_to_intervals = self._missing_intervals(
|
|
127
|
+
plan, snapshots_by_name, deployability_index
|
|
128
|
+
)
|
|
129
|
+
needs_backfill = (
|
|
130
|
+
not plan.empty_backfill and not plan.skip_backfill and bool(snapshots_to_intervals)
|
|
131
|
+
)
|
|
132
|
+
missing_intervals_before_promote: SnapshotToIntervals = {}
|
|
133
|
+
missing_intervals_after_promote: SnapshotToIntervals = {}
|
|
134
|
+
if needs_backfill:
|
|
135
|
+
for snapshot, intervals in snapshots_to_intervals.items():
|
|
136
|
+
if snapshot.snapshot_id in before_promote_snapshots:
|
|
137
|
+
missing_intervals_before_promote[snapshot] = intervals
|
|
138
|
+
elif snapshot.snapshot_id in after_promote_snapshots:
|
|
139
|
+
missing_intervals_after_promote[snapshot] = intervals
|
|
140
|
+
|
|
141
|
+
stages: t.List[PlanStage] = []
|
|
142
|
+
|
|
143
|
+
before_all_stage = self._get_before_all_stage(plan)
|
|
144
|
+
if before_all_stage:
|
|
145
|
+
stages.append(before_all_stage)
|
|
146
|
+
|
|
147
|
+
stages.append(
|
|
148
|
+
self._get_physical_layer_update_stage(
|
|
149
|
+
plan, snapshots, snapshots_to_intervals, deployability_index_for_creation
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
audit_only_snapshots = get_audit_only_snapshots(new_snapshots, self.state_reader)
|
|
154
|
+
if audit_only_snapshots:
|
|
155
|
+
stages.append(AuditOnlyRunStage(snapshots=list(audit_only_snapshots.values())))
|
|
156
|
+
|
|
157
|
+
restatement_stage = self._get_restatement_stage(plan, snapshots_by_name)
|
|
158
|
+
if restatement_stage:
|
|
159
|
+
stages.append(restatement_stage)
|
|
160
|
+
|
|
161
|
+
if missing_intervals_before_promote:
|
|
162
|
+
stages.append(
|
|
163
|
+
BackfillStage(
|
|
164
|
+
snapshot_to_intervals=missing_intervals_before_promote,
|
|
165
|
+
deployability_index=deployability_index,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
elif not needs_backfill:
|
|
169
|
+
# Append an empty backfill stage so that explainer can show that the stage is skipped
|
|
170
|
+
stages.append(
|
|
171
|
+
BackfillStage(snapshot_to_intervals={}, deployability_index=deployability_index)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
stages.append(EnvironmentRecordUpdateStage())
|
|
175
|
+
|
|
176
|
+
if snapshots_with_schema_migration:
|
|
177
|
+
stages.append(MigrateSchemasStage(snapshots=snapshots_with_schema_migration))
|
|
178
|
+
|
|
179
|
+
if missing_intervals_after_promote:
|
|
180
|
+
stages.append(
|
|
181
|
+
BackfillStage(
|
|
182
|
+
snapshot_to_intervals=missing_intervals_after_promote,
|
|
183
|
+
deployability_index=deployability_index,
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
virtual_layer_update_stage = self._get_virtual_layer_update_stage(plan, deployability_index)
|
|
188
|
+
if virtual_layer_update_stage:
|
|
189
|
+
stages.append(virtual_layer_update_stage)
|
|
190
|
+
|
|
191
|
+
after_all_stage = self._get_after_all_stage(plan)
|
|
192
|
+
if after_all_stage:
|
|
193
|
+
stages.append(after_all_stage)
|
|
194
|
+
|
|
195
|
+
return stages
|
|
196
|
+
|
|
197
|
+
def _get_before_all_stage(self, plan: EvaluatablePlan) -> t.Optional[BeforeAllStage]:
|
|
198
|
+
before_all = [
|
|
199
|
+
statement
|
|
200
|
+
for environment_statements in plan.environment_statements or []
|
|
201
|
+
for statement in environment_statements.before_all
|
|
202
|
+
]
|
|
203
|
+
return BeforeAllStage(statements=before_all) if before_all else None
|
|
204
|
+
|
|
205
|
+
def _get_after_all_stage(self, plan: EvaluatablePlan) -> t.Optional[AfterAllStage]:
|
|
206
|
+
after_all = [
|
|
207
|
+
statement
|
|
208
|
+
for environment_statements in plan.environment_statements or []
|
|
209
|
+
for statement in environment_statements.after_all
|
|
210
|
+
]
|
|
211
|
+
return AfterAllStage(statements=after_all) if after_all else None
|
|
212
|
+
|
|
213
|
+
def _get_restatement_stage(
|
|
214
|
+
self, plan: EvaluatablePlan, snapshots_by_name: t.Dict[str, Snapshot]
|
|
215
|
+
) -> t.Optional[RestatementStage]:
|
|
216
|
+
snapshot_intervals_to_restate = {}
|
|
217
|
+
for name, interval in plan.restatements.items():
|
|
218
|
+
restated_snapshot = snapshots_by_name[name]
|
|
219
|
+
restated_snapshot.remove_interval(interval)
|
|
220
|
+
snapshot_intervals_to_restate[restated_snapshot.table_info] = interval
|
|
221
|
+
if not snapshot_intervals_to_restate or plan.is_dev:
|
|
222
|
+
return None
|
|
223
|
+
return RestatementStage(snapshot_intervals=snapshot_intervals_to_restate)
|
|
224
|
+
|
|
225
|
+
def _get_physical_layer_update_stage(
|
|
226
|
+
self,
|
|
227
|
+
plan: EvaluatablePlan,
|
|
228
|
+
snapshots: t.Dict[SnapshotId, Snapshot],
|
|
229
|
+
snapshots_to_intervals: SnapshotToIntervals,
|
|
230
|
+
deployability_index: DeployabilityIndex,
|
|
231
|
+
) -> PhysicalLayerUpdateStage:
|
|
232
|
+
snapshots_to_create = [
|
|
233
|
+
s
|
|
234
|
+
for s in get_snapshots_to_create(plan, snapshots)
|
|
235
|
+
if s in snapshots_to_intervals and s.is_model and not s.is_symbolic
|
|
236
|
+
]
|
|
237
|
+
return PhysicalLayerUpdateStage(
|
|
238
|
+
snapshots=snapshots_to_create,
|
|
239
|
+
deployability_index=deployability_index,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def _get_virtual_layer_update_stage(
|
|
243
|
+
self, plan: EvaluatablePlan, deployability_index: DeployabilityIndex
|
|
244
|
+
) -> t.Optional[VirtualLayerUpdateStage]:
|
|
245
|
+
promoted_snapshots, demoted_snapshots = self._get_promoted_demoted_snapshots(plan)
|
|
246
|
+
if not promoted_snapshots and not demoted_snapshots:
|
|
247
|
+
return None
|
|
248
|
+
return VirtualLayerUpdateStage(
|
|
249
|
+
promoted_snapshots=promoted_snapshots,
|
|
250
|
+
demoted_snapshots=demoted_snapshots,
|
|
251
|
+
deployability_index=deployability_index,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def _get_promoted_demoted_snapshots(
|
|
255
|
+
self, plan: EvaluatablePlan
|
|
256
|
+
) -> t.Tuple[t.Set[SnapshotTableInfo], t.Set[SnapshotTableInfo]]:
|
|
257
|
+
existing_environment = self.state_reader.get_environment(plan.environment.name)
|
|
258
|
+
if existing_environment:
|
|
259
|
+
snapshots_by_name = {s.name: s for s in existing_environment.snapshots}
|
|
260
|
+
demoted_snapshot_names = {s.name for s in existing_environment.promoted_snapshots} - {
|
|
261
|
+
s.name for s in plan.environment.promoted_snapshots
|
|
262
|
+
}
|
|
263
|
+
demoted_snapshots = {snapshots_by_name[name] for name in demoted_snapshot_names}
|
|
264
|
+
else:
|
|
265
|
+
demoted_snapshots = set()
|
|
266
|
+
promoted_snapshots = set(plan.environment.promoted_snapshots)
|
|
267
|
+
if existing_environment and plan.environment.can_partially_promote(existing_environment):
|
|
268
|
+
promoted_snapshots -= set(existing_environment.promoted_snapshots)
|
|
269
|
+
|
|
270
|
+
def _snapshot_filter(snapshot: SnapshotTableInfo) -> bool:
|
|
271
|
+
return snapshot.is_model and not snapshot.is_symbolic
|
|
272
|
+
|
|
273
|
+
return {s for s in promoted_snapshots if _snapshot_filter(s)}, {
|
|
274
|
+
s for s in demoted_snapshots if _snapshot_filter(s)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
def _missing_intervals(
|
|
278
|
+
self,
|
|
279
|
+
plan: EvaluatablePlan,
|
|
280
|
+
snapshots_by_name: t.Dict[str, Snapshot],
|
|
281
|
+
deployability_index: DeployabilityIndex,
|
|
282
|
+
) -> SnapshotToIntervals:
|
|
283
|
+
return merged_missing_intervals(
|
|
284
|
+
snapshots=snapshots_by_name.values(),
|
|
285
|
+
start=plan.start,
|
|
286
|
+
end=plan.end,
|
|
287
|
+
execution_time=plan.execution_time,
|
|
288
|
+
restatements={
|
|
289
|
+
snapshots_by_name[name].snapshot_id: interval
|
|
290
|
+
for name, interval in plan.restatements.items()
|
|
291
|
+
},
|
|
292
|
+
deployability_index=deployability_index,
|
|
293
|
+
end_bounded=plan.end_bounded,
|
|
294
|
+
interval_end_per_model=plan.interval_end_per_model,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def build_plan_stages(
|
|
299
|
+
plan: EvaluatablePlan,
|
|
300
|
+
state_reader: StateReader,
|
|
301
|
+
default_catalog: t.Optional[str],
|
|
302
|
+
) -> t.List[PlanStage]:
|
|
303
|
+
return PlanStagesBuilder(state_reader, default_catalog).build(plan)
|
sqlmesh/core/scheduler.py
CHANGED
|
@@ -118,16 +118,12 @@ class Scheduler:
|
|
|
118
118
|
allow_partials, and other attributes that could cause the intervals to exceed the target end date.
|
|
119
119
|
selected_snapshots: A set of snapshot names to run. If not provided, all snapshots will be run.
|
|
120
120
|
"""
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
snapshots,
|
|
127
|
-
start=start or earliest_start_date(snapshots),
|
|
128
|
-
end=end or now_timestamp(),
|
|
121
|
+
snapshots_to_intervals = merged_missing_intervals(
|
|
122
|
+
snapshots=self.snapshot_per_version.values(),
|
|
123
|
+
start=start,
|
|
124
|
+
end=end,
|
|
125
|
+
execution_time=execution_time,
|
|
129
126
|
deployability_index=deployability_index,
|
|
130
|
-
execution_time=execution_time or now_timestamp(),
|
|
131
127
|
restatements=restatements,
|
|
132
128
|
interval_end_per_model=interval_end_per_model,
|
|
133
129
|
ignore_cron=ignore_cron,
|
|
@@ -700,6 +696,52 @@ class Scheduler:
|
|
|
700
696
|
return audit_results
|
|
701
697
|
|
|
702
698
|
|
|
699
|
+
def merged_missing_intervals(
|
|
700
|
+
snapshots: t.Collection[Snapshot],
|
|
701
|
+
start: t.Optional[TimeLike] = None,
|
|
702
|
+
end: t.Optional[TimeLike] = None,
|
|
703
|
+
execution_time: t.Optional[TimeLike] = None,
|
|
704
|
+
deployability_index: t.Optional[DeployabilityIndex] = None,
|
|
705
|
+
restatements: t.Optional[t.Dict[SnapshotId, Interval]] = None,
|
|
706
|
+
interval_end_per_model: t.Optional[t.Dict[str, int]] = None,
|
|
707
|
+
ignore_cron: bool = False,
|
|
708
|
+
end_bounded: bool = False,
|
|
709
|
+
) -> SnapshotToIntervals:
|
|
710
|
+
"""Find the largest contiguous date interval parameters based only on what is missing.
|
|
711
|
+
|
|
712
|
+
For each node name, find all dependencies and look for a stored snapshot from the metastore. If a snapshot is found,
|
|
713
|
+
calculate the missing intervals that need to be processed given the passed in start and end intervals.
|
|
714
|
+
|
|
715
|
+
This is a superset of what may actually get processed at runtime based on things like batch size, signal readiness, etc.
|
|
716
|
+
|
|
717
|
+
Args:
|
|
718
|
+
snapshots: A set of target snapshots for which intervals should be computed.
|
|
719
|
+
start: The start of the run. Defaults to the min node start date.
|
|
720
|
+
end: The end of the run. Defaults to now.
|
|
721
|
+
execution_time: The date/time reference to use for execution time. Defaults to now.
|
|
722
|
+
deployability_index: Determines snapshots that are deployable in the context of this evaluation.
|
|
723
|
+
restatements: A set of snapshot names being restated.
|
|
724
|
+
interval_end_per_model: The mapping from model FQNs to target end dates.
|
|
725
|
+
ignore_cron: Whether to ignore the node's cron schedule.
|
|
726
|
+
end_bounded: If set to true, the returned intervals will be bounded by the target end date, disregarding lookback,
|
|
727
|
+
allow_partials, and other attributes that could cause the intervals to exceed the target end date.
|
|
728
|
+
"""
|
|
729
|
+
restatements = restatements or {}
|
|
730
|
+
validate_date_range(start, end)
|
|
731
|
+
|
|
732
|
+
return compute_interval_params(
|
|
733
|
+
snapshots,
|
|
734
|
+
start=start or earliest_start_date(snapshots),
|
|
735
|
+
end=end or now_timestamp(),
|
|
736
|
+
deployability_index=deployability_index,
|
|
737
|
+
execution_time=execution_time or now_timestamp(),
|
|
738
|
+
restatements=restatements,
|
|
739
|
+
interval_end_per_model=interval_end_per_model,
|
|
740
|
+
ignore_cron=ignore_cron,
|
|
741
|
+
end_bounded=end_bounded,
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
|
|
703
745
|
def compute_interval_params(
|
|
704
746
|
snapshots: t.Collection[Snapshot],
|
|
705
747
|
*,
|
|
@@ -24,7 +24,6 @@ from datetime import datetime
|
|
|
24
24
|
|
|
25
25
|
from sqlglot import exp
|
|
26
26
|
|
|
27
|
-
from sqlmesh.core import constants as c
|
|
28
27
|
from sqlmesh.core.console import Console, get_console
|
|
29
28
|
from sqlmesh.core.engine_adapter import EngineAdapter
|
|
30
29
|
from sqlmesh.core.environment import Environment, EnvironmentStatements, EnvironmentSummary
|
|
@@ -224,13 +223,7 @@ class EngineAdapterStateSync(StateSync):
|
|
|
224
223
|
}
|
|
225
224
|
|
|
226
225
|
added_table_infos = set(table_infos.values())
|
|
227
|
-
if (
|
|
228
|
-
existing_environment
|
|
229
|
-
and existing_environment.finalized_ts
|
|
230
|
-
and not existing_environment.expired
|
|
231
|
-
and existing_environment.gateway_managed == environment.gateway_managed
|
|
232
|
-
and existing_environment.name == c.PROD
|
|
233
|
-
):
|
|
226
|
+
if existing_environment and environment.can_partially_promote(existing_environment):
|
|
234
227
|
# Only promote new snapshots.
|
|
235
228
|
added_table_infos -= set(existing_environment.promoted_snapshots)
|
|
236
229
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlmesh
|
|
3
|
-
Version: 0.188.2.
|
|
3
|
+
Version: 0.188.2.dev31
|
|
4
4
|
Summary: Next-generation data transformation framework
|
|
5
5
|
Author-email: "TobikoData Inc." <engineering@tobikodata.com>
|
|
6
6
|
License: Apache License
|
|
@@ -221,7 +221,7 @@ License-File: LICENSE
|
|
|
221
221
|
Requires-Dist: astor
|
|
222
222
|
Requires-Dist: click
|
|
223
223
|
Requires-Dist: croniter
|
|
224
|
-
Requires-Dist: duckdb!=0.10.3
|
|
224
|
+
Requires-Dist: duckdb!=0.10.3,>=0.10.0
|
|
225
225
|
Requires-Dist: dateparser
|
|
226
226
|
Requires-Dist: hyperscript>=0.1.0
|
|
227
227
|
Requires-Dist: importlib-metadata; python_version < "3.12"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
sqlmesh/__init__.py,sha256=QjwaamJIdW7voXoS880yAx7tHm-Im4_mqGA-XJgafos,7631
|
|
2
|
-
sqlmesh/_version.py,sha256=
|
|
2
|
+
sqlmesh/_version.py,sha256=gzm94aWOg0XB5l8YtYf7gt6I9XBNkBbbsBL_cOtPCFo,530
|
|
3
3
|
sqlmesh/magics.py,sha256=fu7-OeT_0_TAkWZOLoiioE3-3iFpvTRxFCbQUlRnfuw,39944
|
|
4
4
|
sqlmesh/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
sqlmesh/cicd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -7,16 +7,16 @@ sqlmesh/cicd/bot.py,sha256=2zlbn-DXkqQzr3lA0__IGU4XaIfXBXBKLWXNI2DRJX8,759
|
|
|
7
7
|
sqlmesh/cicd/config.py,sha256=mpgkisSBBovlNrNZir2unT8Wes6b38RW2UsU_o4e8vY,109
|
|
8
8
|
sqlmesh/cli/__init__.py,sha256=MAXAyO7isbyM-PaSdFjkJNcjn32WjOl0XzxT5gvp_vo,1690
|
|
9
9
|
sqlmesh/cli/example_project.py,sha256=QAFQbw_kzqn30GLTthqsuWXbFDBpX8W6RsCgQ-jx65M,9308
|
|
10
|
-
sqlmesh/cli/main.py,sha256=
|
|
10
|
+
sqlmesh/cli/main.py,sha256=gk2iNJ3trprWtVpqGafbaCubYR3U6ByoacwZGFubRPM,32434
|
|
11
11
|
sqlmesh/cli/options.py,sha256=aC_L5WnMScKzMU55BD1Y-8R-QzqCkPSYrlCI-payHRA,1333
|
|
12
12
|
sqlmesh/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
sqlmesh/core/_typing.py,sha256=e1f7xO2aZCy38M_3VoDy-XI3shyd99-IFctgIFSgXWM,497
|
|
14
14
|
sqlmesh/core/console.py,sha256=x7qWETjHgu8ZpMtfW8V3bGKByT4nZXDjgtO6xmvAb-I,151143
|
|
15
15
|
sqlmesh/core/constants.py,sha256=oqt6zjepUhQoWlgapKn_HpG7Pu9mrFwepdX5caNfJUI,2499
|
|
16
|
-
sqlmesh/core/context.py,sha256=
|
|
16
|
+
sqlmesh/core/context.py,sha256=PoqGhpRxIes9Qo59YW_KggqL8Wbcj3rleH8RAQkLHT4,117312
|
|
17
17
|
sqlmesh/core/context_diff.py,sha256=I7luvY2za30E5ynVaFPaEQQEmNViJpWAmW48PGSyDCU,21135
|
|
18
18
|
sqlmesh/core/dialect.py,sha256=93cXYVuNoZ9CvJNTVn2Yz7WDOdHINwVXflbn0eZ-p14,49764
|
|
19
|
-
sqlmesh/core/environment.py,sha256=
|
|
19
|
+
sqlmesh/core/environment.py,sha256=bNj-JCG_l13dwrp--YPIuGNsuWLTMLcFA5sY8IazVK8,12917
|
|
20
20
|
sqlmesh/core/lineage.py,sha256=zjB0Zfamo2Fja2r5SSZPMbrEKTXr1WjozZPVqvxdybI,3143
|
|
21
21
|
sqlmesh/core/loader.py,sha256=L2pxQZ2OuXnBHcruJW2fsclPIrFyb-TogoWHwm_qKC8,36417
|
|
22
22
|
sqlmesh/core/macros.py,sha256=L_UlI4kysfDiYYpv78kODZw8kYbC9-FdBx7nHm1hBjs,60584
|
|
@@ -24,7 +24,7 @@ sqlmesh/core/node.py,sha256=r4_1MRTPcU0MAlwZ5KzrykLT58B7dkKvHzPCsJmEKD8,14497
|
|
|
24
24
|
sqlmesh/core/notification_target.py,sha256=PPGoDrgbRKxr27vJEu03XqNTQLYTw0ZF_b0yAapxGeI,16158
|
|
25
25
|
sqlmesh/core/reference.py,sha256=k7OSkLqTjPR8WJjNeFj0xAJ297nZUMgb_iTVwKRRKjc,4875
|
|
26
26
|
sqlmesh/core/renderer.py,sha256=KOr7rQ-RfWiS51cEj2fzdhxCrYbU0A5wNTGwKyeqLt4,26636
|
|
27
|
-
sqlmesh/core/scheduler.py,sha256=
|
|
27
|
+
sqlmesh/core/scheduler.py,sha256=pbSBaKjHwPMW8XIVDChcACv8THSjTPLA_FGwvHKB6rk,36283
|
|
28
28
|
sqlmesh/core/schema_diff.py,sha256=Sp2MoqAngp-yTYevkLaf9jb26SuAdGXoZ54g5MjWowU,30516
|
|
29
29
|
sqlmesh/core/schema_loader.py,sha256=lvrI9k7hr1P4X4E6t3U-KrcuvdQuw0UdNlN90P2s52w,3563
|
|
30
30
|
sqlmesh/core/selector.py,sha256=_hcktUQiKXCPE-mOqXClHUVWBfEZKMPVVNDtpSSRhbI,12660
|
|
@@ -64,7 +64,7 @@ sqlmesh/core/engine_adapter/base_postgres.py,sha256=DOsOdCJyQAVfA9S0GznuH0J1bonM
|
|
|
64
64
|
sqlmesh/core/engine_adapter/bigquery.py,sha256=lU2zUFuD11qVxrGXX82oUSLMkUZAs-1lWk1XdJFofXo,50234
|
|
65
65
|
sqlmesh/core/engine_adapter/clickhouse.py,sha256=LoPdm_s_s-ZxS1XJPA3Et0iOHeJ1LgqTAFbwBdiqE7A,34912
|
|
66
66
|
sqlmesh/core/engine_adapter/databricks.py,sha256=8ZExPIeT3dDh9MTJW8udvmh9LiY-f9HkRAN1Q5Qebhs,14541
|
|
67
|
-
sqlmesh/core/engine_adapter/duckdb.py,sha256=
|
|
67
|
+
sqlmesh/core/engine_adapter/duckdb.py,sha256=rFv27sF1KXhJcrIuM9Zdfl2cyZNRXEF9qQczgODdK0M,4614
|
|
68
68
|
sqlmesh/core/engine_adapter/mixins.py,sha256=HMB0Z9O3p5jaTZMolKiWKqpVDZokf5GoI99YXwEJ8bc,22470
|
|
69
69
|
sqlmesh/core/engine_adapter/mssql.py,sha256=nsBM9U6N6CmKCY8gr8PTNeQAMD8UeSM51uM9eCjlP3A,13880
|
|
70
70
|
sqlmesh/core/engine_adapter/mysql.py,sha256=lVC-siXZK3iSfqCmBjZNw8uy09Z8la8v8_oUZ-E2OwI,6966
|
|
@@ -92,10 +92,12 @@ sqlmesh/core/model/kind.py,sha256=i6_AMBarXPGPoMdF9kzBkyqS9tu__a2339-BoX7TgCM,36
|
|
|
92
92
|
sqlmesh/core/model/meta.py,sha256=TRsCztBea2gSknPQxoY1ct13hdWgD0mM9XTHqu5KDgs,21167
|
|
93
93
|
sqlmesh/core/model/schema.py,sha256=OClbAeA8LpW-IiSiLT_TlDWkEqmIIvSk0GXq8SWI-GQ,3443
|
|
94
94
|
sqlmesh/core/model/seed.py,sha256=a0M-1zY1gOkN5ph2GQyataEdBSCtq50YjeFk2LyvInI,5058
|
|
95
|
-
sqlmesh/core/plan/__init__.py,sha256=
|
|
96
|
-
sqlmesh/core/plan/builder.py,sha256=
|
|
97
|
-
sqlmesh/core/plan/definition.py,sha256=
|
|
98
|
-
sqlmesh/core/plan/evaluator.py,sha256=
|
|
95
|
+
sqlmesh/core/plan/__init__.py,sha256=aieod5xxcJJl0m7Xp9BXCwwrYnhMbeltGoNZGjwoVTA,521
|
|
96
|
+
sqlmesh/core/plan/builder.py,sha256=mZ64P0m4LDtkyy4NoPsz8gqhtezF8Piaw5aj0_YuTjA,38496
|
|
97
|
+
sqlmesh/core/plan/definition.py,sha256=pBjQu1P5U-XheWOe9WkSNAZeLzYE7LFgYtq6-4Fx67g,13236
|
|
98
|
+
sqlmesh/core/plan/evaluator.py,sha256=7dPtjxc4SuKf9fHsMDwaGsprlXmhcoKi0N6Q1N8eFY4,27009
|
|
99
|
+
sqlmesh/core/plan/explainer.py,sha256=-12__YFHEeKfvcggquZVMFq-Q3DBIy-kMvdyU5j8K5U,11237
|
|
100
|
+
sqlmesh/core/plan/stages.py,sha256=AIBzEji7OorFx9B0Hm50E6h8zWIGSuLkoVWqIhq9AVw,10911
|
|
99
101
|
sqlmesh/core/snapshot/__init__.py,sha256=Z5HTetNCZGoKF1WAKXk4LWL9dc8EC8lwS2O4TtoBmt4,1317
|
|
100
102
|
sqlmesh/core/snapshot/cache.py,sha256=bgqCR2hyf6r2A_8QP1EnXFK25gDX37-Zg0YeMuETWxg,3934
|
|
101
103
|
sqlmesh/core/snapshot/categorizer.py,sha256=qGG5870mu1jR2whKisSBH_GFbsyh96RnzJDgkN7DV9g,2482
|
|
@@ -108,7 +110,7 @@ sqlmesh/core/state_sync/common.py,sha256=THK-6AGHFBAuLXydD8QhzmzCdyl0BMrz2rB2xpm
|
|
|
108
110
|
sqlmesh/core/state_sync/export_import.py,sha256=WJ7sFX2b_RPAFE4yspJRi_4JQnaO23xpJgRxwXD0agA,8709
|
|
109
111
|
sqlmesh/core/state_sync/db/__init__.py,sha256=jGCqKYB7npq4eh6H2LyfBAJWdOP-IFu0DC4CMiQgYvw,107
|
|
110
112
|
sqlmesh/core/state_sync/db/environment.py,sha256=RKaP71CDI7YFhLYuyYVbtyBEWfwvNd2_XsDvjtrQq2Q,13645
|
|
111
|
-
sqlmesh/core/state_sync/db/facade.py,sha256=
|
|
113
|
+
sqlmesh/core/state_sync/db/facade.py,sha256=xCK8zb3mKM3c5Oa8HgId_f2iYzC7Rw5hx5bSfPmGw_g,25319
|
|
112
114
|
sqlmesh/core/state_sync/db/interval.py,sha256=qDo5qX7vulibbg9M8mDA0PjoQQJlUCouBJeWuTuLJ6Y,18043
|
|
113
115
|
sqlmesh/core/state_sync/db/migrator.py,sha256=2LNUwOBNFkA1ntp-Z93S_OWxFQhOBj3ZD_ZqPCyPEAE,18139
|
|
114
116
|
sqlmesh/core/state_sync/db/snapshot.py,sha256=7ccdRxni4UGAul-_15z6-I9GAifC94jfNTvAdUEQ9pw,29480
|
|
@@ -270,7 +272,7 @@ sqlmesh/utils/pydantic.py,sha256=I898jAFqa3qU6lG2uPpRebVFncOUo8ZrLWM_R3K3DHU,116
|
|
|
270
272
|
sqlmesh/utils/rich.py,sha256=xNZyPCqEc4TAlJlsEthg-5eYUWpaizz0QjBzO7a1Yv4,1509
|
|
271
273
|
sqlmesh/utils/windows.py,sha256=r7Yin4WEwbvH7sKA3hOJtRsclmscNuBeCklCfVPvtQs,611
|
|
272
274
|
sqlmesh/utils/yaml.py,sha256=np-WVcwmwsKghCgAj1OBBMsK1SXSRn4wR-H37cGiFr8,2282
|
|
273
|
-
sqlmesh-0.188.2.
|
|
275
|
+
sqlmesh-0.188.2.dev31.dist-info/licenses/LICENSE,sha256=OlMefUjgWJdULtf84BLW0AZZcY8DwdgQqb_1j2862j8,11346
|
|
274
276
|
web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
275
277
|
web/client/dist/index.html,sha256=LbR9XrOXE77otBPoDnaomssYz6h2TBwLANs6hUJ0p2M,5472
|
|
276
278
|
web/client/dist/assets/Audits-DB1HcP9M.js,sha256=eGI_Soj69Jntsixw1NqQpdwG94N5jipQZv7T1Y7WUSs,1041
|
|
@@ -388,8 +390,8 @@ web/server/api/endpoints/models.py,sha256=ud-lRwriyI_OYg9tZchrBgeiXELiRTLNEodraf
|
|
|
388
390
|
web/server/api/endpoints/modules.py,sha256=8hqqgonGay_mJmpCw0IdbjsPhWlQH2VLdKAqha-myac,468
|
|
389
391
|
web/server/api/endpoints/plan.py,sha256=OYuQ_4dgbhG9ibTcX1a8mvYgH3ANxw9Fhbb4wg6QWqc,9222
|
|
390
392
|
web/server/api/endpoints/table_diff.py,sha256=SCy3-pr5x6bEEiOaZGCDbiHQhpgxwmnz3Acqa2Ie6jI,2090
|
|
391
|
-
sqlmesh-0.188.2.
|
|
392
|
-
sqlmesh-0.188.2.
|
|
393
|
-
sqlmesh-0.188.2.
|
|
394
|
-
sqlmesh-0.188.2.
|
|
395
|
-
sqlmesh-0.188.2.
|
|
393
|
+
sqlmesh-0.188.2.dev31.dist-info/METADATA,sha256=XR2JL_XCGcYhXBnInxphjWqebJk2X9_KPXtW94Sgjrg,26541
|
|
394
|
+
sqlmesh-0.188.2.dev31.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
395
|
+
sqlmesh-0.188.2.dev31.dist-info/entry_points.txt,sha256=pIYKtg-u-uO7BK_Y0qrdmfyj0bYrcMc3Xiqr5WE3VFk,121
|
|
396
|
+
sqlmesh-0.188.2.dev31.dist-info/top_level.txt,sha256=bspjo5vjELQkuN0qKuTFIbsrUt3Zeo8D2mlKeHG1LdE,12
|
|
397
|
+
sqlmesh-0.188.2.dev31.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|