Repository: TobikoData/sqlmesh
Pull request #4183 (2025-04-18 20:05:49)

Update CLI documentation descriptions to match current output format
This PR updates the CLI documentation output descriptions to align with the current SQLMesh CLI output.

Changes include:
- Fixed line number references in output explanations
- Updated descriptions of model execution behaviors
- Clarified the explanation of virtual updates vs. backfills
- Ensured all descriptions match the visual output shown in screenshots

These updates help users better understand what's happening when they run CLI commands.


Patch:
diff --git a/docs/quickstart/cli.md b/docs/quickstart/cli.md
index 403246a5..cf990eb7 100644
--- a/docs/quickstart/cli.md
+++ b/docs/quickstart/cli.md
@@ -148,17 +148,18 @@ $ sqlmesh plan
 ======================================================================
 Successfully Ran 1 tests against duckdb
 ----------------------------------------------------------------------
+
 `prod` environment will be initialized
 
 Models:
 └── Added:
-    ├── sqlmesh_example.seed_model
+    ├── sqlmesh_example.full_model
     ├── sqlmesh_example.incremental_model
-    └── sqlmesh_example.full_model
-Models needing backfill (missing dates):
-├── sqlmesh_example.full_model: 2020-01-01 - 2023-05-31
-├── sqlmesh_example.incremental_model: 2020-01-01 - 2023-05-31
-└── sqlmesh_example.seed_model: 2023-05-31 - 2023-05-31
+    └── sqlmesh_example.seed_model
+Models needing backfill:
+├── sqlmesh_example.full_model: [full refresh]
+├── sqlmesh_example.incremental_model: [2020-01-01 - 2025-04-17]
+└── sqlmesh_example.seed_model: [full refresh]
 Apply - Backfill Tables [y/n]:
 ```
 
@@ -168,10 +169,7 @@ Line 5 describes what environments the plan will affect when applied - a new `pr
 
 Lines 7-11 of the output show that SQLMesh detected three new models relative to the current empty environment.
 
-Lines 12-15 list each model that will be executed by the plan, along with the date intervals that will be run. Note that `full_model` and `incremental_model` both show `2020-01-01` as their start date because:
-
-1. The incremental model specifies that date in the `start` property of its `MODEL` statement and
-2. The full model depends on the incremental model.
+Lines 12-16 list each model that will be executed by the plan, along with the date intervals or refresh types. For both `full_model` and `seed_model`, it shows `[full refresh]`, while for `incremental_model` it shows a specific date range `[2020-01-01 - 2025-04-17]`. The incremental model date range begins from 2020-01-01 because the `full` model kind always fully rebuilds its table.
 
 The `seed_model` date range begins on the same day the plan was made because `SEED` models have no temporality associated with them other than whether they have been modified since the previous SQLMesh plan.
 
@@ -254,20 +252,24 @@ Line 16 asks you whether to proceed with executing the model backfills described
 
 ```bash linenums="1"
 Apply - Backfill Tables [y/n]: y
-Creating physical tables ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00
 
-All model versions have been created successfully
+Updating physical layer ━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00
 
-[1/1] sqlmesh_example.seed_model evaluated in 0.01s
-[1/1] sqlmesh_example.incremental_model evaluated in 0.01s
-[1/1] sqlmesh_example.full_model evaluated in 0.02s
-Evaluating models ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00
+✔ Physical layer updated
 
-All model batches have been executed successfully
+[1/1] sqlmesh_example.seed_model         [insert seed file]
+0.02s
+[1/1] sqlmesh_example.incremental_model  [insert 2020-01-01 -
+2025-04-17]            0.03s
+[1/1] sqlmesh_example.full_model         [full refresh, audits ✔1]     
+0.05s
+Executing model batches ━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00
 
-Virtually Updating 'prod' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 0:00:00
+✔ Model batches executed
 
-The target environment has been updated successfully
+Updating virtual layer  ━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00
+
+✔ Virtual layer updated
 ```
 
 SQLMesh performs three actions when applying the plan:
@@ -276,11 +278,13 @@ SQLMesh performs three actions when applying the plan:
 - Evaluating/running the models
 - Virtually updating the plan's target environment
 
