Repository: TobikoData/sqlmesh
Pull request #4516 (2025-05-22 20:41:07)

Fix: only warn for disabled restatements if snapshot in restatement subgraph


Patch:
diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py
index c19cf5d7..8a282636 100644
--- a/sqlmesh/core/model/definition.py
+++ b/sqlmesh/core/model/definition.py
@@ -2488,22 +2488,27 @@ def _create_model(
 
     model.audit_definitions.update(audit_definitions)
 
-    from sqlmesh.core.audit.builtin import BUILT_IN_AUDITS
+    # Any macro referenced in audits or signals needs to be treated as metadata-only
+    statements.extend((audit.query, True) for audit in audit_definitions.values())
 
     # Ensure that all audits referenced in the model are defined
+    from sqlmesh.core.audit.builtin import BUILT_IN_AUDITS
+
     available_audits = BUILT_IN_AUDITS.keys() | model.audit_definitions.keys()
-    for referenced_audit, *_ in model.audits:
+    for referenced_audit, audit_args in model.audits:
         if referenced_audit not in available_audits:
             raise_config_error(f"Audit '{referenced_audit}' is undefined", location=path)
 
-    # Any macro referenced in audits or signals needs to be treated as metadata-only
-    statements.extend((audit.query, True) for audit in audit_definitions.values())
-    for _, audit_args in model.audits:
         statements.extend(
             (audit_arg_expression, True) for audit_arg_expression in audit_args.values()
         )
 
-    for _, kwargs in model.signals:
+    signal_definitions = signal_definitions or UniqueKeyDict("signals")
+
+    for referenced_signal, kwargs in model.signals:
+        if referenced_signal and referenced_signal not in signal_definitions:
+            raise_config_error(f"Signal '{referenced_signal}' is undefined", location=path)
+
         statements.extend((signal_kwarg, True) for signal_kwarg in kwargs.values())
 
     python_env = make_python_env(
@@ -2523,7 +2528,7 @@ def _create_model(
     env: t.Dict[str, t.Tuple[t.Any, t.Optional[bool]]] = {}
 
     for signal_name, _ in model.signals:
-        if signal_definitions and signal_name in signal_definitions:
+        if signal_name and signal_name in signal_definitions:
             func = signal_definitions[signal_name].func
             setattr(func, c.SQLMESH_METADATA, True)
             build_env(func, env=env, name=signal_name, path=module_path)
diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py
index eed05e04..0e8bc481 100644
--- a/sqlmesh/core/plan/builder.py
+++ b/sqlmesh/core/plan/builder.py
@@ -344,6 +344,15 @@ class PlanBuilder:
         for s_id in dag:
             snapshot = self._context_diff.snapshots[s_id]
 
+            # Since we are traversing the graph in topological order and the largest interval range is pushed down
+            # the graph we just have to check our immediate parents in the graph and not the whole upstream graph.
+            restating_parents = [
+                self._context_diff.snapshots[s] for s in snapshot.parents if s in restatements
+            ]
+
+            if not restating_parents and snapshot.name not in restate_models:
+                continue
+
             if not forward_only_preview_needed:
                 if self._is_dev and not snapshot.is_paused:
                     self._console.log_warning(
@@ -362,15 +371,6 @@ class PlanBuilder:
                     logger.info("Skipping restatement for model '%s'", snapshot.name)
                     continue
 
-            # Since we are traversing the graph in topological order and the largest interval range is pushed down
-            # the graph we just have to check our immediate parents in the graph and not the whole upstream graph.
-            restating_parents = [
-                self._context_diff.snapshots[s] for s in snapshot.parents if s in restatements
-            ]
-
-            if not restating_parents and snapshot.name not in restate_models:
-                continue
-
             possible_intervals = {
                 restatements[p.snapshot_id] for p in restating_parents if p.is_incremental
             }


Test patch:
diff --git a/tests/core/test_model.py b/tests/core/test_model.py
index a6607a74..fadbaa8b 100644
--- a/tests/core/test_model.py
+++ b/tests/core/test_model.py
@@ -4919,6 +4919,10 @@ def test_signals():
     model = load_sql_based_model(expressions)
     assert model.signals[0][1] == {"arg": exp.Literal.number(1)}
 
+    @signal()
+    def my_signal(batch):
+        return True
+
     expressions = d.parse(
         """
         MODEL (
@@ -4946,7 +4950,10 @@ def test_signals():
         """
     )
 
-    model = load_sql_based_model(expressions)
+    model = load_sql_based_model(
+        expressions,
+        signal_definitions={"my_signal": signal.get_registry()["my_signal"]},
+    )
     assert model.signals == [
         (
             "my_signal",
@@ -9837,6 +9844,22 @@ def test_invalid_audit_reference():
         load_sql_based_model(expressions)
 
 
+def test_invalid_signal_reference():
+    sql = """
+    MODEL (
+      name test,
+      signals (s())
+    );
+
+    SELECT
+      1 AS id
+    """
+    expressions = d.parse(sql)
+
+    with pytest.raises(ConfigError, match="Signal 's' is undefined"):
+        load_sql_based_model(expressions)
+
+
 def test_scd_time_data_type_does_not_cause_diff_after_deserialization() -> None:
     for dialect in (
         "athena",