Skip to content

Commit 4860651

Browse files
authored
Merge branch 'main' into feature/DRM/add-dev-container
2 parents d80a154 + ad2d113 commit 4860651

26 files changed

Lines changed: 1269 additions & 142 deletions

docs/concepts/macros/sqlmesh_macros.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1608,7 +1608,7 @@ def add_args(
16081608
return argument_1 + argument_2 + argument_3
16091609
```
16101610

1611-
An `@add_args` call providing values for all arguments accepts positional arguments like this: `@add_args(5, 6, 7)` (which returns 5 + 6 + 7 = `18`). A call omitting and using the default value for the the final `argument_3` can also use positional arguments: `@add_args(5, 6)` (which returns 5 + 6 + 3 = `14`).
1611+
An `@add_args` call providing values for all arguments accepts positional arguments like this: `@add_args(5, 6, 7)` (which returns 5 + 6 + 7 = `18`). A call omitting and using the default value for the final `argument_3` can also use positional arguments: `@add_args(5, 6)` (which returns 5 + 6 + 3 = `14`).
16121612

16131613
However, skipping an argument requires specifying the names of subsequent arguments (i.e., using "keyword arguments"). For example, skipping the second argument above by just omitting it - `@add_args(5, , 7)` - results in an error.
16141614

docs/concepts/models/model_kinds.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ MODEL (
583583

584584
### When Matched Expression
585585

586-
The logic to use when updating columns when a match occurs (the source and target match on the given keys) by default updates all the columns. This can be overriden with custom logic like below:
586+
The logic to use when updating columns when a match occurs (the source and target match on the given keys) by default updates all the columns. This can be overridden with custom logic like below:
587587

588588
```sql linenums="1" hl_lines="5"
589589
MODEL (
@@ -1437,7 +1437,7 @@ GROUP BY
14371437

14381438
SCD Type 2 models are designed by default to protect the data that has been captured because it is not possible to recreate the history once it has been lost.
14391439
However, there are cases where you may want to clear the history and start fresh.
1440-
For this use use case you will want to start by setting `disable_restatement` to `false` in the model definition.
1440+
For this use case you will want to start by setting `disable_restatement` to `false` in the model definition.
14411441

14421442
```sql linenums="1" hl_lines="5"
14431443
MODEL (

docs/guides/linter.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ Here are all of SQLMesh's built-in linting rules:
7474
| `invalidselectstarexpansion` | Correctness | The query's top-level selection may be `SELECT *`, but only if SQLMesh can expand the `SELECT *` into individual columns |
7575
| `noselectstar` | Stylistic | The query's top-level selection may not be `SELECT *`, even if SQLMesh can expand the `SELECT *` into individual columns |
7676
| `nomissingaudits` | Governance | SQLMesh did not find any `audits` in the model's configuration to test data quality. |
77+
| `nomissingunittest` | Governance | SQLMesh did not find any `unit tests` associated with the model to test |
7778

7879
### User-defined rules
7980

docs/guides/ui.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ You may include all a project's models by clicking `All` in the Show drop-down o
225225

226226
![Lineage module - all models](./ui/ui-guide_lineage-all.png){ loading=lazy }
227227

228-
Click `Connected` in the Show drop-down menu to highlight edges between upstream parents and downstream children in blue. This may be helpful when when a project contains many models:
228+
Click `Connected` in the Show drop-down menu to highlight edges between upstream parents and downstream children in blue. This may be helpful when a project contains many models:
229229

230230
![Lineage module - all models, connected edges](./ui/ui-guide_lineage-all-connected.png){ loading=lazy }
231231

docs/quickstart/notebook.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ You've now created a new production environment with all of history backfilled.
248248

249249
## 3. Update a model
250250

251-
Now that we have have populated the `prod` environment, let's modify one of the SQL models.
251+
Now that we have populated the `prod` environment, let's modify one of the SQL models.
252252

253253
We can modify the incremental SQL model using the `%model` *line* notebook magic (note the single `%`) and the model name:
254254

sqlmesh/core/console.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2920,7 +2920,10 @@ def __init__(
29202920

29212921
super().__init__(console, **kwargs)
29222922

2923-
self.display = display or get_ipython().user_ns.get("display", ipython_display)
2923+
ipython = get_ipython()
2924+
self.display = display or (
2925+
ipython.user_ns.get("display", ipython_display) if ipython else ipython_display
2926+
)
29242927
self.missing_dates_output = widgets.Output()
29252928
self.dynamic_options_after_categorization_output = widgets.VBox()
29262929

sqlmesh/core/context.py

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@
118118
filter_tests_by_patterns,
119119
)
120120
from sqlmesh.core.user import User
121-
from sqlmesh.utils import UniqueKeyDict, Verbosity
121+
from sqlmesh.utils import CorrelationId, UniqueKeyDict, Verbosity
122122
from sqlmesh.utils.concurrency import concurrent_apply_to_values
123123
from sqlmesh.utils.dag import DAG
124124
from sqlmesh.utils.date import (
@@ -811,6 +811,9 @@ def run(
811811
engine_type=self.snapshot_evaluator.adapter.dialect,
812812
state_sync_type=self.state_sync.state_type(),
813813
)
814+
snapshot_evaluator = self.snapshot_evaluator.set_correlation_id(
815+
CorrelationId.from_run_id(analytics_run_id)
816+
)
814817
self._load_materializations()
815818

816819
env_check_attempts_num = max(
@@ -863,6 +866,7 @@ def _has_environment_changed() -> bool:
863866
select_models=select_models,
864867
circuit_breaker=_has_environment_changed,
865868
no_auto_upstream=no_auto_upstream,
869+
snapshot_evaluator=snapshot_evaluator,
866870
)
867871
done = True
868872
except CircuitBreakerError:
@@ -2605,8 +2609,9 @@ def _run(
26052609
select_models: t.Optional[t.Collection[str]],
26062610
circuit_breaker: t.Optional[t.Callable[[], bool]],
26072611
no_auto_upstream: bool,
2612+
snapshot_evaluator: t.Optional[SnapshotEvaluator] = None,
26082613
) -> CompletionStatus:
2609-
scheduler = self.scheduler(environment=environment)
2614+
scheduler = self.scheduler(environment=environment, snapshot_evaluator=snapshot_evaluator)
26102615
snapshots = scheduler.snapshots
26112616

26122617
if select_models is not None:
@@ -3065,10 +3070,17 @@ def _cleanup_environments(
30653070
expired_env = self.state_reader.get_environment(expired_env_summary.name)
30663071

30673072
if expired_env:
3073+
cleanup_default_adapter, cleanup_engine_adapters, failure = (
3074+
self._cleanup_adapters_for_environment(expired_env)
3075+
)
3076+
if failure:
3077+
logger.warning(failure)
3078+
failures.append(failure)
3079+
continue
30683080
failures.extend(
30693081
cleanup_expired_views(
3070-
default_adapter=self.engine_adapter,
3071-
engine_adapters=self.engine_adapters,
3082+
default_adapter=cleanup_default_adapter,
3083+
engine_adapters=cleanup_engine_adapters,
30723084
environments=[expired_env],
30733085
console=self.console,
30743086
)
@@ -3080,6 +3092,62 @@ def _cleanup_environments(
30803092
self.state_sync.delete_expired_environments(current_ts=current_ts, name=name)
30813093
return failures
30823094

3095+
def _cleanup_adapters_for_environment(
3096+
self, environment: Environment
3097+
) -> t.Tuple[EngineAdapter, t.Dict[str, EngineAdapter], t.Optional[str]]:
3098+
"""Create cleanup-scoped adapters for an expired environment.
3099+
3100+
Persisted catalog-qualified view names indicate that virtual catalog injection was active,
3101+
so cleanup can clone only the selected adapters with the historical catalog and leave the
3102+
context's adapters unchanged.
3103+
"""
3104+
engine_adapters = self.engine_adapters
3105+
default_adapter = self.engine_adapter
3106+
catalogs_by_gateway: t.Dict[str, t.Set[str]] = collections.defaultdict(set)
3107+
3108+
for snapshot in environment.snapshots:
3109+
if not snapshot.is_model or snapshot.is_symbolic:
3110+
continue
3111+
3112+
gateway = (
3113+
snapshot.model_gateway
3114+
if environment.gateway_managed and snapshot.model_gateway in engine_adapters
3115+
else self.selected_gateway
3116+
)
3117+
adapter = engine_adapters.get(gateway, default_adapter)
3118+
catalog = snapshot.qualified_view_name.catalog_for_environment(
3119+
environment.naming_info, dialect=adapter.dialect
3120+
)
3121+
if catalog and adapter.supports_virtual_catalog() is True:
3122+
catalogs_by_gateway[gateway].add(catalog)
3123+
3124+
for gateway, catalogs in catalogs_by_gateway.items():
3125+
if len(catalogs) > 1:
3126+
catalogs_description = ", ".join(f"'{catalog}'" for catalog in sorted(catalogs))
3127+
return (
3128+
default_adapter,
3129+
engine_adapters,
3130+
(
3131+
f"Failed to clean up expired environment '{environment.name}': gateway "
3132+
f"'{gateway}' references multiple virtual catalogs: {catalogs_description}"
3133+
),
3134+
)
3135+
3136+
cleanup_engine_adapters = engine_adapters.copy()
3137+
cleanup_default_adapter = default_adapter
3138+
for gateway, catalogs in catalogs_by_gateway.items():
3139+
cleanup_adapter = engine_adapters.get(gateway, default_adapter).with_settings()
3140+
cleanup_adapter.inject_virtual_catalog(gateway)
3141+
# inject_virtual_catalog() may initialize adapter-specific state in addition to
3142+
# _default_catalog. Override only the cleanup clone with the catalog persisted in the
3143+
# expired environment so historical names pass SINGLE_CATALOG_ONLY validation.
3144+
cleanup_adapter._default_catalog = next(iter(catalogs))
3145+
cleanup_engine_adapters[gateway] = cleanup_adapter
3146+
if gateway == self.selected_gateway:
3147+
cleanup_default_adapter = cleanup_adapter
3148+
3149+
return cleanup_default_adapter, cleanup_engine_adapters, None
3150+
30833151
def _try_connection(self, connection_name: str, validator: t.Callable[[], None]) -> None:
30843152
connection_name = connection_name.capitalize()
30853153
try:

sqlmesh/core/dialect.py

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,36 @@ def _parse_table_parts(
551551
return table
552552

553553

554+
# Only needed for T-SQL: it spells a column's nullability right after its type, e.g.
555+
# ALTER TABLE t ALTER COLUMN c INT NOT NULL. Without this the trailing clause is left
556+
# over, so the whole statement falls back to a Command and any macros it contains (such as
557+
# @this_model) are no longer resolved, which means they reach the engine verbatim.
558+
#
559+
# See: https://learn.microsoft.com/en-us/sql/t-sql/statements/alter-table-transact-sql
560+
def _parse_alter_table_alter(self: Parser) -> t.Optional[exp.Expr]:
561+
alter_column = self.__parse_alter_table_alter() # type: ignore
562+
563+
if isinstance(alter_column, exp.AlterColumn) and alter_column.args.get("dtype"):
564+
if self._match_pair(TokenType.NOT, TokenType.NULL):
565+
alter_column.set("allow_null", False)
566+
elif self._match(TokenType.NULL):
567+
alter_column.set("allow_null", True)
568+
569+
return alter_column
570+
571+
572+
def altercolumn_sql(self: Generator, expression: exp.AlterColumn) -> str:
573+
sql = self._altercolumn_sql(expression) # type: ignore
574+
575+
# sqlglot's generator returns as soon as it renders the type, so the nullability parsed
576+
# above has to be appended here
577+
allow_null = expression.args.get("allow_null")
578+
if expression.args.get("dtype") and allow_null is not None:
579+
sql = f"{sql} NULL" if allow_null else f"{sql} NOT NULL"
580+
581+
return sql
582+
583+
554584
def _parse_if(self: Parser) -> t.Optional[exp.Expr]:
555585
# If we fail to parse an IF function with expressions as arguments, we then try
556586
# to parse a statement / command to support the macro @IF(condition, statement)
@@ -780,8 +810,12 @@ def _parse_interval_span(self: Parser, this: exp.Expr) -> exp.Interval:
780810
return interval
781811

782812

783-
def _override(klass: t.Type[Tokenizer | Parser], func: t.Callable) -> None:
813+
def _override(klass: t.Type[Tokenizer | Parser | Generator], func: t.Callable) -> None:
784814
name = func.__name__
815+
if getattr(klass, name, None) is func:
816+
# Already overridden. Re-applying would save the override itself as the
817+
# "original", making the wrapper call itself and recurse infinitely.
818+
return
785819
setattr(klass, f"_{name}", getattr(klass, name))
786820
setattr(klass, name, func)
787821

@@ -1170,11 +1204,12 @@ def extend_sqlglot() -> None:
11701204
MacroDef,
11711205
)
11721206

1173-
generator.UNWRAPPED_INTERVAL_VALUES = (
1174-
*generator.UNWRAPPED_INTERVAL_VALUES,
1175-
MacroStrReplace,
1176-
MacroVar,
1177-
)
1207+
if MacroVar not in generator.UNWRAPPED_INTERVAL_VALUES:
1208+
generator.UNWRAPPED_INTERVAL_VALUES = (
1209+
*generator.UNWRAPPED_INTERVAL_VALUES,
1210+
MacroStrReplace,
1211+
MacroVar,
1212+
)
11781213

11791214
_override(Parser, _parse_select)
11801215
_override(Parser, _parse_statement)
@@ -1194,6 +1229,8 @@ def extend_sqlglot() -> None:
11941229
_override(Parser, _parse_interval_span)
11951230
_override(Parser, _warn_unsupported)
11961231
_override(Snowflake.Parser, _parse_table_parts)
1232+
_override(TSQL.Parser, _parse_alter_table_alter)
1233+
_override(TSQL.Generator, altercolumn_sql)
11971234

11981235
# DuckDB's prefix absolute power operator `@` clashes with the macro syntax
11991236
DuckDB.Parser.NO_PAREN_FUNCTION_PARSERS.pop("@", None)

sqlmesh/core/engine_adapter/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ def with_settings(self, **kwargs: t.Any) -> EngineAdapter:
178178
"query_execution_tracker": kwargs.pop(
179179
"query_execution_tracker", self._query_execution_tracker
180180
),
181+
"pre_ping": kwargs.pop("pre_ping", self._pre_ping),
181182
**self._extra_config,
182183
**kwargs,
183184
}

sqlmesh/core/engine_adapter/clickhouse.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,48 @@ def _create_table(
595595
target_columns_to_types or self.columns(table_name),
596596
)
597597

598+
def create_view(
599+
self,
600+
view_name: TableName,
601+
query_or_df: QueryOrDF,
602+
target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
603+
replace: bool = True,
604+
materialized: bool = False,
605+
materialized_properties: t.Optional[t.Dict[str, t.Any]] = None,
606+
table_description: t.Optional[str] = None,
607+
column_descriptions: t.Optional[t.Dict[str, str]] = None,
608+
view_properties: t.Optional[t.Dict[str, exp.Expr]] = None,
609+
source_columns: t.Optional[t.List[str]] = None,
610+
**create_kwargs: t.Any,
611+
) -> None:
612+
if self._default_catalog and isinstance(query_or_df, exp.Query):
613+
from sqlmesh.utils.errors import SQLMeshError
614+
615+
query_or_df = query_or_df.copy()
616+
for table in query_or_df.find_all(exp.Table):
617+
if not table.catalog:
618+
continue
619+
if table.catalog != self._default_catalog:
620+
raise SQLMeshError(
621+
f"{self.dialect} requires that all catalog operations be against a single "
622+
f"catalog: {self._default_catalog}. Provided catalog: {table.catalog}"
623+
)
624+
table.set("catalog", None)
625+
626+
super().create_view(
627+
view_name,
628+
query_or_df,
629+
target_columns_to_types=target_columns_to_types,
630+
replace=replace,
631+
materialized=materialized,
632+
materialized_properties=materialized_properties,
633+
table_description=table_description,
634+
column_descriptions=column_descriptions,
635+
view_properties=view_properties,
636+
source_columns=source_columns,
637+
**create_kwargs,
638+
)
639+
598640
def _strip_virtual_catalog(self, name: "TableName") -> exp.Table:
599641
"""Strip the virtual catalog prefix from a table name if present.
600642
@@ -850,7 +892,7 @@ def _build_table_properties_exp(
850892
primary_key_vals = []
851893
if isinstance(primary_key, (exp.Tuple, exp.Array)):
852894
primary_key_vals = primary_key.expressions
853-
if isinstance(ordered_by_raw, exp.Paren):
895+
if isinstance(primary_key, exp.Paren):
854896
primary_key_vals = [primary_key.this]
855897

856898
if not primary_key_vals:

0 commit comments

Comments
 (0)