-Line 2 provides a progress bar and elapsed time for the first step of creating new model versions (very fast in this simple project). Line 4 reports that the first step has completed.
+Lines 2-4 show the progress and completion of the first step - updating the physical layer (creating new model versions).
+
+Lines 6-11 show the execution of each model with their specific operations and timing. Line 6 shows the seed model being inserted, line 8 shows the incremental model being inserted for the specified date range, and line 10 shows the full model being processed with its audit check passing.
 
-Lines 6-8 show the run time for each model in the project. Line 9 provides a progress bar and total elapsed time for the second step of evaluating the models. Line 11 reports that the second step has completed.
+Lines 12-14 show the progress and completion of the second step - executing model batches.
 
-Line 13 provides a progress bar and total elapsed time for the third step of virtually updating the plan's target environment. Line 15 reports that the third step has completed and the `prod` environment now points to the tables created during model execution.
+Lines 16-18 show the progress and completion of the final step - virtually updating the plan's target environment, which makes the data available for querying.
 
 You've now created a new production environment with all of history backfilled.
 
@@ -323,10 +327,11 @@ Run `sqlmesh plan dev` to create a development environment called `dev`:
 $ sqlmesh plan dev
 ======================================================================
 Successfully Ran 1 tests against duckdb
-----------------------------------------------------------------------
+---------------------------------------------------------------------- 
 
 New environment `dev` will be created from `prod`
 
+
 Differences from the `prod` environment:
 
 Models:
@@ -334,52 +339,63 @@ Models:
 │   └── sqlmesh_example__dev.incremental_model
 └── Indirectly Modified:
     └── sqlmesh_example__dev.full_model
----
-
-+++
-
-@@ -10,6 +10,7 @@
 
+---
+                                                                       
++++                                                                    
+                                                                       
+@@ -14,6 +14,7 @@
+                                                                       
  SELECT
-   id,
-   item_id,
+   id,                                                                 
+   item_id,                                                            
 +  'z' AS new_column,
-   event_date
+   event_date                                                          
  FROM sqlmesh_example.seed_model
  WHERE
-Directly Modified: sqlmesh_example__dev.incremental_model (Non-breaking)
+
+Directly Modified: sqlmesh_example__dev.incremental_model 
+(Non-breaking)
 └── Indirectly Modified Children:
-    └── sqlmesh_example__dev.full_model (Indirect Non-breaking)
+    └── sqlmesh_example__dev.full_model (Indirect Non-breaking)        
 Models needing backfill:
-└── sqlmesh_example__dev.incremental_model: [2020-01-01 - 2023-05-31]
-Apply - Backfill Tables [y/n]: y
+└── sqlmesh_example__dev.incremental_model: [2020-01-01 - 2025-04-17]  
+Apply - Backfill Tables [y/n]:
 ```
 
 Line 6 of the output states that a new environment `dev` will be created from the existing `prod` environment.
 
-Lines 8-14 summarize the differences between the modified model and the `prod` environment, detecting that we directly modified `incremental_model` and that `full_model` was indirectly modified because it selects from the incremental model. Note that the model schemas are `sqlmesh_example__dev`, indicating that they are being created in the `dev` environment.
+Lines 10-15 summarize the differences between the modified model and the `prod` environment, detecting that we directly modified `incremental_model` and that `full_model` was indirectly modified because it selects from the incremental model. Note that the model schemas are `sqlmesh_example__dev`, indicating that they are being created in the `dev` environment.
 
-On line 28, we see that SQLMesh automatically classified the change as `Non-breaking` because it understood that the change was additive (added a column not used by `full_model`) and did not invalidate any data already in `prod`.
+On line 31, we see that SQLMesh automatically classified the change as `Non-breaking` because it understood that the change was additive (added a column not used by `full_model`) and did not invalidate any data already in `prod`.
 
 Enter `y` at the prompt and press `Enter` to apply the plan and execute the backfill:
 
 ```bash linenums="1"
 Apply - Backfill Tables [y/n]: y
-Creating physical tables ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 2/2 • 0:00:00
 
-Model versions created successfully
+Updating physical layer ━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 2/2 • 0:00:00
+
+✔ Physical layer updated
 
