|
11 | 11 | from sqlmesh.core.linter.rule import Rule, RuleViolation, Range, Fix, TextEdit |
12 | 12 | from sqlmesh.core.linter.definition import RuleSet |
13 | 13 | from sqlmesh.core.model import Model, SqlModel, ExternalModel |
| 14 | +from sqlmesh.core.model import Model, SqlModel, ExternalModel |
| 15 | +from sqlmesh.core.linter.rules.helpers.lineage import find_external_model_ranges |
14 | 16 |
|
15 | 17 |
|
16 | 18 | class NoSelectStar(Rule): |
@@ -110,31 +112,82 @@ def check_model(self, model: Model) -> t.Optional[RuleViolation]: |
110 | 112 | return self.violation() |
111 | 113 |
|
112 | 114 |
|
113 | | -class NoMissingExternalModels(Rule): |
| 115 | +class NoUnregisteredExternalModels(Rule): |
114 | 116 | """All external models must be registered in the external_models.yaml file""" |
115 | 117 |
|
116 | | - def check_model(self, model: Model) -> t.Optional[RuleViolation]: |
117 | | - # Ignore external models themselves, because either they are registered, |
118 | | - # and if they are not, they will be caught as referenced in another model. |
| 118 | + def check_model( |
| 119 | + self, model: Model |
| 120 | + ) -> t.Optional[t.Union[RuleViolation, t.List[RuleViolation]]]: |
| 121 | + depends_on = model.depends_on |
| 122 | + |
| 123 | + # Ignore external models themselves, because either they are registered |
| 124 | + # if they are not, they will be caught as referenced in another model. |
119 | 125 | if isinstance(model, ExternalModel): |
120 | 126 | return None |
121 | 127 |
|
122 | | - # Handle other models that may refer to the external models. |
| 128 | + # Handle other models that are referring to them |
123 | 129 | not_registered_external_models: t.Set[str] = set() |
124 | | - for depends_on_model in model.depends_on: |
| 130 | + for depends_on_model in depends_on: |
125 | 131 | existing_model = self.context.get_model(depends_on_model) |
126 | 132 | if existing_model is None: |
127 | 133 | not_registered_external_models.add(depends_on_model) |
128 | 134 |
|
129 | 135 | if not not_registered_external_models: |
130 | 136 | return None |
131 | 137 |
|
| 138 | + path = model._path |
| 139 | + # For SQL models, try to do better than just raise it |
| 140 | + if isinstance(model, SqlModel) and path is not None and str(path).endswith(".sql"): |
| 141 | + external_model_ranges = self.find_external_model_ranges( |
| 142 | + not_registered_external_models, model |
| 143 | + ) |
| 144 | + if external_model_ranges is None: |
| 145 | + return RuleViolation( |
| 146 | + rule=self, |
| 147 | + violation_msg=f"Model '{model.fqn}' depends on unregistered external models: " |
| 148 | + f"{', '.join(m for m in not_registered_external_models)}. " |
| 149 | + "Please register them in the external_models.yaml file.", |
| 150 | + ) |
| 151 | + |
| 152 | + outs: t.List[RuleViolation] = [] |
| 153 | + for external_model in not_registered_external_models: |
| 154 | + external_model_range = external_model_ranges.get(external_model) |
| 155 | + if external_model_range: |
| 156 | + outs.extend( |
| 157 | + RuleViolation( |
| 158 | + rule=self, |
| 159 | + violation_msg=f"Model '{model.fqn}' depends on unregistered external model: " |
| 160 | + f"{external_model}. Please register it in the external_models.yaml file.", |
| 161 | + violation_range=target, |
| 162 | + ) |
| 163 | + for target in external_model_range |
| 164 | + ) |
| 165 | + else: |
| 166 | + outs.append( |
| 167 | + RuleViolation( |
| 168 | + rule=self, |
| 169 | + violation_msg=f"Model '{model.fqn}' depends on unregistered external model: " |
| 170 | + f"{external_model}. Please register it in the external_models.yaml file.", |
| 171 | + ) |
| 172 | + ) |
| 173 | + |
| 174 | + return outs |
| 175 | + |
132 | 176 | return RuleViolation( |
133 | 177 | rule=self, |
134 | 178 | violation_msg=f"Model '{model.name}' depends on unregistered external models: " |
135 | 179 | f"{', '.join(m for m in not_registered_external_models)}. " |
136 | | - "Please register them in the external models file. This can be done by running 'sqlmesh create_external_models'.", |
| 180 | + "Please register them in the external_models.yaml file.", |
137 | 181 | ) |
138 | 182 |
|
| 183 | + def find_external_model_ranges( |
| 184 | + self, external_models_not_registered: t.Set[str], model: SqlModel |
| 185 | + ) -> t.Optional[t.Dict[str, t.List[Range]]]: |
| 186 | + """Returns a map of external model names to their ranges found in the query. |
| 187 | +
|
| 188 | + It returns a dictionary of fqn to a list of ranges where the external model |
| 189 | + """ |
| 190 | + return find_external_model_ranges(self.context, external_models_not_registered, model) |
| 191 | + |
139 | 192 |
|
140 | 193 | BUILTIN_RULES = RuleSet(subclasses(__name__, Rule, (Rule,))) |
0 commit comments