|
11 | 11 | SelectionDifference, |
12 | 12 | SelectionIntersection, |
13 | 13 | SelectionUnion, |
| 14 | + _has_graph_operator, |
| 15 | + _is_unaccompanied_graph_operator, |
14 | 16 | ) |
15 | 17 |
|
16 | 18 |
|
@@ -219,3 +221,81 @@ def test_union(): |
219 | 221 | [{"model_a", "model_b"}, {"model_b", "model_c"}, {"model_d"}] |
220 | 222 | ) |
221 | 223 | assert combined == {"model_a", "model_b", "model_c", "model_d"} |
| 224 | + |
| 225 | + |
| 226 | +# ── Unaccompanied graph operator detection ──────────────────────────────────── |
| 227 | + |
| 228 | + |
| 229 | +@pytest.mark.parametrize( |
| 230 | + "groupdict,expected", |
| 231 | + [ |
| 232 | + # No graph operator at all — not unaccompanied |
| 233 | + ({"childrens_parents": None, "parents": None, "children": None, "value": ""}, False), |
| 234 | + ({"childrens_parents": None, "parents": None, "children": None, "value": "mymodel"}, False), |
| 235 | + # Has operator, empty value — unaccompanied |
| 236 | + ({"childrens_parents": None, "parents": "+", "children": None, "value": ""}, True), |
| 237 | + ({"childrens_parents": "@", "parents": None, "children": None, "value": ""}, True), |
| 238 | + ({"childrens_parents": None, "parents": "1+", "children": None, "value": ""}, True), |
| 239 | + # Has operator, numeric-only value — unaccompanied (looks like a depth modifier) |
| 240 | + ({"childrens_parents": None, "parents": "1+", "children": None, "value": "1"}, True), |
| 241 | + ({"childrens_parents": None, "parents": "+", "children": None, "value": "2"}, True), |
| 242 | + # Has operator, real string value — NOT unaccompanied |
| 243 | + ({"childrens_parents": None, "parents": "+", "children": None, "value": "mymodel"}, False), |
| 244 | + ({"childrens_parents": "@", "parents": None, "children": None, "value": "mymodel"}, False), |
| 245 | + ({"childrens_parents": None, "parents": "2+", "children": "+3", "value": "mymodel"}, False), |
| 246 | + # Children-only with real value — not unaccompanied |
| 247 | + ({"childrens_parents": None, "parents": None, "children": "+", "value": "mymodel"}, False), |
| 248 | + # Children-only with numeric value — unaccompanied |
| 249 | + ({"childrens_parents": None, "parents": None, "children": "+1", "value": "1"}, True), |
| 250 | + # Alphanumeric value with operator — NOT unaccompanied (could be a model named "1abc") |
| 251 | + ({"childrens_parents": None, "parents": "+", "children": None, "value": "1abc"}, False), |
| 252 | + ], |
| 253 | +) |
| 254 | +def test_is_unaccompanied_graph_operator(groupdict, expected): |
| 255 | + assert _is_unaccompanied_graph_operator(groupdict) == expected |
| 256 | + |
| 257 | + |
| 258 | +@pytest.mark.parametrize( |
| 259 | + "raw_spec", |
| 260 | + [ |
| 261 | + "+", |
| 262 | + "@", |
| 263 | + "1+", |
| 264 | + "1+1", |
| 265 | + "+2", |
| 266 | + "2+3", |
| 267 | + ], |
| 268 | +) |
| 269 | +def test_from_single_spec_warns_on_unaccompanied_operator(raw_spec): |
| 270 | + """from_single_spec emits a NoNodesForSelectionCriteria warning for bare operators.""" |
| 271 | + with patch("dbt.graph.selector_spec.warn_or_error") as mock_warn: |
| 272 | + SelectionCriteria.from_single_spec(raw_spec) |
| 273 | + mock_warn.assert_called_once() |
| 274 | + event = mock_warn.call_args[0][0] |
| 275 | + assert type(event).__name__ == "NoNodesForSelectionCriteria" |
| 276 | + |
| 277 | + |
| 278 | +@pytest.mark.parametrize( |
| 279 | + "raw_spec", |
| 280 | + [ |
| 281 | + "mymodel", |
| 282 | + "+mymodel", |
| 283 | + "mymodel+", |
| 284 | + "@mymodel", |
| 285 | + "2+mymodel+3", |
| 286 | + "tag:my_tag", |
| 287 | + "+tag:my_tag", |
| 288 | + "path/to/models", |
| 289 | + "+path/to/models", |
| 290 | + # model names that start with digits are valid if they contain letters too |
| 291 | + "1abc", |
| 292 | + "+1abc", |
| 293 | + ], |
| 294 | +) |
| 295 | +def test_from_single_spec_no_warning_for_valid_selectors(raw_spec): |
| 296 | + """from_single_spec does not warn when a graph operator accompanies a real model name.""" |
| 297 | + with patch("dbt.graph.selector_spec.warn_or_error") as mock_warn: |
| 298 | + with patch("dbt.graph.selector_spec.get_flags") as patched_flags: |
| 299 | + patched_flags.return_value.INDIRECT_SELECTION = IndirectSelection.Eager |
| 300 | + SelectionCriteria.from_single_spec(raw_spec) |
| 301 | + mock_warn.assert_not_called() |
0 commit comments