sqlmesh 0.209.1.dev12__py3-none-any.whl → 0.209.1.dev14__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.
Files changed (35) hide show
  1. sqlmesh/_version.py +2 -2
  2. sqlmesh/core/audit/definition.py +5 -3
  3. sqlmesh/core/config/common.py +29 -0
  4. sqlmesh/core/config/root.py +14 -7
  5. sqlmesh/core/constants.py +2 -0
  6. sqlmesh/core/context.py +7 -4
  7. sqlmesh/core/engine_adapter/base.py +73 -5
  8. sqlmesh/core/engine_adapter/redshift.py +6 -1
  9. sqlmesh/core/engine_adapter/shared.py +3 -0
  10. sqlmesh/core/loader.py +2 -0
  11. sqlmesh/core/macros.py +16 -3
  12. sqlmesh/core/model/common.py +198 -41
  13. sqlmesh/core/model/decorator.py +3 -0
  14. sqlmesh/core/model/definition.py +20 -11
  15. sqlmesh/core/model/meta.py +2 -0
  16. sqlmesh/core/plan/builder.py +40 -67
  17. sqlmesh/core/plan/common.py +19 -0
  18. sqlmesh/core/plan/stages.py +47 -8
  19. sqlmesh/core/snapshot/definition.py +49 -15
  20. sqlmesh/core/snapshot/evaluator.py +25 -4
  21. sqlmesh/core/state_sync/db/snapshot.py +3 -4
  22. sqlmesh/dbt/basemodel.py +5 -1
  23. sqlmesh/dbt/loader.py +5 -1
  24. sqlmesh/dbt/model.py +6 -1
  25. sqlmesh/dbt/seed.py +6 -1
  26. sqlmesh/migrations/v0088_add_virtual_environment_mode.py +5 -0
  27. sqlmesh/migrations/v0088_warn_about_variable_python_env_diffs.py +74 -0
  28. sqlmesh/utils/jinja.py +1 -1
  29. sqlmesh/utils/metaprogramming.py +12 -5
  30. {sqlmesh-0.209.1.dev12.dist-info → sqlmesh-0.209.1.dev14.dist-info}/METADATA +1 -1
  31. {sqlmesh-0.209.1.dev12.dist-info → sqlmesh-0.209.1.dev14.dist-info}/RECORD +35 -32
  32. {sqlmesh-0.209.1.dev12.dist-info → sqlmesh-0.209.1.dev14.dist-info}/WHEEL +0 -0
  33. {sqlmesh-0.209.1.dev12.dist-info → sqlmesh-0.209.1.dev14.dist-info}/entry_points.txt +0 -0
  34. {sqlmesh-0.209.1.dev12.dist-info → sqlmesh-0.209.1.dev14.dist-info}/licenses/LICENSE +0 -0
  35. {sqlmesh-0.209.1.dev12.dist-info → sqlmesh-0.209.1.dev14.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.dev12'
32
- __version_tuple__ = version_tuple = (0, 209, 1, 'dev12')
31
+ __version__ = version = '0.209.1.dev14'
32
+ __version_tuple__ = version_tuple = (0, 209, 1, 'dev14')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -438,13 +438,15 @@ def load_audit(
438
438
 
439
439
  extra_kwargs: t.Dict[str, t.Any] = {}
440
440
  if is_standalone:
441
- jinja_macro_refrences, used_variables = extract_macro_references_and_variables(
441
+ jinja_macro_refrences, referenced_variables = extract_macro_references_and_variables(
442
442
  *(gen(s) for s in statements),
443
443
  gen(query),
444
444
  )
445
445
  jinja_macros = (jinja_macros or JinjaMacroRegistry()).trim(jinja_macro_refrences)
446
446
  for jinja_macro in jinja_macros.root_macros.values():
447
- used_variables.update(extract_macro_references_and_variables(jinja_macro.definition)[1])
447
+ referenced_variables.update(
448
+ extract_macro_references_and_variables(jinja_macro.definition)[1]
449
+ )
448
450
 
449
451
  extra_kwargs["jinja_macros"] = jinja_macros
450
452
  extra_kwargs["python_env"] = make_python_env(
@@ -453,7 +455,7 @@ def load_audit(
453
455
  module_path,
454
456
  macros or macro.get_registry(),
455
457
  variables=variables,
456
- used_variables=used_variables,
458
+ referenced_variables=referenced_variables,
457
459
  )
458
460
  extra_kwargs["default_catalog"] = default_catalog
459
461
  if project is not None:
@@ -49,6 +49,35 @@ class EnvironmentSuffixTarget(str, Enum):
49
49
  return str(self)
50
50
 
51
51
 
52
+ class VirtualEnvironmentMode(str, Enum):
53
+ """Mode for virtual environment behavior.
54
+
55
+ FULL: Use full virtual environment functionality with versioned table names and virtual layer updates.
56
+ DEV_ONLY: Bypass virtual environments in production, using original unversioned model names.
57
+ """
58
+
59
+ FULL = "full"
60
+ DEV_ONLY = "dev_only"
61
+
62
+ @property
63
+ def is_full(self) -> bool:
64
+ return self == VirtualEnvironmentMode.FULL
65
+
66
+ @property
67
+ def is_dev_only(self) -> bool:
68
+ return self == VirtualEnvironmentMode.DEV_ONLY
69
+
70
+ @classproperty
71
+ def default(cls) -> VirtualEnvironmentMode:
72
+ return VirtualEnvironmentMode.FULL
73
+
74
+ def __str__(self) -> str:
75
+ return self.name
76
+
77
+ def __repr__(self) -> str:
78
+ return str(self)
79
+
80
+
52
81
  class TableNamingConvention(str, Enum):
53
82
  # Causes table names at the physical layer to follow the convention:
54
83
  # <schema-name>__<table-name>__<fingerprint>
@@ -14,7 +14,11 @@ from sqlglot.optimizer.normalize_identifiers import normalize_identifiers
14
14
  from sqlmesh.cicd.config import CICDBotConfig
15
15
  from sqlmesh.core import constants as c
16
16
  from sqlmesh.core.console import get_console
17
- from sqlmesh.core.config import EnvironmentSuffixTarget, TableNamingConvention
17
+ from sqlmesh.core.config.common import (
18
+ EnvironmentSuffixTarget,
19
+ TableNamingConvention,
20
+ VirtualEnvironmentMode,
21
+ )
18
22
  from sqlmesh.core.config.base import BaseConfig, UpdateStrategy
19
23
  from sqlmesh.core.config.common import variables_validator, compile_regex_mapping
20
24
  from sqlmesh.core.config.connection import (
@@ -110,6 +114,7 @@ class Config(BaseConfig):
110
114
  physical_schema_mapping: A mapping from regular expressions to names of schemas in which physical tables for corresponding models will be placed.
111
115
  environment_suffix_target: Indicates whether to append the environment name to the schema or table name.
112
116
  physical_table_naming_convention: Indicates how tables should be named at the physical layer
117
+ virtual_environment_mode: Indicates how environments should be handled.
113
118
  gateway_managed_virtual_layer: Whether the models' views in the virtual layer are created by the model-specific gateway rather than the default gateway.
114
119
  infer_python_dependencies: Whether to statically analyze Python code to automatically infer Python package requirements.
115
120
  environment_catalog_mapping: A mapping from regular expressions to catalog names. The catalog name is used to determine the target catalog for a given environment.
@@ -148,12 +153,9 @@ class Config(BaseConfig):
148
153
  env_vars: t.Dict[str, str] = {}
149
154
  username: str = ""
150
155
  physical_schema_mapping: RegexKeyDict = {}
151
- environment_suffix_target: EnvironmentSuffixTarget = Field(
152
- default=EnvironmentSuffixTarget.default
153
- )
154
- physical_table_naming_convention: TableNamingConvention = Field(
155
- default=TableNamingConvention.default
156
- )
156
+ environment_suffix_target: EnvironmentSuffixTarget = EnvironmentSuffixTarget.default
157
+ physical_table_naming_convention: TableNamingConvention = TableNamingConvention.default
158
+ virtual_environment_mode: VirtualEnvironmentMode = VirtualEnvironmentMode.default
157
159
  gateway_managed_virtual_layer: bool = False
158
160
  infer_python_dependencies: bool = True
159
161
  environment_catalog_mapping: RegexKeyDict = {}
@@ -260,6 +262,11 @@ class Config(BaseConfig):
260
262
  "Please specify one or the other"
261
263
  )
262
264
 
265
+ if self.plan.use_finalized_state and not self.virtual_environment_mode.is_full:
266
+ raise ConfigError(
267
+ "Using the finalized state is only supported when `virtual_environment_mode` is set to `full`."
268
+ )
269
+
263
270
  if self.environment_catalog_mapping:
264
271
  _normalize_identifiers("environment_catalog_mapping")
265
272
  if self.physical_schema_mapping:
sqlmesh/core/constants.py CHANGED
@@ -80,7 +80,9 @@ REQUIREMENTS = "sqlmesh-requirements.lock"
80
80
  DEFAULT_SCHEMA = "default"
81
81
 
82
82
  SQLMESH_VARS = "__sqlmesh__vars__"
83
+ SQLMESH_VARS_METADATA = "__sqlmesh__vars__metadata__"
83
84
  SQLMESH_BLUEPRINT_VARS = "__sqlmesh__blueprint__vars__"
85
+ SQLMESH_BLUEPRINT_VARS_METADATA = "__sqlmesh__blueprint__vars__metadata__"
84
86
 
85
87
  VAR = "var"
86
88
  BLUEPRINT_VAR = "blueprint_var"
sqlmesh/core/context.py CHANGED
@@ -1616,6 +1616,11 @@ class GenericContext(BaseContext, t.Generic[C]):
1616
1616
  max_interval_end_per_model,
1617
1617
  )
1618
1618
 
1619
+ if not self.config.virtual_environment_mode.is_full:
1620
+ forward_only = True
1621
+ elif forward_only is None:
1622
+ forward_only = self.config.plan.forward_only
1623
+
1619
1624
  return self.PLAN_BUILDER_TYPE(
1620
1625
  context_diff=context_diff,
1621
1626
  start=start,
@@ -1628,9 +1633,7 @@ class GenericContext(BaseContext, t.Generic[C]):
1628
1633
  skip_backfill=skip_backfill,
1629
1634
  empty_backfill=empty_backfill,
1630
1635
  is_dev=is_dev,
1631
- forward_only=(
1632
- forward_only if forward_only is not None else self.config.plan.forward_only
1633
- ),
1636
+ forward_only=forward_only,
1634
1637
  allow_destructive_models=expanded_destructive_models,
1635
1638
  environment_ttl=environment_ttl,
1636
1639
  environment_suffix_target=self.config.environment_suffix_target,
@@ -2936,7 +2939,7 @@ class GenericContext(BaseContext, t.Generic[C]):
2936
2939
  def _plan_preview_enabled(self) -> bool:
2937
2940
  if self.config.plan.enable_preview is not None:
2938
2941
  return self.config.plan.enable_preview
2939
- # It is dangerous to enable preview by default for dbt projects that rely on engines that don’t support cloning.
2942
+ # It is dangerous to enable preview by default for dbt projects that rely on engines that don't support cloning.
2940
2943
  # Enabling previews in such cases can result in unintended full refreshes because dbt incremental models rely on
2941
2944
  # the maximum timestamp value in the target table.
2942
2945
  return self._project_type == c.NATIVE or self.engine_adapter.SUPPORTS_CLONING
@@ -32,6 +32,7 @@ from sqlmesh.core.engine_adapter.shared import (
32
32
  CommentCreationTable,
33
33
  CommentCreationView,
34
34
  DataObject,
35
+ DataObjectType,
35
36
  EngineRunMode,
36
37
  InsertOverwriteStrategy,
37
38
  SourceQuery,
@@ -39,13 +40,13 @@ from sqlmesh.core.engine_adapter.shared import (
39
40
  )
40
41
  from sqlmesh.core.model.kind import TimeColumn
41
42
  from sqlmesh.core.schema_diff import SchemaDiffer
42
- from sqlmesh.utils import columns_to_types_all_known, random_id, CorrelationId
43
- from sqlmesh.utils.connection_pool import create_connection_pool, ConnectionPool
43
+ from sqlmesh.utils import CorrelationId, columns_to_types_all_known, random_id
44
+ from sqlmesh.utils.connection_pool import ConnectionPool, create_connection_pool
44
45
  from sqlmesh.utils.date import TimeLike, make_inclusive, to_time_column
45
46
  from sqlmesh.utils.errors import (
47
+ MissingDefaultCatalogError,
46
48
  SQLMeshError,
47
49
  UnsupportedCatalogOperationError,
48
- MissingDefaultCatalogError,
49
50
  )
50
51
  from sqlmesh.utils.pandas import columns_to_types_from_df
51
52
 
@@ -54,8 +55,8 @@ if t.TYPE_CHECKING:
54
55
 
55
56
  from sqlmesh.core._typing import SchemaName, SessionProperties, TableName
56
57
  from sqlmesh.core.engine_adapter._typing import (
57
- BigframeSession,
58
58
  DF,
59
+ BigframeSession,
59
60
  PySparkDataFrame,
60
61
  PySparkSession,
61
62
  Query,
@@ -369,6 +370,12 @@ class EngineAdapter:
369
370
  kwargs: Optional create table properties.
370
371
  """
371
372
  target_table = exp.to_table(table_name)
373
+
374
+ target_data_object = self.get_data_object(target_table)
375
+ table_exists = target_data_object is not None
376
+ if self.drop_data_object_on_type_mismatch(target_data_object, DataObjectType.TABLE):
377
+ table_exists = False
378
+
372
379
  source_queries, columns_to_types = self._get_source_queries_and_columns_to_types(
373
380
  query_or_df, columns_to_types, target_table=target_table
374
381
  )
@@ -390,7 +397,7 @@ class EngineAdapter:
390
397
  )
391
398
  # All engines support `CREATE TABLE AS` so we use that if the table doesn't already exist and we
392
399
  # use `CREATE OR REPLACE TABLE AS` if the engine supports it
393
- if self.SUPPORTS_REPLACE_TABLE or not self.table_exists(target_table):
400
+ if self.SUPPORTS_REPLACE_TABLE or not table_exists:
394
401
  return self._create_table_from_source_queries(
395
402
  target_table,
396
403
  source_queries,
@@ -930,6 +937,28 @@ class EngineAdapter:
930
937
  )
931
938
  )
932
939
 
940
+ def drop_data_object(self, data_object: DataObject, ignore_if_not_exists: bool = True) -> None:
941
+ """Drops a data object of arbitrary type.
942
+
943
+ Args:
944
+ data_object: The data object to drop.
945
+ ignore_if_not_exists: If True, no error will be raised if the data object does not exist.
946
+ """
947
+ if data_object.type.is_view:
948
+ self.drop_view(data_object.to_table(), ignore_if_not_exists=ignore_if_not_exists)
949
+ elif data_object.type.is_materialized_view:
950
+ self.drop_view(
951
+ data_object.to_table(), ignore_if_not_exists=ignore_if_not_exists, materialized=True
952
+ )
953
+ elif data_object.type.is_table:
954
+ self.drop_table(data_object.to_table(), exists=ignore_if_not_exists)
955
+ elif data_object.type.is_managed_table:
956
+ self.drop_managed_table(data_object.to_table(), exists=ignore_if_not_exists)
957
+ else:
958
+ raise SQLMeshError(
959
+ f"Can't drop data object '{data_object.to_table().sql(dialect=self.dialect)}' of type '{data_object.type.value}'"
960
+ )
961
+
933
962
  def drop_table(self, table_name: TableName, exists: bool = True) -> None:
934
963
  """Drops a table.
935
964
 
@@ -1118,6 +1147,12 @@ class EngineAdapter:
1118
1147
  if properties.expressions:
1119
1148
  create_kwargs["properties"] = properties
1120
1149
 
1150
+ if replace:
1151
+ self.drop_data_object_on_type_mismatch(
1152
+ self.get_data_object(view_name),
1153
+ DataObjectType.VIEW if not materialized else DataObjectType.MATERIALIZED_VIEW,
1154
+ )
1155
+
1121
1156
  with source_queries[0] as query:
1122
1157
  self.execute(
1123
1158
  exp.Create(
@@ -2022,6 +2057,15 @@ class EngineAdapter:
2022
2057
  )
2023
2058
  self._rename_table(old_table_name, new_table_name)
2024
2059
 
2060
+ def get_data_object(self, target_name: TableName) -> t.Optional[DataObject]:
2061
+ target_table = exp.to_table(target_name)
2062
+ existing_data_objects = self.get_data_objects(
2063
+ schema_(target_table.db, target_table.catalog), {target_table.name}
2064
+ )
2065
+ if existing_data_objects:
2066
+ return existing_data_objects[0]
2067
+ return None
2068
+
2025
2069
  def get_data_objects(
2026
2070
  self, schema_name: SchemaName, object_names: t.Optional[t.Set[str]] = None
2027
2071
  ) -> t.List[DataObject]:
@@ -2483,6 +2527,30 @@ class EngineAdapter:
2483
2527
  table = exp.to_table(table_name)
2484
2528
  self.execute(f"TRUNCATE TABLE {table.sql(dialect=self.dialect, identify=True)}")
2485
2529
 
2530
+ def drop_data_object_on_type_mismatch(
2531
+ self, data_object: t.Optional[DataObject], expected_type: DataObjectType
2532
+ ) -> bool:
2533
+ """Drops a data object if it exists and is not of the expected type.
2534
+
2535
+ Args:
2536
+ data_object: The data object to check.
2537
+ expected_type: The expected type of the data object.
2538
+
2539
+ Returns:
2540
+ True if the data object was dropped, False otherwise.
2541
+ """
2542
+ if data_object is None or data_object.type == expected_type:
2543
+ return False
2544
+
2545
+ logger.warning(
2546
+ "Target data object '%s' is a %s and not a %s, dropping it",
2547
+ data_object.to_table().sql(dialect=self.dialect),
2548
+ data_object.type.value,
2549
+ expected_type.value,
2550
+ )
2551
+ self.drop_data_object(data_object)
2552
+ return True
2553
+
2486
2554
  def _replace_by_key(
2487
2555
  self,
2488
2556
  target_table: TableName,
@@ -262,7 +262,12 @@ class RedshiftEngineAdapter(
262
262
  """
263
263
  import pandas as pd
264
264
 
265
- if not isinstance(query_or_df, pd.DataFrame) or not self.table_exists(table_name):
265
+ target_data_object = self.get_data_object(table_name)
266
+ table_exists = target_data_object is not None
267
+ if self.drop_data_object_on_type_mismatch(target_data_object, DataObjectType.TABLE):
268
+ table_exists = False
269
+
270
+ if not isinstance(query_or_df, pd.DataFrame) or not table_exists:
266
271
  return super().replace_query(
267
272
  table_name,
268
273
  query_or_df,
@@ -171,6 +171,9 @@ class DataObject(PydanticModel):
171
171
  def is_clustered(self) -> bool:
172
172
  return bool(self.clustering_key)
173
173
 
174
+ def to_table(self) -> exp.Table:
175
+ return exp.table_(self.name, db=self.schema_name, catalog=self.catalog, quoted=True)
176
+
174
177
 
175
178
  class CatalogSupport(Enum):
176
179
  # The engine has no concept of catalogs
sqlmesh/core/loader.py CHANGED
@@ -603,6 +603,7 @@ class SqlMeshLoader(Loader):
603
603
  infer_names=self.config.model_naming.infer_names,
604
604
  signal_definitions=signals,
605
605
  default_catalog_per_gateway=self.context.default_catalog_per_gateway,
606
+ virtual_environment_mode=self.config.virtual_environment_mode,
606
607
  **loading_default_kwargs or {},
607
608
  )
608
609
 
@@ -683,6 +684,7 @@ class SqlMeshLoader(Loader):
683
684
  audit_definitions=audits,
684
685
  signal_definitions=signals,
685
686
  default_catalog_per_gateway=self.context.default_catalog_per_gateway,
687
+ virtual_environment_mode=self.config.virtual_environment_mode,
686
688
  ):
687
689
  if model.enabled:
688
690
  models[model.fqn] = model
sqlmesh/core/macros.py CHANGED
@@ -210,7 +210,12 @@ class MacroEvaluator:
210
210
  self.macros[normalize_macro_name(k)] = self.env[k]
211
211
  elif v.is_value:
212
212
  value = self.env[k]
213
- if k in (c.SQLMESH_VARS, c.SQLMESH_BLUEPRINT_VARS):
213
+ if k in (
214
+ c.SQLMESH_VARS,
215
+ c.SQLMESH_VARS_METADATA,
216
+ c.SQLMESH_BLUEPRINT_VARS,
217
+ c.SQLMESH_BLUEPRINT_VARS_METADATA,
218
+ ):
214
219
  value = {
215
220
  var_name: (
216
221
  self.parse_one(var_value.sql)
@@ -557,17 +562,25 @@ class MacroEvaluator:
557
562
 
558
563
  def var(self, var_name: str, default: t.Optional[t.Any] = None) -> t.Optional[t.Any]:
559
564
  """Returns the value of the specified variable, or the default value if it doesn't exist."""
560
- return (self.locals.get(c.SQLMESH_VARS) or {}).get(var_name.lower(), default)
565
+ return {
566
+ **(self.locals.get(c.SQLMESH_VARS) or {}),
567
+ **(self.locals.get(c.SQLMESH_VARS_METADATA) or {}),
568
+ }.get(var_name.lower(), default)
561
569
 
562
570
  def blueprint_var(self, var_name: str, default: t.Optional[t.Any] = None) -> t.Optional[t.Any]:
563
571
  """Returns the value of the specified blueprint variable, or the default value if it doesn't exist."""
564
- return (self.locals.get(c.SQLMESH_BLUEPRINT_VARS) or {}).get(var_name.lower(), default)
572
+ return {
573
+ **(self.locals.get(c.SQLMESH_BLUEPRINT_VARS) or {}),
574
+ **(self.locals.get(c.SQLMESH_BLUEPRINT_VARS_METADATA) or {}),
575
+ }.get(var_name.lower(), default)
565
576
 
566
577
  @property
567
578
  def variables(self) -> t.Dict[str, t.Any]:
568
579
  return {
569
580
  **self.locals.get(c.SQLMESH_VARS, {}),
581
+ **self.locals.get(c.SQLMESH_VARS_METADATA, {}),
570
582
  **self.locals.get(c.SQLMESH_BLUEPRINT_VARS, {}),
583
+ **self.locals.get(c.SQLMESH_BLUEPRINT_VARS_METADATA, {}),
571
584
  }
572
585
 
573
586
  def _coerce(self, expr: exp.Expression, typ: t.Any, strict: bool = False) -> t.Any: