Skip to content

Commit

Permalink
Add tags to SavedQuery (#366)
Browse files Browse the repository at this point in the history
Resolves [#369](#369)

### Description

Addresses internal Linear issue SL-2896.

Users currently can execute parts of their DAG conditionally based on tags added to individual nodes (see [documentation](https://docs.getdbt.com/reference/resource-configs/tags)).  This brings SavedQueries in line with other similar nodes and allows the use of tags as described in that documentation.  (See the added tests for examples.)

It does not add any hierarchical behaviors for these tags.

A related [PR #10987](dbt-labs/dbt-core#10987) is in progress in dbt-core.

### Checklist

- [x] I have read [the contributing guide](https://github.com/dbt-labs/dbt-semantic-interfaces/blob/main/CONTRIBUTING.md) and understand what's expected of me
- [x] I have signed the [CLA](https://docs.getdbt.com/docs/contributor-license-agreements)
- [x] This PR includes tests, or tests are not required/relevant for this PR
- [x] I have run `changie new` to [create a changelog entry](https://github.com/dbt-labs/dbt-semantic-interfaces/blob/main/CONTRIBUTING.md#adding-a-changelog-entry)
  • Loading branch information
theyostalservice authored Nov 14, 2024
2 parents aa3d8b5 + 019d648 commit f3a50f0
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 3 deletions.
6 changes: 6 additions & 0 deletions .changes/unreleased/Features-20241113-110648.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: Features
body: Add "tags" to SavedQuery nodes, similar to existing nodes' tags.
time: 2024-11-13T11:06:48.562566-08:00
custom:
Author: theyostalservice
Issue: "369"
24 changes: 21 additions & 3 deletions dbt_semantic_interfaces/implementations/saved_query.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from __future__ import annotations

from typing import List, Optional
from copy import deepcopy
from typing import Any, List, Optional, Union

from typing_extensions import override
from typing_extensions import Self, override

from dbt_semantic_interfaces.implementations.base import (
HashableBaseModel,
Expand Down Expand Up @@ -35,7 +36,11 @@ def _implements_protocol(self) -> SavedQueryQueryParams:
where: Optional[PydanticWhereFilterIntersection] = None


class PydanticSavedQuery(HashableBaseModel, ModelWithMetadataParsing, ProtocolHint[SavedQuery]):
class PydanticSavedQuery(
HashableBaseModel,
ModelWithMetadataParsing,
ProtocolHint[SavedQuery],
):
"""Pydantic implementation of SavedQuery."""

@override
Expand All @@ -48,3 +53,16 @@ def _implements_protocol(self) -> SavedQuery:
metadata: Optional[PydanticMetadata] = None
label: Optional[str] = None
exports: List[PydanticExport] = Field(default_factory=list)
tags: Union[str, List[str]] = Field(
default_factory=list,
)

@classmethod
def parse_obj(cls, input: Any) -> Self: # noqa
data = deepcopy(input)
if isinstance(data, dict):
if isinstance(data.get("tags"), str):
data["tags"] = [data["tags"]]
if isinstance(data.get("tags"), list):
data["tags"].sort()
return super(HashableBaseModel, cls).parse_obj(data)
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,19 @@
},
"query_params": {
"$ref": "#/definitions/saved_query_query_params_schema"
},
"tags": {
"oneOf": [
{
"type": "string"
},
{
"items": {
"type": "string"
},
"type": "array"
}
]
}
},
"required": [
Expand Down
9 changes: 9 additions & 0 deletions dbt_semantic_interfaces/parsing/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,15 @@
"query_params": {"$ref": "saved_query_query_params_schema"},
"label": {"type": "string"},
"exports": {"type": "array", "items": {"$ref": "export_schema"}},
"tags": {
"oneOf": [
{"type": "string"},
{
"type": "array",
"items": {"type": "string"},
},
],
},
},
"required": ["name", "query_params"],
"additionalProperties": False,
Expand Down
6 changes: 6 additions & 0 deletions dbt_semantic_interfaces/protocols/saved_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@ def label(self) -> Optional[str]:
def exports(self) -> Sequence[Export]:
"""Exports that can run using this saved query."""
pass

@property
@abstractmethod
def tags(self) -> Sequence[str]:
"""List of tags to be used as part of resource selection in dbt."""
pass
112 changes: 112 additions & 0 deletions tests/parsing/test_saved_query_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,118 @@ def test_saved_query_where() -> None:
assert where == saved_query.query_params.where.where_filters[0].where_sql_template


def test_saved_query_with_single_tag_string() -> None:
"""Test for parsing a single string (not a list) tag in a saved query."""
yaml_contents = textwrap.dedent(
"""\
saved_query:
name: test_saved_query_group_bys
tags: "tag_1"
query_params:
metrics:
- test_metric_a
"""
)
file = YamlConfigFile(filepath="test_dir/inline_for_test", contents=yaml_contents)

build_result = parse_yaml_files_to_semantic_manifest(files=[file, EXAMPLE_PROJECT_CONFIGURATION_YAML_CONFIG_FILE])
assert len(build_result.semantic_manifest.saved_queries) == 1
saved_query = build_result.semantic_manifest.saved_queries[0]
assert saved_query.tags is not None
assert len(saved_query.tags) == 1
assert saved_query.tags == ["tag_1"]


def test_saved_query_with_multiline_list_of_tags() -> None:
"""Test for parsing a multiline list of tags in a saved query."""
yaml_contents = textwrap.dedent(
"""\
saved_query:
name: test_saved_query_group_bys
tags: ["tag_1", "tag_2"]
query_params:
metrics:
- test_metric_a
"""
)
file = YamlConfigFile(filepath="test_dir/inline_for_test", contents=yaml_contents)

build_result = parse_yaml_files_to_semantic_manifest(files=[file, EXAMPLE_PROJECT_CONFIGURATION_YAML_CONFIG_FILE])
assert len(build_result.semantic_manifest.saved_queries) == 1
saved_query = build_result.semantic_manifest.saved_queries[0]
assert saved_query.tags is not None
assert len(saved_query.tags) == 2
assert saved_query.tags == ["tag_1", "tag_2"]


def test_saved_query_with_single_line_list_of_tags() -> None:
"""Test for parsing a single-line list of tags in a saved query."""
yaml_contents = textwrap.dedent(
"""\
saved_query:
name: test_saved_query_group_bys
tags:
- "tag_1"
- "tag_2"
query_params:
metrics:
- test_metric_a
"""
)
file = YamlConfigFile(filepath="test_dir/inline_for_test", contents=yaml_contents)

build_result = parse_yaml_files_to_semantic_manifest(files=[file, EXAMPLE_PROJECT_CONFIGURATION_YAML_CONFIG_FILE])
assert len(build_result.semantic_manifest.saved_queries) == 1
saved_query = build_result.semantic_manifest.saved_queries[0]
assert saved_query.tags is not None
assert len(saved_query.tags) == 2
assert saved_query.tags == ["tag_1", "tag_2"]


def test_saved_query_tags_are_sorted() -> None:
"""Test tags in a saved query are SORTED after parsing."""
yaml_contents = textwrap.dedent(
"""\
saved_query:
name: test_saved_query_group_bys
tags:
- "tag_2"
- "tag_1"
query_params:
metrics:
- test_metric_a
"""
)
file = YamlConfigFile(filepath="test_dir/inline_for_test", contents=yaml_contents)

build_result = parse_yaml_files_to_semantic_manifest(files=[file, EXAMPLE_PROJECT_CONFIGURATION_YAML_CONFIG_FILE])
assert len(build_result.semantic_manifest.saved_queries) == 1
saved_query = build_result.semantic_manifest.saved_queries[0]
assert saved_query.tags is not None
assert len(saved_query.tags) == 2
assert saved_query.tags == ["tag_1", "tag_2"]


def test_saved_query_with_no_tags_defaults_to_empty_list() -> None:
"""Test tags in a saved query will default to empty list if missing."""
yaml_contents = textwrap.dedent(
"""\
saved_query:
name: test_saved_query_group_bys
query_params:
metrics:
- test_metric_a
"""
)
file = YamlConfigFile(filepath="test_dir/inline_for_test", contents=yaml_contents)

build_result = parse_yaml_files_to_semantic_manifest(files=[file, EXAMPLE_PROJECT_CONFIGURATION_YAML_CONFIG_FILE])
assert len(build_result.semantic_manifest.saved_queries) == 1
saved_query = build_result.semantic_manifest.saved_queries[0]
assert saved_query.tags is not None
assert saved_query.tags == []


def test_saved_query_exports() -> None:
"""Test for parsing exports referenced in a saved query."""
yaml_contents = textwrap.dedent(
Expand Down

0 comments on commit f3a50f0

Please sign in to comment.