Skip to content

Commit 8890694

Browse files
committed
temp [ci skip]
1 parent 07f9df2 commit 8890694

File tree

7 files changed

+119
-58
lines changed

7 files changed

+119
-58
lines changed

examples/sushi/models/marketing.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ MODEL (
22
name sushi.marketing,
33
kind SCD_TYPE_2(unique_key customer_id),
44
owner jen,
5+
owners test,
56
cron '@daily',
67
grain customer_id,
78
description 'Sushi marketing data'

sqlmesh/core/loader.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ def _load(path: Path) -> t.List[Model]:
364364
self._failed_to_load_model_error(
365365
path, f"Duplicate external model name: '{model.name}'."
366366
),
367-
path
367+
path,
368368
)
369369
models[model.fqn] = model
370370

@@ -404,13 +404,15 @@ def _load_requirements(self) -> t.Tuple[t.Dict[str, str], t.Set[str]]:
404404
args = [k.strip() for k in line.split("==")]
405405
if len(args) != 2:
406406
raise ConfigError(
407-
f"Invalid lock file entry '{line.strip()}'. Only 'dep==ver' is supported", requirements_path
407+
f"Invalid lock file entry '{line.strip()}'. Only 'dep==ver' is supported",
408+
requirements_path,
408409
)
409410
dep, ver = args
410411
other_ver = requirements.get(dep, ver)
411412
if ver != other_ver:
412413
raise ConfigError(
413-
f"Conflicting requirement {dep}: {ver} != {other_ver}. Fix your {c.REQUIREMENTS} file.", requirements_path
414+
f"Conflicting requirement {dep}: {ver} != {other_ver}. Fix your {c.REQUIREMENTS} file.",
415+
requirements_path,
414416
)
415417
requirements[dep] = ver
416418

@@ -622,7 +624,7 @@ def _load_sql_models(
622624
self._failed_to_load_model_error(
623625
path, f"Duplicate SQL model name: '{model.name}'."
624626
),
625-
path
627+
path,
626628
)
627629
elif model.enabled:
628630
model._path = path
@@ -785,7 +787,9 @@ def _load_metrics(self) -> UniqueKeyDict[str, MetricMeta]:
785787
metric = load_metric_ddl(expression, path=path, dialect=dialect)
786788
metrics[metric.name] = metric
787789
except SqlglotError as ex:
788-
raise ConfigError(f"Failed to parse metric definitions at '{path}': {ex}.", path)
790+
raise ConfigError(
791+
f"Failed to parse metric definitions at '{path}': {ex}.", path
792+
)
789793

790794
return metrics
791795

