77from sqlglot .expressions import Star
88from sqlglot .helper import subclasses
99
10- from sqlmesh .core .linter .helpers import TokenPositionDetails , get_range_of_model_block
10+ from sqlmesh .core .dialect import normalize_model_name
11+ from sqlmesh .core .linter .helpers import (
12+ TokenPositionDetails ,
13+ get_range_of_model_block ,
14+ read_range_from_string ,
15+ )
1116from sqlmesh .core .linter .rule import Rule , RuleViolation , Range , Fix , TextEdit
1217from sqlmesh .core .linter .definition import RuleSet
1318from sqlmesh .core .model import Model , SqlModel , ExternalModel
19+ from sqlmesh .utils .lineage import extract_references_from_query , LSPExternalModelReference
1420
1521
1622class NoSelectStar (Rule ):
@@ -113,7 +119,9 @@ def check_model(self, model: Model) -> t.Optional[RuleViolation]:
113119class NoMissingExternalModels (Rule ):
114120 """All external models must be registered in the external_models.yaml file"""
115121
116- def check_model (self , model : Model ) -> t .Optional [RuleViolation ]:
122+ def check_model (
123+ self , model : Model
124+ ) -> t .Optional [t .Union [RuleViolation , t .List [RuleViolation ]]]:
117125 # Ignore external models themselves, because either they are registered,
118126 # and if they are not, they will be caught as referenced in another model.
119127 if isinstance (model , ExternalModel ):
@@ -129,10 +137,74 @@ def check_model(self, model: Model) -> t.Optional[RuleViolation]:
129137 if not not_registered_external_models :
130138 return None
131139
140+ # If the model is anything other than a sql model that and has a path
141+ # that ends with .sql, we cannot extract the references from the query.
142+ path = getattr (model , "_path" , None )
143+ if not isinstance (model , SqlModel ) or not path or not str (path ).endswith (".sql" ):
144+ return self ._standard_error_message (
145+ model_name = model .fqn ,
146+ external_models = not_registered_external_models ,
147+ )
148+
149+ with open (path , "r" , encoding = "utf-8" ) as file :
150+ read_file = file .read ()
151+ split_read_file = read_file .splitlines ()
152+
153+ # If there are any unregistered external models, return a violation find
154+ # the ranges for them.
155+ references = extract_references_from_query (
156+ query = model .query ,
157+ context = self .context ,
158+ document_path = path ,
159+ read_file = split_read_file ,
160+ depends_on = model .depends_on ,
161+ dialect = model .dialect ,
162+ )
163+ external_references = {
164+ normalize_model_name (
165+ table = read_range_from_string (read_file , ref .range ),
166+ default_catalog = model .default_catalog ,
167+ dialect = model .dialect ,
168+ ): ref
169+ for ref in references
170+ if isinstance (ref , LSPExternalModelReference ) and ref .path is None
171+ }
172+
173+ # Ensure that depends_on are a subset of the external references. If not, return generic violation.
174+ if not_registered_external_models != set (external_references .keys ()):
175+ return self ._standard_error_message (
176+ model_name = model .fqn ,
177+ external_models = not_registered_external_models ,
178+ )
179+
180+ # Return a violation for each unregistered external model with its range.
181+ violations = []
182+ for ref_name , ref in external_references .items ():
183+ if ref_name in not_registered_external_models :
184+ violations .append (
185+ RuleViolation (
186+ rule = self ,
187+ violation_msg = f"Model '{ model .fqn } ' depends on unregistered external model '{ ref_name } '. "
188+ "Please register it in the external models file. This can be done by running 'sqlmesh create_external_models'." ,
189+ violation_range = ref .range ,
190+ )
191+ )
192+
193+ if len (violations ) < len (not_registered_external_models ):
194+ return self ._standard_error_message (
195+ model_name = model .fqn ,
196+ external_models = not_registered_external_models ,
197+ )
198+
199+ return violations
200+
201+ def _standard_error_message (
202+ self , model_name : str , external_models : t .Set [str ]
203+ ) -> RuleViolation :
132204 return RuleViolation (
133205 rule = self ,
134- violation_msg = f"Model '{ model . name } ' depends on unregistered external models: "
135- f"{ ', ' .join (m for m in not_registered_external_models )} . "
206+ violation_msg = f"Model '{ model_name } ' depends on unregistered external models: "
207+ f"{ ', ' .join (m for m in external_models )} . "
136208 "Please register them in the external models file. This can be done by running 'sqlmesh create_external_models'." ,
137209 )
138210
0 commit comments