Skip to content

Commit fafe4c6

Browse files
authored
Merge branch 'main' into fix/audit-condition-z-score
2 parents 8a2169b + f6527ff commit fafe4c6

19 files changed

Lines changed: 845 additions & 296 deletions

File tree

‎pnpm-lock.yaml‎

Lines changed: 153 additions & 222 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pyproject.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ requires-python = ">= 3.9"
99
dependencies = [
1010
"click",
1111
"croniter",
12-
"duckdb>=0.10.0,!=0.10.3",
12+
"duckdb>=0.10.0,!=0.10.3,<2",
1313
"dateparser<=1.2.1",
1414
"humanize",
1515
"hyperscript>=0.1.0",
@@ -121,7 +121,7 @@ fabric = ["pyodbc>=5.0.0"]
121121
fabric-mssql-python = ["mssql-python>=1.1.0;python_version>=\"3.10\""]
122122
gcppostgres = ["cloud-sql-python-connector[pg8000]>=1.8.0"]
123123
github = ["PyGithub>=2.6.0"]
124-
motherduck = ["duckdb>=1.3.2"]
124+
motherduck = ["duckdb>=1.3.2,<2"]
125125
mssql = ["pymssql"]
126126
mssql-odbc = ["pyodbc>=5.0.0"]
127127
mssql-python = ["mssql-python>=1.1.0;python_version>=\"3.10\""]

‎sqlmesh/core/context.py‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,25 @@ def load(
748748
prod = self.state_reader.get_environment(c.PROD)
749749

750750
if prod:
751-
for snapshot in self.state_reader.get_snapshots(prod.snapshots).values():
751+
# Environment snapshot infos already contain the names we need to distinguish
752+
# local nodes from nodes owned by another project. Hydrating local snapshots
753+
# here is wasteful: their payloads are not used, and a large remote state can
754+
# spend most of Context.load() decoding those model object graphs.
755+
#
756+
# Only hydrate names which are absent locally. These can be deleted nodes from
757+
# this project or nodes from another project which must be merged into the
758+
# context. The project field on the hydrated node disambiguates the two cases.
759+
remote_snapshot_infos = []
760+
for snapshot_info in prod.snapshots:
761+
local_store = (
762+
self._standalone_audits if snapshot_info.is_audit else self._models
763+
)
764+
if snapshot_info.name in local_store:
765+
uncached.add(snapshot_info.name)
766+
else:
767+
remote_snapshot_infos.append(snapshot_info)
768+
769+
for snapshot in self.state_reader.get_snapshots(remote_snapshot_infos).values():
752770
if snapshot.node.project in self._projects:
753771
uncached.add(snapshot.name)
754772
else:

‎sqlmesh/core/model/cache.py‎

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -112,22 +112,48 @@ def with_optimized_query(self, model: Model, name: t.Optional[str] = None) -> bo
112112
cache_entry = self._file_cache.get(name)
113113
if cache_entry:
114114
try:
115-
# If the optimized rendered query is None, then there are likely adapter calls in the query
116-
# that prevent us from rendering it at load time. This means that we can safely set the
117-
# unoptimized cache to None as well to prevent attempts to render it downstream.
118-
optimized = cache_entry.optimized_rendered_query is not None
119-
model._query_renderer.update_cache(
120-
cache_entry.optimized_rendered_query,
121-
cache_entry.renderer_violations,
122-
optimized=optimized,
123-
)
115+
self.with_optimized_query_entry(model, cache_entry)
124116
return True
125117
except Exception as ex:
126118
logger.warning("Failed to load a cache entry '%s': %s", name, ex)
127119

128120
self._put(name, model)
129121
return False
130122

123+
@staticmethod
124+
def with_optimized_query_entry(model: Model, cache_entry: OptimizedQueryCacheEntry) -> None:
125+
"""Adds an already-decoded optimized query cache entry to a model."""
126+
if not isinstance(model, SqlModel):
127+
return
128+
129+
# If the optimized rendered query is None, then there are likely adapter calls in the
130+
# query that prevent us from rendering it at load time. This means that we can safely
131+
# set the unoptimized cache to None as well to prevent attempts to render it downstream.
132+
optimized = cache_entry.optimized_rendered_query is not None
133+
model._query_renderer.update_cache(
134+
cache_entry.optimized_rendered_query,
135+
cache_entry.renderer_violations,
136+
optimized=optimized,
137+
)
138+
139+
def get_or_create_entry(
140+
self, model: Model, name: t.Optional[str] = None
141+
) -> t.Optional[OptimizedQueryCacheEntry]:
142+
"""Returns one decoded cache entry, creating it when necessary."""
143+
if not isinstance(model, SqlModel):
144+
return None
145+
146+
name = self._entry_name(model) if name is None else name
147+
cache_entry = self._file_cache.get(name)
148+
if cache_entry:
149+
try:
150+
self.with_optimized_query_entry(model, cache_entry)
151+
return cache_entry
152+
except Exception as ex:
153+
logger.warning("Failed to load a cache entry '%s': %s", name, ex)
154+
155+
return self._put(name, model)
156+
131157
def put(self, model: Model) -> t.Optional[str]:
132158
if not isinstance(model, SqlModel):
133159
return None
@@ -140,14 +166,15 @@ def put(self, model: Model) -> t.Optional[str]:
140166
self._put(name, model)
141167
return name
142168

143-
def _put(self, name: str, model: SqlModel) -> None:
169+
def _put(self, name: str, model: SqlModel) -> OptimizedQueryCacheEntry:
144170
optimized_query = model.render_query()
145171

146172
new_entry = OptimizedQueryCacheEntry(
147173
optimized_rendered_query=optimized_query,
148174
renderer_violations=model.violated_rules_for_query,
149175
)
150176
self._file_cache.put(name, value=new_entry)
177+
return new_entry
151178

152179
@staticmethod
153180
def _entry_name(model: SqlModel) -> str:
@@ -197,7 +224,13 @@ def load_optimized_query(
197224

198225
def load_optimized_query_and_mapping(
199226
model: Model, mapping: t.Dict
200-
) -> t.Tuple[str, t.Optional[str], str, str, t.Dict]:
227+
) -> t.Tuple[
228+
str,
229+
t.Optional[OptimizedQueryCacheEntry],
230+
str,
231+
str,
232+
t.Dict,
233+
]:
201234
assert _optimized_query_cache
202235

203236
schema = MappingSchema(normalize=False)
@@ -207,13 +240,13 @@ def load_optimized_query_and_mapping(
207240

208241
if isinstance(model, SqlModel):
209242
entry_name = _optimized_query_cache._entry_name(model)
210-
_optimized_query_cache.with_optimized_query(model, entry_name)
243+
cache_entry = _optimized_query_cache.get_or_create_entry(model, entry_name)
211244
else:
212-
entry_name = None
245+
cache_entry = None
213246

214247
return (
215248
model.fqn,
216-
entry_name,
249+
cache_entry,
217250
model.data_hash,
218251
model.metadata_hash,
219252
model.mapping_schema,

‎sqlmesh/core/model/schema.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,16 @@ def process_models(completed_model: t.Optional[Model] = None) -> None:
8787
for future in as_completed(futures):
8888
try:
8989
futures.remove(future)
90-
fqn, entry_name, data_hash, metadata_hash, mapping_schema = future.result()
90+
fqn, cache_entry, data_hash, metadata_hash, mapping_schema = future.result()
9191
model = models[fqn]
9292
model._data_hash = data_hash
9393
model._metadata_hash = metadata_hash
9494
if model.mapping_schema != mapping_schema:
9595
model.set_mapping_schema(mapping_schema)
96-
optimized_query_cache.with_optimized_query(model, entry_name)
96+
if cache_entry is not None:
97+
# The worker has already decompressed and decoded this entry. Passing
98+
# it back avoids a second gzip/pickle read in the parent process.
99+
optimized_query_cache.with_optimized_query_entry(model, cache_entry)
97100
_update_schema_with_model(schema, model)
98101
process_models(completed_model=model)
99102
except Exception as ex:

‎sqlmesh/core/plan/explainer.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88

99
from rich.console import Console as RichConsole
1010
from rich.tree import Tree
11+
from sqlglot import exp
1112
from sqlglot.dialects.dialect import DialectType
1213
from sqlmesh.core import constants as c
1314
from sqlmesh.core.console import Console, TerminalConsole, get_console
15+
from sqlmesh.core.dialect import schema_
1416
from sqlmesh.core.environment import EnvironmentNamingInfo
1517
from sqlmesh.core.plan.common import (
1618
SnapshotIntervalClearRequest,
@@ -145,6 +147,29 @@ def visit_before_all_stage(self, stage: stages.BeforeAllStage) -> Tree:
145147
def visit_after_all_stage(self, stage: stages.AfterAllStage) -> Tree:
146148
return Tree("[bold]Execute after all statements[/bold]")
147149

150+
def visit_physical_layer_schema_creation_stage(
151+
self, stage: stages.PhysicalLayerSchemaCreationStage
152+
) -> t.Optional[Tree]:
153+
tables = [
154+
exp.to_table(
155+
snapshot.table_name(is_deployable=stage.deployability_index.is_deployable(snapshot))
156+
)
157+
for snapshot in stage.snapshots
158+
if snapshot.is_model and not snapshot.is_symbolic
159+
]
160+
schemas = {
161+
schema_(table.args["db"], table.args.get("catalog")).sql(dialect=self.dialect)
162+
for table in tables
163+
if table.db
164+
}
165+
if not schemas:
166+
return None
167+
168+
tree = Tree("[bold]Create physical schemas if they do not exist[/bold]")
169+
for schema in sorted(schemas):
170+
tree.add(schema)
171+
return tree
172+
148173
def visit_physical_layer_update_stage(self, stage: stages.PhysicalLayerUpdateStage) -> Tree:
149174
snapshots = [
150175
s for s in stage.snapshots if s.snapshot_id in stage.snapshots_with_missing_intervals

0 commit comments

Comments
 (0)