@@ -399,6 +399,114 @@ def test_multiple_gateways(tmp_path: Path):
399399 assert context .dag ._sorted == ['"db"."staging"."stg_model"' , '"db"."main"."final_model"' ]
400400
401401
402+ def test_multi_gateway_catalog_aware_and_unsupported (tmp_path : Path , mocker ):
403+ """ClickHouse (catalog UNSUPPORTED) alongside DuckDB (catalog FULL_SUPPORT) must not raise a
404+ nesting-level SchemaError when models are loaded.
405+
406+ Expected behaviour after the fix:
407+ - get_default_catalog_per_gateway assigns the gateway name as a virtual catalog for
408+ catalog-unsupported gateways when catalog-aware gateways are present.
409+ - ClickHouse models end up with a 3-level FQN so the MappingSchema nesting is uniform.
410+ - The virtual catalog is stripped from DDL expressions (not raised as an error) because the
411+ adapter's catalog_support flips to SINGLE_CATALOG_ONLY when _default_catalog is set.
412+ """
413+
414+ from sqlmesh .core .config .scheduler import BuiltInSchedulerConfig
415+ from sqlmesh .core .engine_adapter .clickhouse import ClickhouseEngineAdapter
416+ from sqlmesh .core .engine_adapter .duckdb import DuckDBEngineAdapter
417+ from sqlmesh .core .engine_adapter .shared import CatalogSupport
418+
419+ db_path = str (tmp_path / "db.db" )
420+
421+ # Build a real DuckDB adapter for the primary gateway.
422+ duck_adapter = DuckDBEngineAdapter (
423+ lambda * a , ** k : __import__ ("duckdb" ).connect (db_path ),
424+ dialect = "duckdb" ,
425+ )
426+
427+ # Build a minimal ClickHouse adapter stub — no real connection needed.
428+ ch_adapter = ClickhouseEngineAdapter (
429+ lambda * a , ** k : mocker .NonCallableMock (),
430+ dialect = "clickhouse" ,
431+ )
432+
433+ # Simulate the context's engine_adapters dict and call the scheduler directly.
434+ engine_adapters = {
435+ "duckdb_gw" : duck_adapter ,
436+ "clickhouse_gw" : ch_adapter ,
437+ }
438+
439+ ctx_mock = mocker .MagicMock ()
440+ ctx_mock .engine_adapters = engine_adapters
441+
442+ scheduler = BuiltInSchedulerConfig ()
443+ catalog_per_gw = scheduler .get_default_catalog_per_gateway (ctx_mock )
444+
445+ # DuckDB gateway must have a real catalog entry.
446+ assert "duckdb_gw" in catalog_per_gw
447+ # DuckDB's default catalog is the database filename without extension.
448+ assert catalog_per_gw ["duckdb_gw" ] == "db"
449+ # ClickHouse gateway must now also have a virtual catalog equal to its gateway name.
450+ assert "clickhouse_gw" in catalog_per_gw
451+ assert catalog_per_gw ["clickhouse_gw" ] == "clickhouse_gw"
452+
453+ # The ClickHouse adapter's _default_catalog must be mutated to the virtual catalog name.
454+ assert ch_adapter ._default_catalog == "clickhouse_gw"
455+
456+ # The adapter's catalog_support must now be SINGLE_CATALOG_ONLY (not UNSUPPORTED),
457+ # so that the set_catalog decorator strips the virtual catalog instead of raising.
458+ assert ch_adapter .catalog_support == CatalogSupport .SINGLE_CATALOG_ONLY
459+
460+ # Loading models for both gateways must not raise a SchemaError.
461+ duckdb_model = load_sql_based_model (
462+ parse ("MODEL(name main.duckdb_tbl, kind FULL, gateway duckdb_gw);\n SELECT 1 AS col" ),
463+ default_catalog = "db" ,
464+ )
465+ ch_model = load_sql_based_model (
466+ parse ("MODEL(name mydb.ch_tbl, kind FULL, gateway clickhouse_gw);\n SELECT 1 AS col" ),
467+ default_catalog = "clickhouse_gw" ,
468+ )
469+
470+ # Both models must have 3-level FQNs so MappingSchema nesting is uniform.
471+ assert duckdb_model .fqn .count ("." ) == 2 , (
472+ f"Expected 3-level FQN for duckdb model, got: { duckdb_model .fqn } "
473+ )
474+ assert ch_model .fqn .count ("." ) == 2 , f"Expected 3-level FQN for ch model, got: { ch_model .fqn } "
475+
476+ # Both models loaded into the same MappingSchema must not raise a nesting SchemaError.
477+ from sqlglot .schema import MappingSchema
478+
479+ schema = MappingSchema (normalize = False )
480+ schema .add_table (duckdb_model .fqn , duckdb_model .columns_to_types or {})
481+ schema .add_table (ch_model .fqn , ch_model .columns_to_types or {})
482+
483+
484+ def test_single_gateway_clickhouse_no_virtual_catalog (mocker ):
485+ """When ClickHouse is the only gateway (no catalog-aware peer), it must NOT receive a virtual
486+ catalog. Models remain 2-level and catalog_support stays UNSUPPORTED."""
487+ from sqlmesh .core .config .scheduler import BuiltInSchedulerConfig
488+ from sqlmesh .core .engine_adapter .clickhouse import ClickhouseEngineAdapter
489+ from sqlmesh .core .engine_adapter .shared import CatalogSupport
490+
491+ ch_adapter = ClickhouseEngineAdapter (
492+ lambda * a , ** k : mocker .NonCallableMock (),
493+ dialect = "clickhouse" ,
494+ )
495+
496+ ctx_mock = mocker .MagicMock ()
497+ ctx_mock .engine_adapters = {"clickhouse_gw" : ch_adapter }
498+
499+ scheduler = BuiltInSchedulerConfig ()
500+ catalog_per_gw = scheduler .get_default_catalog_per_gateway (ctx_mock )
501+
502+ # With only a catalog-unsupported gateway there must be no entry at all.
503+ assert "clickhouse_gw" not in catalog_per_gw
504+
505+ # The adapter must remain unchanged — no virtual catalog injected.
506+ assert ch_adapter ._default_catalog is None
507+ assert ch_adapter .catalog_support == CatalogSupport .UNSUPPORTED
508+
509+
402510def test_plan_execution_time ():
403511 context = Context (config = Config ())
404512 context .upsert_model (
0 commit comments