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