-[1/1] sqlmesh_example__dev.incremental_model evaluated in 0.01s
-Evaluating models ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 1/1 • 0:00:00
+[1/1] sqlmesh_example__dev.incremental_model  [insert 2020-01-01 - 
+2025-04-17] 0.03s
+Executing model batches ━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 1/1 • 0:00:00
 
-Model batches executed successfully
+✔ Model batches executed
 
-Virtually Updating 'dev' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 0:00:00
+Updating virtual layer  ━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 2/2 • 0:00:00
 
-Target environment updated successfully
+✔ Virtual layer updated
 ```
 
-Line 6 of the output shows that SQLMesh applied the change and evaluated `sqlmesh_example__dev.incremental_model`.
+Lines 3-5 show the progress and completion of updating the physical layer.
+
+Line 7 shows that SQLMesh applied the change and evaluated `sqlmesh_example__dev.incremental_model` for the date range from 2020-01-01 to 2025-04-17.
+
+Lines 9-11 show the progress and completion of executing model batches.
+
+Lines 13-15 show the progress and completion of updating the virtual layer.
 
 SQLMesh did not need to backfill anything for the `full_model` since the change was `Non-breaking`.
 
@@ -431,7 +447,8 @@ Enter `y` and press `Enter` at the `Apply - Virtual Update [y/n]:` prompt to app
 $ sqlmesh plan
 ======================================================================
 Successfully Ran 1 tests against duckdb
-----------------------------------------------------------------------
+---------------------------------------------------------------------- 
+
 Differences from the `prod` environment:
 
 Models:
@@ -439,31 +456,36 @@ Models:
 │   └── sqlmesh_example.incremental_model
 └── Indirectly Modified:
     └── sqlmesh_example.full_model
----
-
-+++
-
-@@ -10,6 +10,7 @@
 
+---
+                                                                       
++++                                                                    
+                                                                       
+@@ -14,6 +14,7 @@
+                                                                       
  SELECT
-   id,
-   item_id,
+   id,                                                                 
+   item_id,                                                            
 +  'z' AS new_column,
-   event_date
+   event_date                                                          
  FROM sqlmesh_example.seed_model
  WHERE
-Directly Modified: sqlmesh_example.incremental_model (Non-breaking)
+
+Directly Modified: sqlmesh_example.incremental_model (Non-breaking)    
 └── Indirectly Modified Children:
     └── sqlmesh_example.full_model (Indirect Non-breaking)
 Apply - Virtual Update [y/n]: y
-Virtually Updating 'prod' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 0:00:00
 
-Target environment updated successfully
+SKIP: No physical layer updates to perform
+
+SKIP: No model batches to execute
+
+Updating virtual layer  ━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 2/2 • 0:00:00
 
-Virtual Update executed successfully
+✔ Virtual layer updated
 ```
 
-Note that a backfill was not necessary and only a Virtual Update occurred.
+Note that a backfill was not necessary and only a Virtual Update occurred, as indicated by the "SKIP: No physical layer updates to perform" and "SKIP: No model batches to execute" messages. This is because the changes were already calculated and executed in the `dev` environment, and SQLMesh is smart enough to recognize that it only needs to update the virtual references to the existing tables rather than recomputing everything.
 
 ### 5.2 Validate updates in prod
 Double-check that the data updated in `prod` by running `sqlmesh fetchdf "select * from sqlmesh_example.incremental_model"`:
@@ -490,4 +512,4 @@ From here, you can:
 * [Learn more about SQLMesh CLI commands](../reference/cli.md)
 * [Set up a connection to a database or SQL engine](../guides/connections.md)
 * [Learn more about SQLMesh concepts](../concepts/overview.md)