sqlmesh/core/model/common.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@
1313
from sqlmesh.core import dialect as d
1414
from sqlmesh.core.macros import MacroRegistry, MacroStrTemplate
1515
from sqlmesh.utils import str_to_bool
16-
from sqlmesh.utils.errors import ConfigError, SQLMeshError, raise_config_error
16+
from sqlmesh.utils.errors import (
17+
ConfigError,
18+
SQLMeshError,
19+
raise_config_error,
20+
ModelBlockFieldValidationMissingFieldsError,
21+
ModeBlockExtraFields,
22+
)
1723
from sqlmesh.utils.metaprogramming import (
1824
Executable,
1925
SqlValue,
@@ -269,34 +275,28 @@ def validate_extra_and_required_fields(
269275
) -> None:
270276
missing_required_fields = klass.missing_required_fields(provided_fields)
271277
if missing_required_fields:
272-
field_names = "'" + "', '".join(missing_required_fields) + "'"
273-
raise_config_error(
274-
f"Please add required field{'s' if len(missing_required_fields) > 1 else ''} {field_names} to the {entity_name}."
275-
)
278+
if path is None:
279+
raise_config_error(
280+
raise ModelBlockFieldValidationMissingFieldsError(path, missing_required_fields)
276281

277282
extra_fields = klass.extra_fields(provided_fields)
278283
if extra_fields:
279284
extra_field_names = "'" + "', '".join(extra_fields) + "'"
280285

281286
all_fields = klass.all_fields()
282-
close_matches = {}
287+
extra_with_close_match: t.Dict[str, t.Optional[str]] = {}
283288
for field in extra_fields:
284289
matches = get_close_matches(field, all_fields, n=1)
285290
if matches:
286-
close_matches[field] = matches[0]
287-
288-
if len(close_matches) == 1:
289-
similar_msg = ". Did you mean " + "'" + "', '".join(close_matches.values()) + "'?"
290-
else:
291-
similar = [
292-
f"- {field}: Did you mean '{match}'?" for field, match in close_matches.items()
293-
]
294-
similar_msg = "\n\n " + "\n ".join(similar) if similar else ""
295-
296-
raise_config_error(
297-
f"Invalid field name{'s' if len(extra_fields) > 1 else ''} present in the {entity_name}: {extra_field_names}{similar_msg}",
298-
path,
299-
)
291+
extra_with_close_match[field] = matches[0]
292+
else:
293+
extra_with_close_match[field] = None
294+
295+
if extra_with_close_match:
296+
raise ModeBlockExtraFields(
297+
path,
298+
extra_fields=extra_with_close_match,
299+
)
300300

301301

302302
def single_value_or_tuple(values: t.Sequence) -> exp.Identifier | exp.Tuple:

sqlmesh/core/model/kind.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,8 +1015,8 @@ def create_model_kind(v: t.Any, dialect: str, defaults: t.Dict[str, t.Any]) -> M
10151015
"The 'materialization' property is required for models of the CUSTOM kind"
10161016
)
10171017

1018-
# The below call will print a warning if a materialization with the given name doesn't exist
1019-
# we dont want to throw an error here because we still want Models with a CustomKind to be able
1018+
# The below call prints a warning if no materialization with the given name doesn't exist.
1019+
# We don't throw an error here because we still want Models with a CustomKind to be able
10201020
# to be serialized / deserialized in contexts where the custom materialization class may not be available,
10211021
# such as in HTTP request handlers
10221022
custom_materialization = get_custom_materialization_type(

sqlmesh/lsp/errors.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from lsprotocol.types import Diagnostic
2+
from sqlmesh.utils.errors import ConfigError, ModeBlockExtraFields, ModelBlockFieldValidationMissingFieldsError
3+
import typing as t
4+
5+
type ContextFailedError = str | ConfigError | ModeBlockExtraFields | ModelBlockFieldValidationMissingFieldsError
6+
7+
def contextErrorToDiagnostic(error: Exception,) -> t.Tuple[t.Optional[t.Tuple[str, Diagnostic]], ContextFailedError]:
8+
if isinstance(error, ModelBlockFieldValidationMissingFieldsError):
9+
return missingErrorToDiagnostic(error), error
10+
if isinstance(error, ModeBlockExtraFields)
11+
return extraFieldsErrorToDiagnostic(error), error
12+
if isinstance(error, ConfigError):
13+
return configErrorToDiagnostic(error), error
14+
return None, str(error)
15+
16+
17+
def missingErrorToDiagnostic(
18+
error: ModelBlockFieldValidationMissingFieldsError,
19+
) ->t.Optional[t.Tuple[str, Diagnostic]]:
20+
raise NotImplementedError()
21+
22+
def extraFieldsErrorToDiagnostic(
23+
error: ModeBlockExtraFields,
24+
) -> t.Optional[t.Tuple[str, Diagnostic]]:
25+
raise NotImplementedError()
26+
27+
def configErrorToDiagnostic(
28+
error: ConfigError,
29+
) ->t.Optional[t.Tuple[str, Diagnostic]]:
30+
return Diagnostic(
31+
32+
)
33+
raise NotImplementedError()
34+

sqlmesh/lsp/main.py

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
FormatProjectResponse,
4747
CustomMethod,
4848
)
49+
from sqlmesh.lsp.errors import ContextFailedError, contextErrorToDiagnostic
4950
from sqlmesh.lsp.hints import get_hints
5051
from sqlmesh.lsp.reference import (
5152
LSPCteReference,
@@ -56,7 +57,7 @@
5657
)
5758
from sqlmesh.lsp.rename import prepare_rename, rename_symbol, get_document_highlights
5859
from sqlmesh.lsp.uri import URI
59-
from sqlmesh.utils.errors import ConfigError
60+
from sqlmesh.utils.errors import ConfigError, ModelBlockFieldValidationMissingFieldsError, ModeBlockExtraFields
6061
from web.server.api.endpoints.lineage import column_lineage, model_lineage
6162
from web.server.api.endpoints.models import get_models
6263
from typing import Union
@@ -77,9 +78,6 @@ class ContextLoaded:
7778
lsp_context: LSPContext
7879

7980

80-
type ContextFailedError = str | ConfigError
81-
82-
8381
@dataclass
8482
class ContextFailed:
8583
"""State when context failed to load with an error message."""
@@ -324,7 +322,9 @@ def initialize(ls: LanguageServer, params: types.InitializeParams) -> None:
324322
ls.show_message("Client supports pull diagnostics", types.MessageType.Info)
325323
else:
326324
self.client_supports_pull_diagnostics = False
327-
ls.show_message("Client does not support pull diagnostics", types.MessageType.Info)
325+
ls.show_message(
326+
"Client does not support pull diagnostics", types.MessageType.Info
327+
)
328328
else:
329329
self.client_supports_pull_diagnostics = False
330330
ls.show_message("Client capabilities not available", types.MessageType.Info)
@@ -357,7 +357,7 @@ def did_open(ls: LanguageServer, params: types.DidOpenTextDocumentParams) -> Non
357357
uri = URI(params.text_document.uri)
358358
try:
359359
context = self._context_get_or_load(ls, uri)
360-
360+
361361
# Only publish diagnostics if client doesn't support pull diagnostics
362362
if not self.client_supports_pull_diagnostics:
363363
diagnostics = context.lint_model(uri)
@@ -894,30 +894,9 @@ def _create_lsp_context(
894894
return self.context_state.lsp_context
895895
except Exception as e:
896896
# Only show the error message once
897-
if not self.has_raised_loading_error:
898-
location_info = f" at {e.location}" if isinstance(e, ConfigError) and e.location else ""
899-
self.server.show_message(
900-
f"Error creating context error type {type(e)}: {e}{location_info}",
901-
types.MessageType.Error,
902-
)
903-
self.has_raised_loading_error = True
904-
error_message = e if isinstance(e, ConfigError) else str(e)
905-
if isinstance(e, ConfigError) and e.location is not None:
906-
uri = URI.from_path(e.location)
907-
ls.show_message(f"Publishing diagnostic to URI: {uri.value}", types.MessageType.Info)
908-
ls.publish_diagnostics(
909-
uri.value,
910-
[
911-
types.Diagnostic(
912-
range=types.Range(
913-
start=types.Position(line=0, character=0),
914-
end=types.Position(line=0, character=0),
915-
),
916-
message=str(error_message),
917-
severity=types.DiagnosticSeverity.Error,
918-
)
919-
],
920-
)
897+
(uri, diagnostic), error = contextErrorToDiagnostic(e)
898+
if diagnostic:
899+
ls.publish_diagnostics(uri, [diagnostic])
921900

922901
# Store the error in context state such that later requests can
923902
# show the actual error. Try to preserve any partially loaded context
@@ -927,7 +906,7 @@ def _create_lsp_context(
927906
context = self.context_state.lsp_context.context
928907
elif isinstance(self.context_state, ContextFailed) and self.context_state.context:
929908
context = self.context_state.context
930-
self.context_state = ContextFailed(error_message=error_message, context=context)
909+
self.context_state = ContextFailed(error_message=error, context=context)
931910
return None
932911

933912
@staticmethod

sqlmesh/utils/errors.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,49 @@ def __init__(self, message: str | Exception, location: t.Optional[Path] = None)
3232
self.location = Path(location) if isinstance(location, str) else location
3333

3434

35+
class ModelBlockFieldValidationMissingFieldsError(ConfigError):
36+
"""Raised when required fields are missing from a model block."""
37+
38+
missing_fields: t.Set[str]
39+
40+
def __init__(self, path: Path, missing_fields: t.Set[str]) -> None:
41+
super().__init__(
42+
self.message(missing_fields),
43+
path,
44+
)
45+
self.missing_fields = missing_fields
46+
47+
@staticmethod
48+
def message(missing_fields: t.Set[str]) -> str:
49+
field_names = "'" + "', '".join(missing_fields) + "'"
50+
return f"Please add required field{'s' if len(missing_fields) > 1 else ''} {field_names} to the model block."
51+
52+
53+
class ModeBlockExtraFields(ConfigError):
54+
"""Raised when there are extra fields in a model block that are not defined in the model schema. If there are close
55+
matches, this tries to recommend them"""
56+
57+
def __init__(self, path: Path, extra_fields: t.Dict[str, t.Optional[None]]) -> None:
58+
super().__init__(
59+
self.message(extra_fields),
60+
path,
61+
)
62+
self.extra_fields = extra_fields
63+
64+
@staticmethod
65+
def message(extra_fields: t.Dict[str, t.Optional[None]]) -> str:
66+
if len(extra_with_close_match) == 1:
67+
similar_msg = (
68+
". Did you mean " + "'" + "', '".join(extra_with_close_match.values()) + "'?"
69+
)
70+
else:
71+
similar = [
72+
f"- {field}: Did you mean '{match}'?" for field, match in close_matches.items()
73+
]
74+
similar_msg = "\n\n " + "\n ".join(similar) if similar else ""
75+
return f"Invalid field name{'s' if len(extra_fields) > 1 else ''} present in the {entity_name}: {extra_field_names}{similar_msg}"
76+
77+
3578
class MissingDependencyError(SQLMeshError):
3679
"""Local environment is missing a required dependency for the given operation"""
3780

0 commit comments

Comments
 (0)