Skip to content

Commit b7ce754

Browse files
authored
Merge branch 'main' into fix/dont-ser-engine
2 parents d3b4ed2 + 956670c commit b7ce754

29 files changed

+1676
-33
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ dependencies = [
2424
"requests",
2525
"rich[jupyter]",
2626
"ruamel.yaml",
27-
"sqlglot[rs]~=27.19.0",
27+
"sqlglot[rs]~=27.20.0",
2828
"tenacity",
2929
"time-machine",
3030
"json-stream"

sqlmesh/core/model/kind.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ def is_custom(self) -> bool:
119119
def is_managed(self) -> bool:
120120
return self.model_kind_name == ModelKindName.MANAGED
121121

122+
@property
123+
def is_dbt_custom(self) -> bool:
124+
return self.model_kind_name == ModelKindName.DBT_CUSTOM
125+
122126
@property
123127
def is_symbolic(self) -> bool:
124128
"""A symbolic model is one that doesn't execute at all."""
@@ -170,6 +174,7 @@ class ModelKindName(str, ModelKindMixin, Enum):
170174
EXTERNAL = "EXTERNAL"
171175
CUSTOM = "CUSTOM"
172176
MANAGED = "MANAGED"
177+
DBT_CUSTOM = "DBT_CUSTOM"
173178

174179
@property
175180
def model_kind_name(self) -> t.Optional[ModelKindName]:
@@ -887,6 +892,46 @@ def supports_python_models(self) -> bool:
887892
return False
888893

889894

895+
class DbtCustomKind(_ModelKind):
896+
name: t.Literal[ModelKindName.DBT_CUSTOM] = ModelKindName.DBT_CUSTOM
897+
materialization: str
898+
adapter: str = "default"
899+
definition: str
900+
dialect: t.Optional[str] = Field(None, validate_default=True)
901+
902+
_dialect_validator = kind_dialect_validator
903+
904+
@field_validator("materialization", "adapter", "definition", mode="before")
905+
@classmethod
906+
def _validate_fields(cls, v: t.Any) -> str:
907+
return validate_string(v)
908+
909+
@property
910+
def data_hash_values(self) -> t.List[t.Optional[str]]:
911+
return [
912+
*super().data_hash_values,
913+
self.materialization,
914+
self.definition,
915+
self.adapter,
916+
self.dialect,
917+
]
918+
919+
def to_expression(
920+
self, expressions: t.Optional[t.List[exp.Expression]] = None, **kwargs: t.Any
921+
) -> d.ModelKind:
922+
return super().to_expression(
923+
expressions=[
924+
*(expressions or []),
925+
*_properties(
926+
{
927+
"materialization": exp.Literal.string(self.materialization),
928+
"adapter": exp.Literal.string(self.adapter),
929+
}
930+
),
931+
],
932+
)
933+
934+
890935
class EmbeddedKind(_ModelKind):
891936
name: t.Literal[ModelKindName.EMBEDDED] = ModelKindName.EMBEDDED
892937

@@ -992,6 +1037,7 @@ def to_expression(
9921037
SCDType2ByColumnKind,
9931038
CustomKind,
9941039
ManagedKind,
1040+
DbtCustomKind,
9951041
],
9961042
Field(discriminator="name"),
9971043
]
@@ -1011,6 +1057,7 @@ def to_expression(
10111057
ModelKindName.SCD_TYPE_2_BY_COLUMN: SCDType2ByColumnKind,
10121058
ModelKindName.CUSTOM: CustomKind,
10131059
ModelKindName.MANAGED: ManagedKind,
1060+
ModelKindName.DBT_CUSTOM: DbtCustomKind,
10141061
}
10151062

10161063

sqlmesh/core/selector.py

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from sqlmesh.core.dialect import normalize_model_name
1717
from sqlmesh.core.environment import Environment
1818
from sqlmesh.core.model import update_model_schemas
19+
from sqlmesh.core.audit import StandaloneAudit
1920
from sqlmesh.utils import UniqueKeyDict
2021
from sqlmesh.utils.dag import DAG
2122
from sqlmesh.utils.git import GitClient
@@ -25,6 +26,7 @@
2526
if t.TYPE_CHECKING:
2627
from typing_extensions import Literal as Lit # noqa
2728
from sqlmesh.core.model import Model
29+
from sqlmesh.core.node import Node
2830
from sqlmesh.core.state_sync import StateReader
2931

3032

@@ -167,7 +169,7 @@ def get_model(fqn: str) -> t.Optional[Model]:
167169
return models
168170

169171
def expand_model_selections(
170-
self, model_selections: t.Iterable[str], models: t.Optional[t.Dict[str, Model]] = None
172+
self, model_selections: t.Iterable[str], models: t.Optional[t.Dict[str, Node]] = None
171173
) -> t.Set[str]:
172174
"""Expands a set of model selections into a set of model fqns that can be looked up in the Context.
173175
@@ -180,7 +182,7 @@ def expand_model_selections(
180182

181183
node = parse(" | ".join(f"({s})" for s in model_selections))
182184

183-
all_models = models or self._models
185+
all_models: t.Dict[str, Node] = models or dict(self._models)
184186
models_by_tags: t.Dict[str, t.Set[str]] = {}
185187

186188
for fqn, model in all_models.items():
@@ -226,6 +228,13 @@ def evaluate(node: exp.Expression) -> t.Set[str]:
226228
if fnmatch.fnmatchcase(tag, pattern)
227229
}
228230
return models_by_tags.get(pattern, set())
231+
if isinstance(node, ResourceType):
232+
resource_type = node.name.lower()
233+
return {
234+
fqn
235+
for fqn, model in all_models.items()
236+
if self._matches_resource_type(resource_type, model)
237+
}
229238
if isinstance(node, Direction):
230239
selected = set()
231240

@@ -243,36 +252,49 @@ def evaluate(node: exp.Expression) -> t.Set[str]:
243252
return evaluate(node)
244253

245254
@abc.abstractmethod
246-
def _model_name(self, model: Model) -> str:
255+
def _model_name(self, model: Node) -> str:
247256
"""Given a model, return the name that a selector pattern contining wildcards should be fnmatch'd on"""
248257
pass
249258

