@@ -248,6 +248,122 @@ def x(evaluator, y=None) -> None:
248248 )
249249
250250
251+ def test_evaluate_reports_materialization_through_transaction_exit (
252+ duck_conn , mocker : MockerFixture , make_snapshot
253+ ):
254+ from sqlmesh .core .engine_adapter import DuckDBEngineAdapter
255+ from sqlmesh .core .execution_observation import observer_scope
256+
257+ model = SqlModel (
258+ name = "test_schema.test_model" ,
259+ kind = FullKind (),
260+ query = parse_one ("SELECT 1 AS value" ),
261+ )
262+ snapshot = make_snapshot (model )
263+ snapshot .categorize_as (SnapshotChangeCategory .BREAKING )
264+ duck_conn .execute ("CREATE SCHEMA sqlmesh__test_schema" )
265+ evaluator = SnapshotEvaluator (DuckDBEngineAdapter (lambda : duck_conn ))
266+ observations : t .List [t .Tuple [str , t .Dict [str , t .Any ], t .Optional [BaseException ]]] = []
267+ materialize_active = False
268+
269+ def observer (phase , facts , error ):
270+ nonlocal materialize_active
271+ observations .append ((phase , dict (facts ), error ))
272+ if facts ["action" ] == "materialize" :
273+ materialize_active = phase == "start"
274+
275+ target = snapshot .table_name ()
276+ transaction_failure = RuntimeError ("transaction exit failed" )
277+ transaction_depth = 0
278+ transaction = evaluator .adapter .transaction
279+
280+ @contextlib .contextmanager
281+ def fail_outer_transaction (* args : t .Any , ** kwargs : t .Any ) -> t .Iterator [None ]:
282+ nonlocal transaction_depth
283+ outermost = transaction_depth == 0
284+ fail_on_exit = outermost and materialize_active
285+ transaction_depth += 1
286+ try :
287+ with transaction (* args , ** kwargs ):
288+ yield
289+ if fail_on_exit :
290+ raise transaction_failure
291+ finally :
292+ transaction_depth -= 1
293+
294+ mocker .patch .object (evaluator .adapter , "transaction" , new = fail_outer_transaction )
295+ with observer_scope (observer ), pytest .raises (RuntimeError ) as raised :
296+ evaluator .evaluate (
297+ snapshot ,
298+ start = "2020-01-01" ,
299+ end = "2020-01-02" ,
300+ execution_time = "2020-01-02" ,
301+ snapshots = {},
302+ target_table_exists = False ,
303+ )
304+
305+ assert raised .value is transaction_failure
306+ assert evaluator .adapter .table_exists (target )
307+ materialize_events = [event for event in observations if event [1 ]["action" ] == "materialize" ]
308+ assert [phase for phase , _ , _ in materialize_events ] == ["start" , "finish" ]
309+ assert all (facts ["kind" ] == "physical" for _ , facts , _ in materialize_events )
310+ assert all (facts ["target" ] == target for _ , facts , _ in materialize_events )
311+ assert all (facts ["creating" ] is True for _ , facts , _ in materialize_events )
312+ assert materialize_events [0 ][2 ] is None
313+ assert materialize_events [1 ][2 ] is transaction_failure
314+
315+
316+ @pytest .mark .parametrize ("workers" , [1 , 2 ])
317+ def test_create_physical_schemas_preserves_observation_context (duck_conn , make_snapshot , workers ):
318+ from contextvars import ContextVar
319+
320+ from sqlmesh .core .engine_adapter import DuckDBEngineAdapter
321+ from sqlmesh .core .execution_observation import observer_scope
322+
323+ snapshots = []
324+ for index in ("one" , "two" ):
325+ snapshot = make_snapshot (
326+ SqlModel (
327+ name = f"schema_{ index } .model" ,
328+ kind = FullKind (),
329+ query = parse_one ("SELECT 1 AS value" ),
330+ )
331+ )
332+ snapshot .categorize_as (SnapshotChangeCategory .BREAKING )
333+ snapshots .append (snapshot )
334+ evaluator = SnapshotEvaluator (
335+ DuckDBEngineAdapter (lambda : duck_conn ), ddl_concurrent_tasks = workers
336+ )
337+ consumer = ContextVar ("schema_consumer" , default = None )
338+ observed = []
339+
340+ def observer (phase , facts , error ):
341+ observed .append ((phase , dict (facts ), error , consumer .get ()))
342+
343+ token = consumer .set ("parent" )
344+ try :
345+ with observer_scope (observer ):
346+ evaluator .create_physical_schemas (snapshots , DeployabilityIndex .all_deployable ())
347+ finally :
348+ consumer .reset (token )
349+
350+ query_events = [event for event in observed if event [1 ]["kind" ] == "query" ]
351+ assert len (query_events ) == 4
352+ assert sum (phase == "start" for phase , _ , _ , _ in query_events ) == 2
353+ assert sum (phase == "finish" for phase , _ , _ , _ in query_events ) == 2
354+ assert all (facts ["action" ] == "execute" for _ , facts , _ , _ in query_events )
355+ assert all (context == "parent" for _ , _ , _ , context in query_events )
356+ assert all (error is None for phase , _ , error , _ in query_events if phase == "finish" )
357+
358+ created_schemas = {
359+ row [0 ]
360+ for row in duck_conn .execute (
361+ "SELECT schema_name FROM information_schema.schemata"
362+ ).fetchall ()
363+ }
364+ assert {"sqlmesh__schema_one" , "sqlmesh__schema_two" } <= created_schemas
365+
366+
251367def test_runtime_stages (capsys , mocker , adapter_mock , make_snapshot ):
252368 evaluator = SnapshotEvaluator (adapter_mock )
253369
0 commit comments