1313from sqlglot import exp
1414from sqlglot .optimizer .normalize_identifiers import normalize_identifiers
1515
16- from sqlmesh .core .config import TableNamingConvention
16+ from sqlmesh .core .config . common import TableNamingConvention , VirtualEnvironmentMode
1717from sqlmesh .core import constants as c
1818from sqlmesh .core .audit import StandaloneAudit
1919from sqlmesh .core .environment import EnvironmentSuffixTarget
@@ -229,6 +229,7 @@ class SnapshotDataVersion(PydanticModel, frozen=True):
229229 physical_schema_ : t .Optional [str ] = Field (default = None , alias = "physical_schema" )
230230 dev_table_suffix : str
231231 table_naming_convention : TableNamingConvention = Field (default = TableNamingConvention .default )
232+ virtual_environment_mode : VirtualEnvironmentMode = Field (default = VirtualEnvironmentMode .default )
232233
233234 def snapshot_id (self , name : str ) -> SnapshotId :
234235 return SnapshotId (name = name , identifier = self .fingerprint .to_identifier ())
@@ -335,7 +336,8 @@ class SnapshotInfoMixin(ModelKindMixin):
335336 # This can be removed from this model once Pydantic 1 support is dropped (must remain in `Snapshot` though)
336337 base_table_name_override : t .Optional [str ]
337338 dev_table_suffix : str
338- table_naming_convention : TableNamingConvention = Field (default = TableNamingConvention .default )
339+ table_naming_convention : TableNamingConvention
340+ virtual_environment_mode : VirtualEnvironmentMode
339341
340342 @cached_property
341343 def identifier (self ) -> str :
@@ -394,7 +396,7 @@ def is_indirect_non_breaking(self) -> bool:
394396 return self .change_category == SnapshotChangeCategory .INDIRECT_NON_BREAKING
395397
396398 @property
397- def reuses_previous_version (self ) -> bool :
399+ def is_no_rebuild (self ) -> bool :
398400 return self .change_category in (
399401 SnapshotChangeCategory .FORWARD_ONLY ,
400402 SnapshotChangeCategory .METADATA ,
@@ -432,6 +434,10 @@ def _table_name(self, version: str, is_deployable: bool) -> str:
432434 if self .is_external :
433435 return self .name
434436
437+ if is_deployable and self .virtual_environment_mode .is_dev_only :
438+ # Use the model name as is if the target is deployable and the virtual environment mode is set to dev-only
439+ return self .name
440+
435441 is_dev_table = not is_deployable
436442 if is_dev_table :
437443 version = self .dev_version
@@ -448,6 +454,7 @@ def _table_name(self, version: str, is_deployable: bool) -> str:
448454 fqt = self .fully_qualified_table .copy ()
449455 fqt .set ("catalog" , None )
450456 base_table_name = fqt .sql ()
457+
451458 return table_name (
452459 self .physical_schema ,
453460 base_table_name ,
@@ -487,6 +494,8 @@ class SnapshotTableInfo(PydanticModel, SnapshotInfoMixin, frozen=True):
487494 custom_materialization : t .Optional [str ] = None
488495 dev_table_suffix : str
489496 model_gateway : t .Optional [str ] = None
497+ table_naming_convention : TableNamingConvention = Field (default = TableNamingConvention .default )
498+ virtual_environment_mode : VirtualEnvironmentMode = Field (default = VirtualEnvironmentMode .default )
490499
491500 def __lt__ (self , other : SnapshotTableInfo ) -> bool :
492501 return self .name < other .name
@@ -528,6 +537,7 @@ def data_version(self) -> SnapshotDataVersion:
528537 physical_schema = self .physical_schema ,
529538 dev_table_suffix = self .dev_table_suffix ,
530539 table_naming_convention = self .table_naming_convention ,
540+ virtual_environment_mode = self .virtual_environment_mode ,
531541 )
532542
533543 @property
@@ -614,6 +624,7 @@ class Snapshot(PydanticModel, SnapshotInfoMixin):
614624 table_naming_convention_ : TableNamingConvention = Field (
615625 default = TableNamingConvention .default , alias = "table_naming_convention"
616626 )
627+ virtual_environment_mode : VirtualEnvironmentMode = Field (default = VirtualEnvironmentMode .default )
617628
618629 @field_validator ("ttl" )
619630 @classmethod
@@ -666,6 +677,7 @@ def from_node(
666677 version : t .Optional [str ] = None ,
667678 cache : t .Optional [t .Dict [str , SnapshotFingerprint ]] = None ,
668679 table_naming_convention : TableNamingConvention = TableNamingConvention .default ,
680+ virtual_environment_mode : VirtualEnvironmentMode = VirtualEnvironmentMode .default ,
669681 ) -> Snapshot :
670682 """Creates a new snapshot for a node.
671683
@@ -677,6 +689,7 @@ def from_node(
677689 version: The version that a snapshot is associated with. Usually set during the planning phase.
678690 cache: Cache of node name to fingerprints.
679691 table_naming_convention: Convention to follow when generating the physical table name
692+ virtual_environment_mode: Mode for handling virtual environments
680693
681694 Returns:
682695 The newly created snapshot.
@@ -709,6 +722,7 @@ def from_node(
709722 ttl = ttl ,
710723 version = version ,
711724 table_naming_convention = table_naming_convention ,
725+ virtual_environment_mode = virtual_environment_mode ,
712726 )
713727
714728 def __eq__ (self , other : t .Any ) -> bool :
@@ -863,16 +877,19 @@ def merge_intervals(self, other: t.Union[Snapshot, SnapshotIntervals]) -> None:
863877 Args:
864878 other: The target snapshot to inherit intervals from.
865879 """
866- effective_from_ts = self .normalized_effective_from_ts or 0
867- apply_effective_from = effective_from_ts > 0 and self .identifier != other .identifier
868-
869- for start , end in other .intervals :
870- # If the effective_from is set, then intervals that come after it must come from
871- # the current snapshost.
872- if apply_effective_from and start < effective_from_ts :
873- end = min (end , effective_from_ts )
874- if not apply_effective_from or end <= effective_from_ts :
875- self .add_interval (start , end )
880+ if self .is_no_rebuild or self .virtual_environment_mode .is_full or not self .is_paused :
881+ # If the virtual environment mode is not full we can only merge prod intervals if this snapshot
882+ # is currently promoted in production or if it's forward-only / metadata / indirect non-breaking.
883+ # Otherwise, we want to ignore any existing intervals and backfill this snapshot from scratch.
884+ effective_from_ts = self .normalized_effective_from_ts or 0
885+ apply_effective_from = effective_from_ts > 0 and self .identifier != other .identifier
886+ for start , end in other .intervals :
887+ # If the effective_from is set, then intervals that come after it must come from
888+ # the current snapshost.
889+ if apply_effective_from and start < effective_from_ts :
890+ end = min (end , effective_from_ts )
891+ if not apply_effective_from or end <= effective_from_ts :
892+ self .add_interval (start , end )
876893
877894 if self .dev_version == other .dev_version :
878895 # Merge dev intervals if the dev versions match which would mean
@@ -1013,15 +1030,18 @@ def categorize_as(self, category: SnapshotChangeCategory) -> None:
10131030 category: The change category to assign to this snapshot.
10141031 """
10151032 self .dev_version_ = self .fingerprint .to_version ()
1016- reuse_previous_version = category in (
1033+ is_no_rebuild = category in (
10171034 SnapshotChangeCategory .FORWARD_ONLY ,
10181035 SnapshotChangeCategory .INDIRECT_NON_BREAKING ,
10191036 SnapshotChangeCategory .METADATA ,
10201037 )
1021- if self .is_model and self .model .physical_version :
1038+ if self .is_model and not self .virtual_environment_mode .is_full :
1039+ # Hardcode the version if the virtual environment is not fully enabled.
1040+ self .version = "novde"
1041+ elif self .is_model and self .model .physical_version :
10221042 # If the model has a pinned version then use that.
10231043 self .version = self .model .physical_version
1024- elif reuse_previous_version and self .previous_version :
1044+ elif is_no_rebuild and self .previous_version :
10251045 previous_version = self .previous_version
10261046 self .version = previous_version .data_version .version
10271047 self .physical_schema_ = previous_version .physical_schema
@@ -1219,7 +1239,8 @@ def table_info(self) -> SnapshotTableInfo:
12191239 custom_materialization = custom_materialization ,
12201240 dev_table_suffix = self .dev_table_suffix ,
12211241 model_gateway = self .model_gateway ,
1222- table_naming_convention = self .table_naming_convention , # type: ignore
1242+ table_naming_convention = self .table_naming_convention ,
1243+ virtual_environment_mode = self .virtual_environment_mode ,
12231244 )
12241245
12251246 @property
@@ -1233,6 +1254,7 @@ def data_version(self) -> SnapshotDataVersion:
12331254 physical_schema = self .physical_schema ,
12341255 dev_table_suffix = self .dev_table_suffix ,
12351256 table_naming_convention = self .table_naming_convention ,
1257+ virtual_environment_mode = self .virtual_environment_mode ,
12361258 )
12371259
12381260 @property
@@ -1501,14 +1523,20 @@ def create(
15011523 for node in dag :
15021524 if node not in snapshots :
15031525 continue
1504- # Make sure that the node is deployable according to all its parents
1505- this_deployable = all (
1506- children_deployability_mapping [p_id ]
1507- for p_id in snapshots [node ].parents
1508- if p_id in children_deployability_mapping
1509- )
1526+ snapshot = snapshots [node ]
1527+
1528+ if not snapshot .virtual_environment_mode .is_full :
1529+ # If the virtual environment is not fully enabled, then the snapshot can never be deployable
1530+ this_deployable = False
1531+ else :
1532+ # Make sure that the node is deployable according to all its parents
1533+ this_deployable = all (
1534+ children_deployability_mapping [p_id ]
1535+ for p_id in snapshots [node ].parents
1536+ if p_id in children_deployability_mapping
1537+ )
1538+
15101539 if this_deployable :
1511- snapshot = snapshots [node ]
15121540 is_forward_only_model = (
15131541 snapshot .is_model and snapshot .model .forward_only and not snapshot .is_metadata
15141542 )
0 commit comments