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 .linter .helpers import (
11+ TokenPositionDetails ,
12+ get_range_of_model_block ,
13+ read_range_from_string ,
14+ )
1115from sqlmesh .core .linter .rule import Rule , RuleViolation , Range , Fix , TextEdit
1216from sqlmesh .core .linter .definition import RuleSet
1317from sqlmesh .core .model import Model , SqlModel , ExternalModel
18+ from sqlmesh .utils .lineage import extract_references_from_query , LSPExternalModelReference
1419
1520
1621class NoSelectStar (Rule ):
@@ -113,7 +118,9 @@ def check_model(self, model: Model) -> t.Optional[RuleViolation]:
113118class NoMissingExternalModels (Rule ):
114119 """All external models must be registered in the external_models.yaml file"""
115120
116- def check_model (self , model : Model ) -> t .Optional [RuleViolation ]:
121+ def check_model (
122+ self , model : Model
123+ ) -> t .Optional [t .Union [RuleViolation , t .List [RuleViolation ]]]:
117124 # Ignore external models themselves, because either they are registered,
118125 # and if they are not, they will be caught as referenced in another model.
119126 if isinstance (model , ExternalModel ):
@@ -129,10 +136,64 @@ def check_model(self, model: Model) -> t.Optional[RuleViolation]:
129136 if not not_registered_external_models :
130137 return None
131138
139+ # If the model is anything other than a sql model that and has a path
140+ # that ends with .sql, we cannot extract the references from the query.
141+ path = getattr (model , "_path" , None )
142+ if not isinstance (model , SqlModel ) or not path or not str (path ).endswith (".sql" ):
143+ return self ._standard_error_message (
144+ model_name = model .fqn ,
145+ external_models = not_registered_external_models ,
146+ )
147+
148+ with open (path , "r" , encoding = "utf-8" ) as file :
149+ read_file = file .read ()
150+ split_read_file = read_file .splitlines ()
151+
152+ # If there are any unregistered external models, return a violation find
153+ # the ranges for them.
154+ references = extract_references_from_query (
155+ query = model .query ,
156+ context = self .context ,
157+ document_path = path ,
158+ read_file = split_read_file ,
159+ depends_on = model .depends_on ,
160+ dialect = model .dialect ,
161+ )
162+ external_references = {
163+ read_range_from_string (read_file , ref .range ): ref
164+ for ref in references
165+ if isinstance (ref , LSPExternalModelReference ) and ref .path is None
166+ }
167+
168+ # Ensure that depends_on are a subset of the external references. If not, return generic violation.
169+ if not_registered_external_models .issubset (external_references .keys ()):
170+ return self ._standard_error_message (
171+ model_name = model .fqn ,
172+ external_models = not_registered_external_models ,
173+ )
174+
175+ # Return a violation for each unregistered external model with its range.
176+ violations = []
177+ for ref_name , ref in external_references .items ():
178+ if ref_name in not_registered_external_models :
179+ violations .append (
180+ RuleViolation (
181+ rule = self ,
182+ violation_msg = f"Model '{ model .fqn } ' depends on unregistered external model '{ ref_name } '. "
183+ "Please register it in the external models file. This can be done by running 'sqlmesh create_external_models'." ,
184+ violation_range = ref .range ,
185+ )
186+ )
187+
188+ return violations
189+
190+ def _standard_error_message (
191+ self , model_name : str , external_models : t .Set [str ]
192+ ) -> RuleViolation :
132193 return RuleViolation (
133194 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 )} . "
195+ violation_msg = f"Model '{ model_name } ' depends on unregistered external models: "
196+ f"{ ', ' .join (m for m in external_models )} . "
136197 "Please register them in the external models file. This can be done by running 'sqlmesh create_external_models'." ,
137198 )
138199
0 commit comments