-* [Join our Slack community](https://tobikodata.com/slack)
+* [Join our Slack community](https://tobikodata.com/slack)
\ No newline at end of file
diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py
index 04857e82..abf7916a 100644
--- a/sqlmesh/core/console.py
+++ b/sqlmesh/core/console.py
@@ -1625,6 +1625,8 @@ class TerminalConsole(Console):
             self._show_categorized_snapshots(plan, default_catalog)
 
         for snapshot in plan.uncategorized:
+            if snapshot.is_model and snapshot.model.forward_only:
+                continue
             if not no_diff:
                 self.show_sql(plan.context_diff.text_diff(snapshot.name))
             tree = Tree(
diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py
index 3ba2725c..dd388811 100644
--- a/sqlmesh/core/plan/builder.py
+++ b/sqlmesh/core/plan/builder.py
@@ -33,7 +33,7 @@ from sqlmesh.utils.date import (
     yesterday_ds,
     to_timestamp,
 )
-from sqlmesh.utils.errors import NoChangesPlanError, PlanError, SQLMeshError
+from sqlmesh.utils.errors import NoChangesPlanError, PlanError
 
 logger = logging.getLogger(__name__)
 
@@ -197,16 +197,16 @@ class PlanBuilder:
         if self._forward_only:
             raise PlanError("Choice setting is not supported by a forward-only plan.")
         if not self._is_new_snapshot(snapshot):
-            raise SQLMeshError(
-                f"A choice can't be changed for the existing version of '{snapshot.name}'."
+            raise PlanError(
+                f"A choice can't be changed for the existing version of {snapshot.name}."
             )
         if (
             not self._context_diff.directly_modified(snapshot.name)
             and snapshot.snapshot_id not in self._context_diff.added
         ):
-            raise SQLMeshError(
-                f"Only directly modified models can be categorized ({snapshot.name})."
-            )
+            raise PlanError(f"Only directly modified models can be categorized ({snapshot.name}).")
+        if snapshot.is_model and snapshot.model.forward_only:
+            raise PlanError(f"Forward-only model {snapshot.name} cannot be categorized manually.")
 
         self._choices[snapshot.snapshot_id] = choice
         self._latest_plan = None
@@ -215,7 +215,7 @@ class PlanBuilder:
     def apply(self) -> None:
         """Builds and applies the plan."""
         if not self._apply:
-            raise SQLMeshError("Plan was not initialized with an applier.")
+            raise PlanError("Plan was not initialized with an applier.")
         self._apply(self.build())
 
     def build(self) -> Plan:


Test patch:
diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py
index 11d94611..89e8d3c7 100644
--- a/tests/core/test_plan.py
+++ b/tests/core/test_plan.py
@@ -3053,3 +3053,55 @@ def test_plan_environment_statements_diff(make_snapshot):
     )
     assert stripped == expected_output
     console_output.close()
+
+
+def test_set_choice_for_forward_only_model(make_snapshot):
+    snapshot = make_snapshot(
+        SqlModel(
+            name="a",
+            query=parse_one("select 1, ds"),
+            dialect="duckdb",
+            kind=IncrementalByTimeRangeKind(time_column="ds", forward_only=True),
+        )
+    )
+    snapshot.categorize_as(SnapshotChangeCategory.BREAKING)
+    updated_snapshot = make_snapshot(
+        SqlModel(
+            name="a",
+            query=parse_one("select 3, ds"),
+            kind=IncrementalByTimeRangeKind(time_column="ds", forward_only=True),
+            dialect="duckdb",
+        )
+    )
+    updated_snapshot.previous_versions = snapshot.all_versions
+
+    context_diff = ContextDiff(
+        environment="test_environment",
+        is_new_environment=True,
+        is_unfinalized_environment=False,
+        normalize_environment_name=True,
+        create_from="prod",
+        create_from_env_exists=True,
+        added=set(),
+        removed_snapshots={},
+        modified_snapshots={updated_snapshot.name: (updated_snapshot, snapshot)},
+        snapshots={updated_snapshot.snapshot_id: updated_snapshot},
+        new_snapshots={updated_snapshot.snapshot_id: updated_snapshot},
+        previous_plan_id=None,
+        previously_promoted_snapshot_ids=set(),
+        previous_finalized_snapshots=None,
+        previous_gateway_managed_virtual_layer=False,
+        gateway_managed_virtual_layer=False,
+    )
+
+    schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER
+    plan_builder = PlanBuilder(context_diff, schema_differ, is_dev=True)
+
+    with pytest.raises(PlanError, match='Forward-only model "a" cannot be categorized manually.'):
+        plan_builder.set_choice(updated_snapshot, SnapshotChangeCategory.BREAKING)
+
+    plan = plan_builder.build()
+    assert (
+        plan.snapshots[updated_snapshot.snapshot_id].change_category
+        == SnapshotChangeCategory.FORWARD_ONLY
+    )