@@ -40,9 +40,8 @@ def _invoke(sqlmesh_args: t.List[str], **kwargs: t.Any) -> subprocess.CompletedP
4040 return _invoke
4141
4242
43- def test_load_snapshots_that_reference_nonexistent_python_libraries (
44- invoke_cli : InvokeCliType , tmp_path : Path
45- ) -> None :
43+ @pytest .fixture
44+ def duckdb_example_project (tmp_path : Path ) -> Path :
4645 init_example_project (tmp_path , dialect = "duckdb" )
4746 config_path = tmp_path / "config.yaml"
4847
@@ -54,6 +53,36 @@ def test_load_snapshots_that_reference_nonexistent_python_libraries(
5453 }
5554 config_path .write_text (yaml .dump (config_dict ))
5655
56+ return tmp_path
57+
58+
59+ @pytest .fixture
60+ def last_log_file_contents (tmp_path : Path ) -> t .Callable [[], str ]:
61+ def _fetch () -> str :
62+ log_file = sorted (list ((tmp_path / "logs" ).iterdir ()))[- 1 ]
63+ return log_file .read_text ()
64+
65+ return _fetch
66+
67+
68+ def test_load_snapshots_that_reference_nonexistent_python_libraries (
69+ invoke_cli : InvokeCliType ,
70+ duckdb_example_project : Path ,
71+ last_log_file_contents : t .Callable [[], str ],
72+ ) -> None :
73+ """
74+ Scenario:
75+ - A model is created using a macro that is imported from an external package
76+ - That model is applied + snapshot committed to state
77+ - The external package is removed locally and the import macro import is changed to an inline definition
78+
79+ Outcome:
80+ - `sqlmesh plan` should not exit with an ImportError when it tries to render the query of the snapshot stored in state
81+ - Instead, it should log a warning and proceed with applying the new model version
82+ """
83+
84+ project_path = duckdb_example_project
85+
5786 # simulate a 3rd party library that provides a macro
5887 site_packages = site .getsitepackages ()[0 ]
5988 sqlmesh_test_macros_package_path = Path (site_packages ) / "sqlmesh_test_macros"
@@ -67,11 +96,11 @@ def do_something(evaluator):
6796""" )
6897
6998 # reference the macro from site-packages
70- (tmp_path / "macros" / "__init__.py" ).write_text ("""
99+ (project_path / "macros" / "__init__.py" ).write_text ("""
71100from sqlmesh_test_macros.macros import do_something
72101""" )
73102
74- (tmp_path / "models" / "example.sql" ).write_text ("""
103+ (project_path / "models" / "example.sql" ).write_text ("""
75104MODEL (
76105 name example.test_model,
77106 kind FULL
@@ -99,7 +128,7 @@ def do_something(evaluator):
99128 shutil .rmtree (sqlmesh_test_macros_package_path )
100129
101130 # Move the macro inline so its no longer being loaded from a library but still exists with the same signature
102- (tmp_path / "macros" / "__init__.py" ).write_text ("""
131+ (project_path / "macros" / "__init__.py" ).write_text ("""
103132from sqlmesh import macro
104133
105134@macro()
@@ -120,8 +149,7 @@ def do_something(evaluator):
120149 assert "Physical layer updated" in result .stdout
121150 assert "Virtual layer updated" in result .stdout
122151
123- log_file = sorted (list ((tmp_path / "logs" ).iterdir ()))[- 1 ]
124- log_file_contents = log_file .read_text ()
152+ log_file_contents = last_log_file_contents ()
125153 assert "ModuleNotFoundError: No module named 'sqlmesh_test_macros'" in log_file_contents
126154 assert (
127155 "ERROR - Failed to cache optimized query for model 'example.test_model'"
@@ -131,3 +159,195 @@ def do_something(evaluator):
131159 'ERROR - Failed to cache snapshot SnapshotId<"db"."example"."test_model"'
132160 in log_file_contents
133161 )
162+
163+
164+ def test_model_selector_snapshot_references_nonexistent_python_libraries (
165+ invoke_cli : InvokeCliType ,
166+ duckdb_example_project : Path ,
167+ last_log_file_contents : t .Callable [[], str ],
168+ ) -> None :
169+ """
170+ Scenario:
171+ - A model is created using a macro that is imported from an external package
172+ - That model is applied + snapshot committed to state
173+ - The external package is removed locally and the import macro import is changed to an inline definition
174+ - Thus, local version of the model can be rendered but the remote version in state cannot
175+
176+ Outcome:
177+ - `sqlmesh plan --select-model <this model>` should work as it picks up the local version
178+ - `sqlmesh plan --select-model <some other model> should exit with an error, because the plan needs a valid DAG and the remote version is no longer valid locally
179+ """
180+ project_path = duckdb_example_project
181+
182+ # simulate a 3rd party library that provides a macro
183+ site_packages = site .getsitepackages ()[0 ]
184+ sqlmesh_test_macros_package_path = Path (site_packages ) / "sqlmesh_test_macros"
185+ sqlmesh_test_macros_package_path .mkdir ()
186+ (sqlmesh_test_macros_package_path / "macros.py" ).write_text ("""
187+ from sqlmesh import macro
188+
189+ @macro()
190+ def do_something(evaluator):
191+ return "'value from site-packages'"
192+ """ )
193+
194+ # reference the macro from site-packages
195+ (project_path / "macros" / "__init__.py" ).write_text ("""
196+ from sqlmesh_test_macros.macros import do_something
197+ """ )
198+
199+ (project_path / "models" / "example.sql" ).write_text ("""
200+ MODEL (
201+ name sqlmesh_example.test_model,
202+ kind FULL
203+ );
204+
205+ select @do_something() as a
206+ """ )
207+
208+ result = invoke_cli (["plan" , "--no-prompts" , "--auto-apply" , "--skip-tests" ])
209+
210+ assert result .returncode == 0
211+ assert "Physical layer updated" in result .stdout
212+ assert "Virtual layer updated" in result .stdout
213+
214+ # clear cache to ensure we are forced to reload everything
215+ assert invoke_cli (["clean" ]).returncode == 0
216+
217+ # deleting this removes the 'do_something()' macro used by the version of the snapshot stored in state
218+ # when loading the old snapshot from state in the local python env, this will create an ImportError
219+ shutil .rmtree (sqlmesh_test_macros_package_path )
220+
221+ # Move the macro inline so its no longer being loaded from a library but still exists with the same signature
222+ (project_path / "macros" / "__init__.py" ).write_text ("""
223+ from sqlmesh import macro
224+
225+ @macro()
226+ def do_something(evaluator):
227+ return "'some value not from site-packages'"
228+ """ )
229+
230+ # the invalid snapshot is in state but is not preventing a plan
231+ result = invoke_cli (
232+ [
233+ "plan" ,
234+ "--no-prompts" ,
235+ "--skip-tests" ,
236+ ],
237+ input = "n" , # for the apply backfill (y/n) prompt
238+ )
239+ assert result .returncode == 0
240+ assert "Apply - Backfill Tables [y/n]:" in result .stdout
241+ assert "Physical layer updated" not in result .stdout
242+
243+ # the invalid snapshot in state should not prevent a plan if --select-model is used on it (since the local version can be rendered)
244+ result = invoke_cli (
245+ ["plan" , "--select-model" , "sqlmesh_example.test_model" , "--no-prompts" , "--skip-tests" ],
246+ input = "n" , # for the apply backfill (y/n) prompt
247+ )
248+ assert result .returncode == 0
249+ assert "ModuleNotFoundError" not in result .stdout
250+ assert "sqlmesh_example.test_model" in result .stdout
251+ assert "Apply - Backfill Tables" in result .stdout
252+
253+ # the invalid snapshot in state should prevent a plan if --select-model is used on another model
254+ # (since this says to SQLMesh "source everything from state except this selected model" and the plan DAG must be valid to run the plan)
255+ result = invoke_cli (
256+ [
257+ "plan" ,
258+ "--select-model" ,
259+ "sqlmesh_example.full_model" ,
260+ "--no-prompts" ,
261+ "--skip-tests" ,
262+ ],
263+ input = "n" , # for the apply backfill (y/n) prompt
264+ )
265+ assert result .returncode == 1
266+ assert (
267+ "Model 'sqlmesh_example.test_model' sourced from state cannot be rendered in the local environment"
268+ in result .stdout
269+ )
270+ assert "No module named 'sqlmesh_test_macros'" in result .stdout
271+ assert (
272+ "If the model has been fixed locally, please ensure that the --select-model expression includes it"
273+ in result .stdout
274+ )
275+
276+ # verify the full stack trace was logged
277+ log_file_contents = last_log_file_contents ()
278+ assert "ModuleNotFoundError: No module named 'sqlmesh_test_macros'" in log_file_contents
279+ assert (
280+ "The above exception was the direct cause of the following exception:" in log_file_contents
281+ )
282+
283+
284+ def test_model_selector_tags_picks_up_both_remote_and_local (
285+ invoke_cli : InvokeCliType , duckdb_example_project : Path
286+ ) -> None :
287+ """
288+ Scenario:
289+ - A model that has already been applied to prod (so exists in state) has a tag added locally
290+ - A new model is created locally that has the same tag
291+
292+ Outcome:
293+ - `sqlmesh plan --select-model tag:<tag>` should include both models
294+ """
295+ project_path = duckdb_example_project
296+
297+ # default state of full_model
298+ (project_path / "models" / "full_model.sql" ).write_text ("""
299+ MODEL (
300+ name sqlmesh_example.full_model,
301+ kind FULL,
302+ cron '@daily',
303+ grain item_id,
304+ audits (assert_positive_order_ids),
305+ );
306+
307+ SELECT
308+ item_id,
309+ COUNT(DISTINCT id) AS num_orders
310+ FROM sqlmesh_example.incremental_model
311+ GROUP BY item_id
312+ """ )
313+
314+ # apply plan - starting point
315+ result = invoke_cli (["plan" , "--no-prompts" , "--auto-apply" , "--skip-tests" ])
316+
317+ assert result .returncode == 0
318+ assert "Physical layer updated" in result .stdout
319+ assert "Virtual layer updated" in result .stdout
320+
321+ # add a new model locally with tag:a
322+ (project_path / "models" / "new_model.sql" ).write_text ("""
323+ MODEL (
324+ name sqlmesh_example.new_model,
325+ kind full,
326+ tags (a)
327+ );
328+
329+ SELECT 1;
330+ """ )
331+
332+ # update full_model with tag:a
333+ (project_path / "models" / "full_model.sql" ).write_text ("""
334+ MODEL (
335+ name sqlmesh_example.full_model,
336+ kind FULL,
337+ tags (a)
338+ );
339+
340+ SELECT
341+ item_id,
342+ COUNT(DISTINCT id) AS num_orders
343+ FROM sqlmesh_example.incremental_model
344+ GROUP BY item_id
345+ """ )
346+
347+ result = invoke_cli (
348+ ["plan" , "--select-model" , "tag:a" , "--no-prompts" , "--skip-tests" ],
349+ input = "n" , # for the apply backfill (y/n) prompt
350+ )
351+ assert result .returncode == 0
352+ assert "sqlmesh_example.full_model" in result .stdout # metadata update: tags
353+ assert "sqlmesh_example.new_model" in result .stdout # added
0 commit comments