250259
@abc.abstractmethod
251-
def _pattern_to_model_fqns(self, pattern: str, all_models: t.Dict[str, Model]) -> t.Set[str]:
260+
def _pattern_to_model_fqns(self, pattern: str, all_models: t.Dict[str, Node]) -> t.Set[str]:
252261
"""Given a pattern, return the keys of the matching models from :all_models"""
253262
pass
254263

264+
@abc.abstractmethod
265+
def _matches_resource_type(self, resource_type: str, model: Node) -> bool:
266+
"""Indicate whether or not the supplied model matches the supplied resource type"""
267+
pass
268+
255269

256270
class NativeSelector(Selector):
257271
"""Implementation of selectors that matches objects based on SQLMesh native names"""
258272

259-
def _model_name(self, model: Model) -> str:
273+
def _model_name(self, model: Node) -> str:
260274
return model.name
261275

262-
def _pattern_to_model_fqns(self, pattern: str, all_models: t.Dict[str, Model]) -> t.Set[str]:
276+
def _pattern_to_model_fqns(self, pattern: str, all_models: t.Dict[str, Node]) -> t.Set[str]:
263277
fqn = normalize_model_name(pattern, self._default_catalog, self._dialect)
264278
return {fqn} if fqn in all_models else set()
265279

280+
def _matches_resource_type(self, resource_type: str, model: Node) -> bool:
281+
if resource_type == "model":
282+
return model.is_model
283+
if resource_type == "audit":
284+
return isinstance(model, StandaloneAudit)
285+
286+
raise SQLMeshError(f"Unsupported resource type: {resource_type}")
287+
266288

267289
class DbtSelector(Selector):
268290
"""Implementation of selectors that matches objects based on the DBT names instead of the SQLMesh native names"""
269291

270-
def _model_name(self, model: Model) -> str:
292+
def _model_name(self, model: Node) -> str:
271293
if dbt_fqn := model.dbt_fqn:
272294
return dbt_fqn
273295
raise SQLMeshError("dbt node information must be populated to use dbt selectors")
274296

275-
def _pattern_to_model_fqns(self, pattern: str, all_models: t.Dict[str, Model]) -> t.Set[str]:
297+
def _pattern_to_model_fqns(self, pattern: str, all_models: t.Dict[str, Node]) -> t.Set[str]:
276298
# a pattern like "staging.customers" should match a model called "jaffle_shop.staging.customers"
277299
# but not a model called "jaffle_shop.customers.staging"
278300
# also a pattern like "aging" should not match "staging" so we need to consider components; not substrings
@@ -306,6 +328,40 @@ def _pattern_to_model_fqns(self, pattern: str, all_models: t.Dict[str, Model]) -
306328
matches.add(fqn)
307329
return matches
308330

331+
def _matches_resource_type(self, resource_type: str, model: Node) -> bool:
332+
"""
333+
ref: https://docs.getdbt.com/reference/node-selection/methods#resource_type
334+
335+
# supported by SQLMesh
336+
"model"
337+
"seed"
338+
"source" # external model
339+
"test" # standalone audit
340+
341+
# not supported by SQLMesh yet, commented out to throw an error if someone tries to use them
342+
"analysis"
343+
"exposure"
344+
"metric"
345+
"saved_query"
346+
"semantic_model"
347+
"snapshot"
348+
"unit_test"
349+
"""
350+
if resource_type not in ("model", "seed", "source", "test"):
351+
raise SQLMeshError(f"Unsupported resource type: {resource_type}")
352+
353+
if isinstance(model, StandaloneAudit):
354+
return resource_type == "test"
355+
356+
if resource_type == "model":
357+
return model.is_model and not model.kind.is_external and not model.kind.is_seed
358+
if resource_type == "source":
359+
return model.kind.is_external
360+
if resource_type == "seed":
361+
return model.kind.is_seed
362+
363+
return False
364+
309365

310366
class SelectorDialect(Dialect):
311367
IDENTIFIERS_CAN_START_WITH_DIGIT = True
@@ -336,6 +392,10 @@ class Tag(exp.Expression):
336392
pass
337393

338394

395+
class ResourceType(exp.Expression):
396+
pass
397+
398+
339399
class Direction(exp.Expression):
340400
pass
341401

@@ -388,7 +448,8 @@ def _parse_var() -> exp.Expression:
388448
upstream = _match(TokenType.PLUS)
389449
downstream = None
390450
tag = _parse_kind("tag")
391-
git = False if tag else _parse_kind("git")
451+
resource_type = False if tag else _parse_kind("resource_type")
452+
git = False if resource_type else _parse_kind("git")
392453
lstar = "*" if _match(TokenType.STAR) else ""
393454
directions = {}
394455

@@ -414,6 +475,8 @@ def _parse_var() -> exp.Expression:
414475

415476
if tag:
416477
this = Tag(this=this)
478+
if resource_type:
479+
this = ResourceType(this=this)
417480
if git:
418481
this = Git(this=this)
419482
if directions:

0 commit comments

Comments
 (0)