Skip to content

Commit 80731de

Browse files
fix(web): resolve model paths against their own project root (#6052)
Signed-off-by: Adegbite Ayoade <tripleaceme@gmail.com> Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com>
1 parent 7e4f83e commit 80731de

2 files changed

Lines changed: 45 additions & 1 deletion

File tree

tests/web/test_models.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
7+
from sqlmesh.core.context import Context
8+
from web.server.api.endpoints.models import get_models
9+
10+
pytestmark = pytest.mark.web
11+
12+
13+
def test_get_models_multi_repo() -> None:
14+
"""Models of every project are serialized, not just those of the first one.
15+
16+
`context.path` is the first configured project, so it is not an ancestor of the models
17+
defined in any of the others.
18+
"""
19+
context = Context(paths=["examples/multi/repo_1", "examples/multi/repo_2"], gateway="memory")
20+
21+
paths_by_name = {model.name: model.path for model in get_models(context)}
22+
23+
# Each model is reported relative to the project that defines it.
24+
assert paths_by_name["bronze.a"] == "models/a.sql"
25+
assert paths_by_name["silver.c"] == "models/c.sql"

web/server/api/endpoints/models.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import typing as t
4+
from pathlib import Path
45

56
from fastapi import APIRouter, Depends, HTTPException
67
from sqlglot import exp
@@ -12,6 +13,7 @@
1213
from sqlmesh.utils.date import now, to_datetime
1314
from web.server import models
1415
from web.server.settings import get_loaded_context
16+
from web.server.utils import is_relative_to
1517

1618
router = APIRouter()
1719

@@ -125,7 +127,7 @@ def serialize_model(context: Context, model: Model, render_query: bool = False)
125127
return models.Model(
126128
name=model.name,
127129
fqn=model.fqn,
128-
path=str(path.absolute().relative_to(context.path).as_posix()) if path else None,
130+
path=_path_relative_to_project(context, path) if path else None,
129131
full_path=str(path.absolute().as_posix()) if path else None,
130132
dialect=dialect,
131133
columns=columns,
@@ -138,6 +140,23 @@ def serialize_model(context: Context, model: Model, render_query: bool = False)
138140
)
139141

140142

143+
def _path_relative_to_project(context: Context, path: Path) -> str:
144+
"""Returns a model's path relative to the project root that defines it.
145+
146+
`context.path` is only the first of the configured projects, so it isn't necessarily an
147+
ancestor of every model when the context is loaded with more than one of them.
148+
"""
149+
absolute_path = path.absolute()
150+
project_roots = [root for root in context.configs if is_relative_to(absolute_path, root)]
151+
if not project_roots:
152+
# The model lives outside of every configured project, so there is nothing to be
153+
# relative to.
154+
return absolute_path.as_posix()
155+
# The deepest root wins so that nested projects report the closest one.
156+
closest_root = max(project_roots, key=lambda root: len(root.parts))
157+
return absolute_path.relative_to(closest_root).as_posix()
158+
159+
141160
def _get_model_type(model: Model) -> str:
142161
if model.is_sql:
143162
return models.ModelType.SQL

0 commit comments

Comments
 (0)