Skip to content

chore: make _path optional and more representative #4303

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions sqlmesh/core/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -942,10 +942,12 @@ def config_for_path(self, path: Path) -> t.Tuple[Config, Path]:

def config_for_node(self, node: str | Model | Audit) -> Config:
if isinstance(node, str):
return self.config_for_path(self.get_snapshot(node, raise_if_missing=True).node._path)[
0
] # type: ignore
return self.config_for_path(node._path)[0] # type: ignore
path = self.get_snapshot(node, raise_if_missing=True).node._path
else:
path = node._path
if path is None:
return self.config
return self.config_for_path(path)[0] # type: ignore

@property
def models(self) -> MappingProxyType[str, Model]:
Expand Down
6 changes: 3 additions & 3 deletions sqlmesh/core/macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def __init__(
resolve_tables: t.Optional[t.Callable[[exp.Expression], exp.Expression]] = None,
snapshots: t.Optional[t.Dict[str, Snapshot]] = None,
default_catalog: t.Optional[str] = None,
path: Path = Path(),
path: t.Optional[Path] = None,
environment_naming_info: t.Optional[EnvironmentNamingInfo] = None,
model_fqn: t.Optional[str] = None,
):
Expand Down Expand Up @@ -1384,7 +1384,7 @@ def normalize_macro_name(name: str) -> str:
def call_macro(
func: t.Callable,
dialect: DialectType,
path: Path,
path: t.Optional[Path],
provided_args: t.Tuple[t.Any, ...],
provided_kwargs: t.Dict[str, t.Any],
**optional_kwargs: t.Any,
Expand Down Expand Up @@ -1431,7 +1431,7 @@ def _coerce(
expr: t.Any,
typ: t.Any,
dialect: DialectType,
path: Path,
path: t.Optional[Path] = None,
strict: bool = False,
) -> t.Any:
"""Coerces the given expression to the specified type on a best-effort basis."""
Expand Down
25 changes: 17 additions & 8 deletions sqlmesh/core/model/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -1646,6 +1646,8 @@ def is_seed(self) -> bool:
def seed_path(self) -> Path:
seed_path = Path(self.kind.path)
if not seed_path.is_absolute():
if self._path is None:
raise SQLMeshError(f"Seed model '{self.name}' has no path")
return self._path.parent / seed_path
return seed_path

Expand Down Expand Up @@ -2020,7 +2022,7 @@ def load_sql_based_model(
expressions: t.List[exp.Expression],
*,
defaults: t.Optional[t.Dict[str, t.Any]] = None,
path: Path = Path(),
path: t.Optional[Path] = None,
module_path: Path = Path(),
time_column_format: str = c.DEFAULT_TIME_COLUMN_FORMAT,
macros: t.Optional[MacroRegistry] = None,
Expand Down Expand Up @@ -2171,6 +2173,8 @@ def load_sql_based_model(
# The name of the model will be inferred from its path relative to `models/`, if it's not explicitly specified
name = meta_fields.pop("name", "")
if not name and infer_names:
if path is None:
raise ValueError(f"Model {name} must have a name")
name = get_model_name(path)

if not name:
Expand Down Expand Up @@ -2249,7 +2253,7 @@ def create_seed_model(
name: TableName,
seed_kind: SeedKind,
*,
path: Path = Path(),
path: t.Optional[Path] = None,
module_path: Path = Path(),
**kwargs: t.Any,
) -> Model:
Expand All @@ -2268,7 +2272,12 @@ def create_seed_model(
seed_path = module_path.joinpath(*subdirs)
seed_kind.path = str(seed_path)
elif not seed_path.is_absolute():
seed_path = path / seed_path if path.is_dir() else path.parent / seed_path
if path is None:
seed_path = seed_path
elif path.is_dir():
seed_path = path / seed_path
else:
seed_path = path.parent / seed_path

seed = create_seed(seed_path)

Expand Down Expand Up @@ -2403,7 +2412,7 @@ def _create_model(
name: TableName,
*,
defaults: t.Optional[t.Dict[str, t.Any]] = None,
path: Path = Path(),
path: t.Optional[Path] = None,
time_column_format: str = c.DEFAULT_TIME_COLUMN_FORMAT,
jinja_macros: t.Optional[JinjaMacroRegistry] = None,
jinja_macro_references: t.Optional[t.Set[MacroReference]] = None,
Expand Down Expand Up @@ -2588,7 +2597,7 @@ def _create_model(

def _split_sql_model_statements(
expressions: t.List[exp.Expression],
path: Path,
path: t.Optional[Path],
dialect: t.Optional[str] = None,
) -> t.Tuple[
t.Optional[exp.Expression],
Expand Down Expand Up @@ -2709,7 +2718,7 @@ def _refs_to_sql(values: t.Any) -> exp.Expression:
def render_meta_fields(
fields: t.Dict[str, t.Any],
module_path: Path,
path: Path,
path: t.Optional[Path],
jinja_macros: t.Optional[JinjaMacroRegistry],
macros: t.Optional[MacroRegistry],
dialect: DialectType,
Expand Down Expand Up @@ -2793,7 +2802,7 @@ def render_field_value(value: t.Any) -> t.Any:
def render_model_defaults(
defaults: t.Dict[str, t.Any],
module_path: Path,
path: Path,
path: t.Optional[Path],
jinja_macros: t.Optional[JinjaMacroRegistry],
macros: t.Optional[MacroRegistry],
dialect: DialectType,
Expand Down Expand Up @@ -2843,7 +2852,7 @@ def parse_defaults_properties(
def render_expression(
expression: exp.Expression,
module_path: Path,
path: Path,
path: t.Optional[Path],
jinja_macros: t.Optional[JinjaMacroRegistry] = None,
macros: t.Optional[MacroRegistry] = None,
dialect: DialectType = None,
Expand Down
2 changes: 1 addition & 1 deletion sqlmesh/core/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ class _Node(PydanticModel):
interval_unit_: t.Optional[IntervalUnit] = Field(alias="interval_unit", default=None)
tags: t.List[str] = []
stamp: t.Optional[str] = None
_path: Path = Path()
_path: t.Optional[Path] = None
_data_hash: t.Optional[str] = None
_metadata_hash: t.Optional[str] = None

Expand Down
2 changes: 1 addition & 1 deletion sqlmesh/core/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def __init__(
expression: exp.Expression,
dialect: DialectType,
macro_definitions: t.List[d.MacroDef],
path: Path = Path(),
path: t.Optional[Path] = None,
jinja_macro_registry: t.Optional[JinjaMacroRegistry] = None,
python_env: t.Optional[t.Dict[str, Executable]] = None,
only_execution_time: bool = False,
Expand Down
2 changes: 1 addition & 1 deletion sqlmesh/core/snapshot/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -2198,7 +2198,7 @@ def check_ready_intervals(
context: ExecutionContext,
python_env: t.Dict[str, Executable],
dialect: DialectType = None,
path: Path = Path(),
path: t.Optional[Path] = None,
kwargs: t.Optional[t.Dict] = None,
) -> Intervals:
checked_intervals: Intervals = []
Expand Down
6 changes: 6 additions & 0 deletions sqlmesh/dbt/converter/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ def _convert_models(

if model.kind.is_seed:
# this will produce the original seed file, eg "items.csv"
if model._path is None:
raise ValueError(f"Unhandled model path for model {model_name}")
seed_filename = model._path.relative_to(input_paths.seeds)

# seed definition - rename "items.csv" -> "items.sql"
Expand All @@ -219,6 +221,8 @@ def _convert_models(
assert isinstance(model.kind, SeedKind)
model.kind.path = str(Path("../seeds", seed_filename))
else:
if model._path is None:
raise ValueError(f"Unhandled model path for model {model_name}")
if input_paths.models in model._path.parents:
model_filename = model._path.relative_to(input_paths.models)
elif input_paths.snapshots in model._path.parents:
Expand Down Expand Up @@ -290,6 +294,8 @@ def _convert_standalone_audits(

audit_definition_string = ";\n".join(stringified)

if audit._path is None:
continue
Comment on lines +297 to +298
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we warn? Seems like we'd otherwise drop the audit silently.

audit_filename = audit._path.relative_to(input_paths.tests)
audit_output_path = output_paths.audits / audit_filename
audit_output_path.write_text(audit_definition_string)
Expand Down
8 changes: 8 additions & 0 deletions sqlmesh/lsp/reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ def get_model_definitions_for_a_path(
else:
return []

if file_path is None:
return []

# Find all possible references
references: t.List[Reference] = []

Expand Down Expand Up @@ -246,6 +249,8 @@ def get_model_definitions_for_a_path(
if referenced_model is None:
continue
referenced_model_path = referenced_model._path
if referenced_model_path is None:
continue
# Check whether the path exists
if not referenced_model_path.is_file():
continue
Expand Down Expand Up @@ -372,6 +377,9 @@ def get_macro_definitions_for_a_path(
else:
return []

if file_path is None:
return []

references = []
_, config_path = lsp_context.context.config_for_path(
file_path,
Expand Down
10 changes: 6 additions & 4 deletions sqlmesh/magics.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,9 @@ def model(self, context: Context, line: str, sql: t.Optional[str] = None) -> Non
if loaded.name == args.model:
model = loaded
else:
with open(model._path, "r", encoding="utf-8") as file:
expressions = parse(file.read(), default_dialect=config.dialect)
if model._path:
with open(model._path, "r", encoding="utf-8") as file:
expressions = parse(file.read(), default_dialect=config.dialect)

formatted = format_model_expressions(
expressions,
Expand All @@ -307,8 +308,9 @@ def model(self, context: Context, line: str, sql: t.Optional[str] = None) -> Non
replace=True,
)

with open(model._path, "w", encoding="utf-8") as file:
file.write(formatted)
if model._path:
with open(model._path, "w", encoding="utf-8") as file:
file.write(formatted)

if sql:
context.console.log_success(f"Model `{args.model}` updated")
Expand Down
4 changes: 2 additions & 2 deletions tests/core/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -3290,7 +3290,7 @@ def runtime_macro(evaluator, **kwargs) -> None:
model = load_sql_based_model(expressions)
with pytest.raises(
ConfigError,
match=r"Dependencies must be provided explicitly for models that can be rendered only at runtime at.*",
match=r"Dependencies must be provided explicitly for models that can be rendered only at runtime",
):
model.validate_definition()

Expand Down Expand Up @@ -8226,7 +8226,7 @@ def test_physical_version():

with pytest.raises(
ConfigError,
match=r"Pinning a physical version is only supported for forward only models at.*",
match=r"Pinning a physical version is only supported for forward only models( at.*)?",
):
load_sql_based_model(
d.parse(
Expand Down
2 changes: 1 addition & 1 deletion tests/integrations/github/cicd/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def _make_function(username: str, state: str, **kwargs) -> PullRequestReview:
github_client.requester,
{},
{
# Name is whatever they provide in their GitHub profile or login as fallback. Always use login.
# Name is whatever they provide in their GitHub profile or login as a fallback. Always use login.
"user": AttributeDict(name="Unrelated", login=username),
"state": state,
**kwargs,
Expand Down
27 changes: 12 additions & 15 deletions vscode/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1382,8 +1382,14 @@
"properties": {
"name": { "type": "string", "title": "Name" },
"fqn": { "type": "string", "title": "Fqn" },
"path": { "type": "string", "title": "Path" },
"full_path": { "type": "string", "title": "Full Path" },
"path": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Path"
},
"full_path": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Full Path"
},
"dialect": { "type": "string", "title": "Dialect" },
"type": { "$ref": "#/components/schemas/ModelType" },
"columns": {
Expand Down Expand Up @@ -1417,16 +1423,7 @@
},
"additionalProperties": false,
"type": "object",
"required": [
"name",
"fqn",
"path",
"full_path",
"dialect",
"type",
"columns",
"hash"
],
"required": ["name", "fqn", "dialect", "type", "columns", "hash"],
"title": "Model"
},
"ModelDetails": {
Expand Down Expand Up @@ -2143,7 +2140,7 @@
"TestCase": {
"properties": {
"name": { "type": "string", "title": "Name" },
"path": { "type": "string", "format": "path", "title": "Path" }
"path": { "type": "string", "title": "Path" }
},
"additionalProperties": false,
"type": "object",
Expand All @@ -2153,7 +2150,7 @@
"TestErrorOrFailure": {
"properties": {
"name": { "type": "string", "title": "Name" },
"path": { "type": "string", "format": "path", "title": "Path" },
"path": { "type": "string", "title": "Path" },
"tb": { "type": "string", "title": "Tb" }
},
"additionalProperties": false,
Expand Down Expand Up @@ -2193,7 +2190,7 @@
"TestSkipped": {
"properties": {
"name": { "type": "string", "title": "Name" },
"path": { "type": "string", "format": "path", "title": "Path" },
"path": { "type": "string", "title": "Path" },
"reason": { "type": "string", "title": "Reason" }
},
"additionalProperties": false,
Expand Down
8 changes: 6 additions & 2 deletions vscode/react/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,10 @@ export interface Meta {
has_running_task?: boolean
}

export type ModelPath = string | null

export type ModelFullPath = string | null

export type ModelDescription = string | null

export type ModelDetailsProperty = ModelDetails | null
Expand All @@ -302,8 +306,8 @@ export type ModelDefaultCatalog = string | null
export interface Model {
name: string
fqn: string
path: string
full_path: string
path?: ModelPath
full_path?: ModelFullPath
dialect: string
type: ModelType
columns: Column[]
Expand Down
12 changes: 9 additions & 3 deletions vscode/react/src/pages/lineage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,12 @@ function Lineage() {
// @ts-ignore
const fileUri: string = activeFile.fileUri
const filePath = URI.file(fileUri).path
const model = models.find(
(m: Model) => URI.file(m.full_path).path === filePath,
)
const model = models.find((m: Model) => {
if (!m.full_path) {
return false
}
return URI.file(m.full_path).path === filePath
})
if (model) {
return model.name
}
Expand Down Expand Up @@ -195,6 +198,9 @@ export function LineageComponentFromWeb({
if (!model) {
throw new Error('Model not found')
}
if (!model.full_path) {
return
}
vscode('openFile', { uri: URI.file(model.full_path).toString() })
}

Expand Down
5 changes: 3 additions & 2 deletions web/server/api/endpoints/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,12 @@ def serialize_model(context: Context, model: Model, render_query: bool = False)
)
sql = query.sql(pretty=True, dialect=model.dialect)

path = model._path
return models.Model(
name=model.name,
fqn=model.fqn,
path=str(model._path.absolute().relative_to(context.path).as_posix()),
full_path=str(model._path.absolute().as_posix()),
path=str(path.absolute().relative_to(context.path).as_posix()) if path else None,
full_path=str(path.absolute().as_posix()) if path else None,
dialect=dialect,
columns=columns,
details=details,
Expand Down
Loading