|
21 | 21 | ) |
22 | 22 | from sqlmesh.core.console import get_console |
23 | 23 | from sqlmesh.core.context import Context |
| 24 | +from sqlmesh.utils import yaml |
24 | 25 | from sqlmesh.utils.date import now |
25 | 26 | from tests.conftest import DuckDBMetadata |
26 | 27 | from tests.utils.test_helpers import use_terminal_console |
@@ -559,3 +560,58 @@ def test_engine_adapters_multi_repo_all_gateways_gathered(copy_to_temp_path): |
559 | 560 | gathered_gateways = context.engine_adapters.keys() |
560 | 561 | expected_gateways = {"local", "memory", "extra"} |
561 | 562 | assert gathered_gateways == expected_gateways |
| 563 | + |
| 564 | + |
| 565 | +@use_terminal_console |
| 566 | +def test_multi_repo_create_external_models(copy_to_temp_path): |
| 567 | + """create_external_models should not classify cross-repo models as external (sqlmesh#5326). |
| 568 | +
|
| 569 | + silver.c and silver.e (repo_2) depend on bronze.a (repo_1). When running |
| 570 | + create_external_models with both repos loaded, bronze.a must NOT be treated |
| 571 | + as an external model because it is an internal model defined in repo_1. |
| 572 | +
|
| 573 | + The observable symptom of the bug is a warning: "Unable to get schema for |
| 574 | + 'bronze.a'" — because SQLMesh tries to query the schema of what it wrongly |
| 575 | + thinks is an external table. A correct implementation never attempts this |
| 576 | + lookup and therefore emits no such warning. |
| 577 | + """ |
| 578 | + paths = copy_to_temp_path("examples/multi") |
| 579 | + repo_1_path = f"{paths[0]}/repo_1" |
| 580 | + repo_2_path = f"{paths[0]}/repo_2" |
| 581 | + |
| 582 | + context = Context(paths=[repo_1_path, repo_2_path], gateway="memory") |
| 583 | + context._new_state_sync().reset(default_catalog=context.default_catalog) |
| 584 | + |
| 585 | + with patch.object(context.console, "log_warning") as mock_warning: |
| 586 | + context.create_external_models() |
| 587 | + |
| 588 | + warning_messages = [str(call) for call in mock_warning.call_args_list] |
| 589 | + schema_lookup_warnings = [ |
| 590 | + msg |
| 591 | + for msg in warning_messages |
| 592 | + if "bronze" in msg and "a" in msg and "schema" in msg.lower() |
| 593 | + ] |
| 594 | + assert not schema_lookup_warnings, ( |
| 595 | + "bronze.a should not be looked up as an external model, but got warnings: " |
| 596 | + + str(schema_lookup_warnings) |
| 597 | + ) |
| 598 | + |
| 599 | + # repo_2's external_models.yaml must not contain bronze.a |
| 600 | + repo_2_external = Path(repo_2_path) / c.EXTERNAL_MODELS_YAML |
| 601 | + if repo_2_external.exists(): |
| 602 | + contents = yaml.load(repo_2_external) |
| 603 | + external_names = [e["name"] for e in contents] |
| 604 | + assert not any("bronze" in name and "a" in name for name in external_names), ( |
| 605 | + f"bronze.a should not be in repo_2's external models, but found: {external_names}" |
| 606 | + ) |
| 607 | + |
| 608 | + # repo_1 has no external dependencies at all |
| 609 | + repo_1_external = Path(repo_1_path) / c.EXTERNAL_MODELS_YAML |
| 610 | + if repo_1_external.exists(): |
| 611 | + contents = yaml.load(repo_1_external) |
| 612 | + assert len(contents) == 0, f"repo_1 should have no external models, got: {contents}" |
| 613 | + |
| 614 | + # Plan should still resolve all 5 models as internal after create_external_models |
| 615 | + context.load() |
| 616 | + plan = context.plan_builder().build() |
| 617 | + assert len(plan.new_snapshots) == 5 |
0 commit comments