diff --git a/.changes/unreleased/Features-20220510-204949.yaml b/.changes/unreleased/Features-20220510-204949.yaml new file mode 100644 index 00000000000..f0f436a9b21 --- /dev/null +++ b/.changes/unreleased/Features-20220510-204949.yaml @@ -0,0 +1,7 @@ +kind: Features +body: Grants as Node Configs +time: 2022-05-10T20:49:49.197999-04:00 +custom: + Author: gshank + Issue: "5189" + PR: "5230" diff --git a/Makefile b/Makefile index 77235123708..0bb4c6b54dd 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,6 @@ endif dev: ## Installs dbt-* packages in develop mode along with development dependencies. @\ pip install -r dev-requirements.txt -r editable-requirements.txt && \ - pre-commit install .PHONY: mypy mypy: .env ## Runs mypy against staged changes for static type checking. diff --git a/core/dbt/context/context_config.py b/core/dbt/context/context_config.py index a0aab160685..a1ee8cdc7be 100644 --- a/core/dbt/context/context_config.py +++ b/core/dbt/context/context_config.py @@ -4,7 +4,7 @@ from typing import List, Iterator, Dict, Any, TypeVar, Generic from dbt.config import RuntimeConfig, Project, IsFQNResource -from dbt.contracts.graph.model_config import BaseConfig, get_config_for +from dbt.contracts.graph.model_config import BaseConfig, get_config_for, _listify from dbt.exceptions import InternalException from dbt.node_types import NodeType from dbt.utils import fqn_search @@ -264,18 +264,49 @@ def add_config_call(self, opts: Dict[str, Any]) -> None: @classmethod def _add_config_call(cls, config_call_dict, opts: Dict[str, Any]) -> None: + # config_call_dict is already encountered configs, opts is new + # This mirrors code in _merge_field_value in model_config.py which is similar but + # operates on config objects. for k, v in opts.items(): # MergeBehavior for post-hook and pre-hook is to collect all # values, instead of overwriting if k in BaseConfig.mergebehavior["append"]: if not isinstance(v, list): v = [v] - if k in BaseConfig.mergebehavior["update"] and not isinstance(v, dict): - raise InternalException(f"expected dict, got {v}") - if k in config_call_dict and isinstance(config_call_dict[k], list): - config_call_dict[k].extend(v) - elif k in config_call_dict and isinstance(config_call_dict[k], dict): - config_call_dict[k].update(v) + if k in config_call_dict: # should always be a list here + config_call_dict[k].extend(v) + else: + config_call_dict[k] = v + + elif k in BaseConfig.mergebehavior["update"]: + if not isinstance(v, dict): + raise InternalException(f"expected dict, got {v}") + if k in config_call_dict and isinstance(config_call_dict[k], dict): + config_call_dict[k].update(v) + else: + config_call_dict[k] = v + elif k in BaseConfig.mergebehavior["dict_key_append"]: + if not isinstance(v, dict): + raise InternalException(f"expected dict, got {v}") + if k in config_call_dict: # should always be a dict + for key, value in v.items(): + extend = False + # This might start with a +, to indicate we should extend the list + # instead of just clobbering it + if key.startswith("+"): + extend = True + if key in config_call_dict[k] and extend: + # extend the list + config_call_dict[k][key].extend(_listify(value)) + else: + # clobber the list + config_call_dict[k][key] = _listify(value) + else: + # This is always a dictionary + config_call_dict[k] = v + # listify everything + for key, value in config_call_dict[k].items(): + config_call_dict[k][key] = _listify(value) else: config_call_dict[k] = v diff --git a/core/dbt/contracts/graph/manifest.py b/core/dbt/contracts/graph/manifest.py index 736460cf5e7..94fa65fab9c 100644 --- a/core/dbt/contracts/graph/manifest.py +++ b/core/dbt/contracts/graph/manifest.py @@ -1135,6 +1135,12 @@ class WritableManifest(ArtifactMixin): ) ) + def __post_serialize__(self, dct): + for unique_id, node in dct["nodes"].items(): + if "config_call_dict" in node: + del node["config_call_dict"] + return dct + def _check_duplicates(value: HasUniqueID, src: Mapping[str, HasUniqueID]): if value.unique_id in src: diff --git a/core/dbt/contracts/graph/model_config.py b/core/dbt/contracts/graph/model_config.py index 3352303a47f..10e84921272 100644 --- a/core/dbt/contracts/graph/model_config.py +++ b/core/dbt/contracts/graph/model_config.py @@ -66,6 +66,7 @@ class MergeBehavior(Metadata): Append = 1 Update = 2 Clobber = 3 + DictKeyAppend = 4 @classmethod def default_field(cls) -> "MergeBehavior": @@ -124,6 +125,9 @@ def _listify(value: Any) -> List: return [value] +# There are two versions of this code. The one here is for config +# objects, the one in _add_config_call in context_config.py is for +# config_call_dict dictionaries. def _merge_field_value( merge_behavior: MergeBehavior, self_value: Any, @@ -141,6 +145,31 @@ def _merge_field_value( value = self_value.copy() value.update(other_value) return value + elif merge_behavior == MergeBehavior.DictKeyAppend: + if not isinstance(self_value, dict): + raise InternalException(f"expected dict, got {self_value}") + if not isinstance(other_value, dict): + raise InternalException(f"expected dict, got {other_value}") + new_dict = {} + for key in self_value.keys(): + new_dict[key] = _listify(self_value[key]) + for key in other_value.keys(): + extend = False + new_key = key + # This might start with a +, to indicate we should extend the list + # instead of just clobbering it + if new_key.startswith("+"): + new_key = key.lstrip("+") + extend = True + if new_key in new_dict and extend: + # extend the list + value = other_value[key] + new_dict[new_key].extend(_listify(value)) + else: + # clobber the list + new_dict[new_key] = _listify(other_value[key]) + return new_dict + else: raise InternalException(f"Got an invalid merge_behavior: {merge_behavior}") @@ -257,6 +286,7 @@ def same_contents(cls, unrendered: Dict[str, Any], other: Dict[str, Any]) -> boo mergebehavior = { "append": ["pre-hook", "pre_hook", "post-hook", "post_hook", "tags"], "update": ["quoting", "column_types", "meta"], + "dict_key_append": ["grants"], } @classmethod @@ -427,6 +457,9 @@ class NodeConfig(NodeAndTestConfig): # sometimes getting the Union order wrong, causing serialization failures. unique_key: Union[str, List[str], None] = None on_schema_change: Optional[str] = "ignore" + grants: Dict[str, Any] = field( + default_factory=dict, metadata=MergeBehavior.DictKeyAppend.meta() + ) @classmethod def __pre_deserialize__(cls, data): diff --git a/core/dbt/contracts/graph/parsed.py b/core/dbt/contracts/graph/parsed.py index eb7ebcf5438..4d9ad584699 100644 --- a/core/dbt/contracts/graph/parsed.py +++ b/core/dbt/contracts/graph/parsed.py @@ -233,8 +233,6 @@ def _serialize(self): return self.to_dict() def __post_serialize__(self, dct): - if "config_call_dict" in dct: - del dct["config_call_dict"] if "_event_status" in dct: del dct["_event_status"] return dct diff --git a/core/dbt/parser/base.py b/core/dbt/parser/base.py index 0b54e89addf..5e04a3b2a94 100644 --- a/core/dbt/parser/base.py +++ b/core/dbt/parser/base.py @@ -271,7 +271,7 @@ def update_parsed_node_config( # build_config_dict takes the config_call_dict in the ContextConfig object # and calls calculate_node_config to combine dbt_project configs and - # config calls from SQL files + # config calls from SQL files, plus patch configs (from schema files) config_dict = config.build_config_dict(patch_config_dict=patch_config_dict) # Set tags on node provided in config blocks. Tags are additive, so even if diff --git a/core/dbt/tests/fixtures/project.py b/core/dbt/tests/fixtures/project.py index 67f0389bbf7..603d7786cca 100644 --- a/core/dbt/tests/fixtures/project.py +++ b/core/dbt/tests/fixtures/project.py @@ -185,7 +185,11 @@ def dbt_project_yml(project_root, project_config_update, logs_dir): "log-path": logs_dir, } if project_config_update: - project_config.update(project_config_update) + if isinstance(project_config_update, dict): + project_config.update(project_config_update) + elif isinstance(project_config_update, str): + updates = yaml.safe_load(project_config_update) + project_config.update(updates) write_file(yaml.safe_dump(project_config), project_root, "dbt_project.yml") return project_config diff --git a/test/unit/test_contracts_graph_compiled.py b/test/unit/test_contracts_graph_compiled.py index cc970997901..577cacc5777 100644 --- a/test/unit/test_contracts_graph_compiled.py +++ b/test/unit/test_contracts_graph_compiled.py @@ -138,6 +138,7 @@ def basic_uncompiled_dict(): 'tags': [], 'on_schema_change': 'ignore', 'meta': {}, + 'grants': {}, }, 'docs': {'show': True}, 'columns': {}, @@ -146,7 +147,8 @@ def basic_uncompiled_dict(): 'extra_ctes': [], 'extra_ctes_injected': False, 'checksum': {'name': 'sha256', 'checksum': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'}, - 'unrendered_config': {} + 'unrendered_config': {}, + 'config_call_dict': {}, } @@ -183,6 +185,7 @@ def basic_compiled_dict(): 'tags': [], 'on_schema_change': 'ignore', 'meta': {}, + 'grants': {}, }, 'docs': {'show': True}, 'columns': {}, @@ -192,7 +195,8 @@ def basic_compiled_dict(): 'extra_ctes_injected': True, 'compiled_sql': 'with whatever as (select * from other) select * from whatever', 'checksum': {'name': 'sha256', 'checksum': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'}, - 'unrendered_config': {} + 'unrendered_config': {}, + 'config_call_dict': {}, } @@ -445,7 +449,8 @@ def basic_uncompiled_schema_test_dict(): 'kwargs': {}, }, 'checksum': {'name': 'sha256', 'checksum': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'}, - 'unrendered_config': {} + 'unrendered_config': {}, + 'config_call_dict': {}, } @@ -497,7 +502,8 @@ def basic_compiled_schema_test_dict(): 'checksum': {'name': 'sha256', 'checksum': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'}, 'unrendered_config': { 'severity': 'warn', - } + }, + 'config_call_dict': {}, } diff --git a/test/unit/test_contracts_graph_parsed.py b/test/unit/test_contracts_graph_parsed.py index c618dfe47a4..2024d5bbe40 100644 --- a/test/unit/test_contracts_graph_parsed.py +++ b/test/unit/test_contracts_graph_parsed.py @@ -72,6 +72,7 @@ def populated_node_config_dict(): 'extra': 'even more', 'on_schema_change': 'ignore', 'meta': {}, + 'grants': {}, } @@ -151,6 +152,7 @@ def base_parsed_model_dict(): 'tags': [], 'on_schema_change': 'ignore', 'meta': {}, + 'grants': {}, }, 'deferred': False, 'docs': {'show': True}, @@ -158,6 +160,7 @@ def base_parsed_model_dict(): 'meta': {}, 'checksum': {'name': 'sha256', 'checksum': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'}, 'unrendered_config': {}, + 'config_call_dict': {}, } @@ -243,6 +246,7 @@ def complex_parsed_model_dict(): 'tags': [], 'on_schema_change': 'ignore', 'meta': {}, + 'grants': {}, }, 'docs': {'show': True}, 'columns': { @@ -259,6 +263,7 @@ def complex_parsed_model_dict(): 'materialized': 'ephemeral', 'post_hook': ['insert into blah(a, b) select "1", 1'], }, + 'config_call_dict': {}, } @@ -435,6 +440,7 @@ def basic_parsed_seed_dict(): 'tags': [], 'on_schema_change': 'ignore', 'meta': {}, + 'grants': {}, }, 'deferred': False, 'docs': {'show': True}, @@ -442,6 +448,7 @@ def basic_parsed_seed_dict(): 'meta': {}, 'checksum': {'name': 'path', 'checksum': 'seeds/seed.csv'}, 'unrendered_config': {}, + 'config_call_dict': {}, } @@ -529,6 +536,7 @@ def complex_parsed_seed_dict(): 'quote_columns': True, 'on_schema_change': 'ignore', 'meta': {}, + 'grants': {}, }, 'deferred': False, 'docs': {'show': True}, @@ -538,6 +546,7 @@ def complex_parsed_seed_dict(): 'unrendered_config': { 'persist_docs': {'relation': True, 'columns': True}, }, + 'config_call_dict': {}, } @@ -779,12 +788,14 @@ def base_parsed_hook_dict(): 'tags': [], 'on_schema_change': 'ignore', 'meta': {}, + 'grants': {}, }, 'docs': {'show': True}, 'columns': {}, 'meta': {}, 'checksum': {'name': 'sha256', 'checksum': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'}, 'unrendered_config': {}, + 'config_call_dict': {}, } @@ -850,6 +861,7 @@ def complex_parsed_hook_dict(): 'tags': [], 'on_schema_change': 'ignore', 'meta': {}, + 'grants': {}, }, 'docs': {'show': True}, 'columns': { @@ -866,6 +878,7 @@ def complex_parsed_hook_dict(): 'column_types': {'a': 'text'}, 'materialized': 'table', }, + 'config_call_dict': {}, } @@ -957,6 +970,7 @@ def minimal_parsed_schema_test_dict(): 'kwargs': {}, }, 'checksum': {'name': 'sha256', 'checksum': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'}, + 'config_call_dict': {}, } @@ -1002,6 +1016,7 @@ def basic_parsed_schema_test_dict(): }, 'checksum': {'name': 'sha256', 'checksum': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'}, 'unrendered_config': {}, + 'config_call_dict': {}, } @@ -1086,6 +1101,7 @@ def complex_parsed_schema_test_dict(): 'materialized': 'table', 'severity': 'WARN' }, + 'config_call_dict': {}, } @@ -1183,6 +1199,7 @@ def basic_timestamp_snapshot_config_dict(): 'target_schema': 'some_snapshot_schema', 'on_schema_change': 'ignore', 'meta': {}, + 'grants': {}, } @@ -1216,6 +1233,7 @@ def complex_timestamp_snapshot_config_dict(): 'updated_at': 'last_update', 'on_schema_change': 'ignore', 'meta': {}, + 'grants': {}, } @@ -1273,6 +1291,7 @@ def basic_check_snapshot_config_dict(): 'check_cols': 'all', 'on_schema_change': 'ignore', 'meta': {}, + 'grants': {}, } @@ -1306,6 +1325,7 @@ def complex_set_snapshot_config_dict(): 'check_cols': ['a', 'b'], 'on_schema_change': 'ignore', 'meta': {}, + 'grants': {}, } @@ -1412,6 +1432,7 @@ def basic_timestamp_snapshot_dict(): 'updated_at': 'last_update', 'on_schema_change': 'ignore', 'meta': {}, + 'grants': {}, }, 'docs': {'show': True}, 'columns': {}, @@ -1424,6 +1445,7 @@ def basic_timestamp_snapshot_dict(): 'target_database': 'some_snapshot_db', 'target_schema': 'some_snapshot_schema', }, + 'config_call_dict': {}, } @@ -1545,6 +1567,7 @@ def basic_check_snapshot_dict(): 'check_cols': 'all', 'on_schema_change': 'ignore', 'meta': {}, + 'grants': {}, }, 'docs': {'show': True}, 'columns': {}, @@ -1557,6 +1580,7 @@ def basic_check_snapshot_dict(): 'strategy': 'check', 'check_cols': 'all', }, + 'config_call_dict': {}, } diff --git a/test/unit/test_manifest.py b/test/unit/test_manifest.py index 1b7ec64b485..1e2f69d4ce8 100644 --- a/test/unit/test_manifest.py +++ b/test/unit/test_manifest.py @@ -44,7 +44,7 @@ 'depends_on', 'database', 'schema', 'name', 'resource_type', 'package_name', 'root_path', 'path', 'original_file_path', 'raw_sql', 'description', 'columns', 'fqn', 'build_path', 'compiled_path', 'patch_path', 'docs', - 'deferred', 'checksum', 'unrendered_config', 'created_at', + 'deferred', 'checksum', 'unrendered_config', 'created_at', 'config_call_dict', }) REQUIRED_COMPILED_NODE_KEYS = frozenset(REQUIRED_PARSED_NODE_KEYS | { diff --git a/test/unit/test_model_config.py b/test/unit/test_model_config.py index c3195a70e32..ca22a8c5720 100644 --- a/test/unit/test_model_config.py +++ b/test/unit/test_model_config.py @@ -11,13 +11,14 @@ class ThingWithMergeBehavior(dbtClassMixin): appended: List[str] = field(metadata={'merge': MergeBehavior.Append}) updated: Dict[str, int] = field(metadata={'merge': MergeBehavior.Update}) clobbered: str = field(metadata={'merge': MergeBehavior.Clobber}) + keysappended: Dict[str, int] = field(metadata={'merge': MergeBehavior.DictKeyAppend}) def test_merge_behavior_meta(): existing = {'foo': 'bar'} initial_existing = existing.copy() - assert set(MergeBehavior) == {MergeBehavior.Append, MergeBehavior.Update, MergeBehavior.Clobber} + assert set(MergeBehavior) == {MergeBehavior.Append, MergeBehavior.Update, MergeBehavior.Clobber, MergeBehavior.DictKeyAppend} for behavior in MergeBehavior: assert behavior.meta() == {'merge': behavior} assert behavior.meta(existing) == {'merge': behavior, 'foo': 'bar'} @@ -27,11 +28,12 @@ def test_merge_behavior_meta(): def test_merge_behavior_from_field(): fields = [f[0] for f in ThingWithMergeBehavior._get_fields()] fields = {name: f for f, name in ThingWithMergeBehavior._get_fields()} - assert set(fields) == {'default_behavior', 'appended', 'updated', 'clobbered'} + assert set(fields) == {'default_behavior', 'appended', 'updated', 'clobbered', 'keysappended'} assert MergeBehavior.from_field(fields['default_behavior']) == MergeBehavior.Clobber assert MergeBehavior.from_field(fields['appended']) == MergeBehavior.Append assert MergeBehavior.from_field(fields['updated']) == MergeBehavior.Update assert MergeBehavior.from_field(fields['clobbered']) == MergeBehavior.Clobber + assert MergeBehavior.from_field(fields['keysappended']) == MergeBehavior.DictKeyAppend @dataclass diff --git a/test/unit/test_parser.py b/test/unit/test_parser.py index 85d301ee13e..3f34b5eb351 100644 --- a/test/unit/test_parser.py +++ b/test/unit/test_parser.py @@ -514,6 +514,9 @@ def test_basic(self): raw_sql=raw_sql, checksum=block.file.checksum, unrendered_config={'materialized': 'table'}, + config_call_dict={ + 'materialized': 'table', + }, ) assertEqualNodes(node, expected) file_id = 'snowplow://' + normalize('models/nested/model_1.sql') @@ -800,6 +803,13 @@ def test_single_block(self): 'strategy': 'timestamp', 'updated_at': 'last_update', }, + config_call_dict={ + 'strategy': 'timestamp', + 'target_database': 'dbt', + 'target_schema': 'analytics', + 'unique_key': 'id', + 'updated_at': 'last_update', + }, ) assertEqualNodes(expected, node) file_id = 'snowplow://' + normalize('snapshots/nested/snap_1.sql') @@ -861,6 +871,13 @@ def test_multi_block(self): 'strategy': 'timestamp', 'updated_at': 'last_update', }, + config_call_dict={ + 'strategy': 'timestamp', + 'target_database': 'dbt', + 'target_schema': 'analytics', + 'unique_key': 'id', + 'updated_at': 'last_update', + }, ) expect_bar = ParsedSnapshotNode( alias='bar', @@ -891,6 +908,13 @@ def test_multi_block(self): 'strategy': 'timestamp', 'updated_at': 'last_update', }, + config_call_dict={ + 'strategy': 'timestamp', + 'target_database': 'dbt', + 'target_schema': 'analytics', + 'unique_key': 'id', + 'updated_at': 'last_update', + }, ) assertEqualNodes(nodes[0], expect_bar) assertEqualNodes(nodes[1], expect_foo) diff --git a/tests/functional/artifacts/expected_manifest.py b/tests/functional/artifacts/expected_manifest.py index 305e1bbeae5..5a5916d3ccf 100644 --- a/tests/functional/artifacts/expected_manifest.py +++ b/tests/functional/artifacts/expected_manifest.py @@ -30,6 +30,7 @@ def get_rendered_model_config(**updates): "on_schema_change": "ignore", "meta": {}, "unique_key": None, + "grants": {}, } result.update(updates) return result @@ -57,6 +58,7 @@ def get_rendered_seed_config(**updates): "alias": None, "meta": {}, "unique_key": None, + "grants": {}, } result.update(updates) return result @@ -88,6 +90,7 @@ def get_rendered_snapshot_config(**updates): "unique_key": "id", "target_schema": None, "meta": {}, + "grants": {}, } result.update(updates) return result diff --git a/tests/functional/configs/test_grant_configs.py b/tests/functional/configs/test_grant_configs.py new file mode 100644 index 00000000000..6d4df29ed78 --- /dev/null +++ b/tests/functional/configs/test_grant_configs.py @@ -0,0 +1,156 @@ +import pytest + +from dbt.tests.util import run_dbt, get_manifest, write_file, write_config_file + +dbt_project_yml = """ +models: + test: + my_model: + +grants: + my_select: ["reporter", "bi"] +""" + +append_schema_yml = """ +version: 2 +models: + - name: my_model + config: + grants: + +my_select: ["someone"] +""" + + +my_model_base_sql = """ +select 1 as fun +""" + + +my_model_clobber_sql = """ +{{ config(grants={'my_select': ['other_user']}) }} +select 1 as fun +""" + +my_model_extend_sql = """ +{{ config(grants={'+my_select': ['other_user']}) }} +select 1 as fun +""" + +my_model_extend_string_sql = """ +{{ config(grants={'+my_select': 'other_user'}) }} +select 1 as fun +""" + +my_model_extend_twice_sql = """ +{{ config(grants={'+my_select': ['other_user']}) }} +{{ config(grants={'+my_select': ['alt_user']}) }} +select 1 as fun +""" + + +class TestGrantConfigs: + @pytest.fixture(scope="class") + def models(self): + return {"my_model.sql": my_model_base_sql} + + @pytest.fixture(scope="class") + def project_config_update(self): + return dbt_project_yml + + def test_model_grant_config(self, project, logs_dir): + # This test uses "my_select" instead of "select", so that when + # actual granting of permissions happens, it won't break this + # test. + results = run_dbt(["run"]) + assert len(results) == 1 + + manifest = get_manifest(project.project_root) + model_id = "model.test.my_model" + assert model_id in manifest.nodes + + model = manifest.nodes[model_id] + model_config = model.config + assert hasattr(model_config, "grants") + + # no schema grant, no model grant, just project + expected = {"my_select": ["reporter", "bi"]} + assert model_config.grants == expected + + # add model grant with clobber + write_file(my_model_clobber_sql, project.project_root, "models", "my_model.sql") + results = run_dbt(["run"]) + manifest = get_manifest(project.project_root) + model_config = manifest.nodes[model_id].config + + expected = {"my_select": ["other_user"]} + assert model_config.grants == expected + + # change model to extend grants + write_file(my_model_extend_sql, project.project_root, "models", "my_model.sql") + results = run_dbt(["run"]) + manifest = get_manifest(project.project_root) + model_config = manifest.nodes[model_id].config + + expected = {"my_select": ["reporter", "bi", "other_user"]} + assert model_config.grants == expected + + # add schema file with extend + write_file(append_schema_yml, project.project_root, "models", "schema.yml") + results = run_dbt(["run"]) + assert len(results) == 1 + + manifest = get_manifest(project.project_root) + model_config = manifest.nodes[model_id].config + + expected = {"my_select": ["reporter", "bi", "someone", "other_user"]} + assert model_config.grants == expected + + # change model file to have string instead of list + write_file(my_model_extend_string_sql, project.project_root, "models", "my_model.sql") + results = run_dbt(["run"]) + assert len(results) == 1 + + manifest = get_manifest(project.project_root) + model_config = manifest.nodes[model_id].config + + expected = {"my_select": ["reporter", "bi", "someone", "other_user"]} + assert model_config.grants == expected + + # change model file to have string instead of list + write_file(my_model_extend_twice_sql, project.project_root, "models", "my_model.sql") + results = run_dbt(["run"]) + assert len(results) == 1 + + manifest = get_manifest(project.project_root) + model_config = manifest.nodes[model_id].config + + expected = {"my_select": ["reporter", "bi", "someone", "other_user", "alt_user"]} + assert model_config.grants == expected + + # Remove grant from dbt_project + config = { + "config-version": 2, + "name": "test", + "version": "0.1.0", + "profile": "test", + "log-path": logs_dir, + } + write_config_file(config, project.project_root, "dbt_project.yml") + results = run_dbt(["run"]) + assert len(results) == 1 + + manifest = get_manifest(project.project_root) + model_config = manifest.nodes[model_id].config + + expected = {"my_select": ["someone", "other_user", "alt_user"]} + assert model_config.grants == expected + + # Remove my_model config, leaving only schema file + write_file(my_model_base_sql, project.project_root, "models", "my_model.sql") + results = run_dbt(["run"]) + assert len(results) == 1 + + manifest = get_manifest(project.project_root) + model_config = manifest.nodes[model_id].config + + expected = {"my_select": ["someone"]} + assert model_config.grants == expected diff --git a/tests/functional/list/test_list.py b/tests/functional/list/test_list.py index f2bae6d8e41..b360a6b4d50 100644 --- a/tests/functional/list/test_list.py +++ b/tests/functional/list/test_list.py @@ -90,6 +90,7 @@ def expect_snapshot_output(self, project): "check_cols": None, "on_schema_change": "ignore", "meta": {}, + "grants": {}, }, "unique_id": "snapshot.test.my_snapshot", "original_file_path": normalize("snapshots/snapshot.sql"), @@ -125,6 +126,7 @@ def expect_analyses_output(self): "alias": None, "meta": {}, "unique_key": None, + "grants": {}, }, "unique_id": "analysis.test.a", "original_file_path": normalize("analyses/a.sql"), @@ -161,6 +163,7 @@ def expect_model_output(self): "schema": None, "alias": None, "meta": {}, + "grants": {}, }, "original_file_path": normalize("models/ephemeral.sql"), "unique_id": "model.test.ephemeral", @@ -192,6 +195,7 @@ def expect_model_output(self): "schema": None, "alias": None, "meta": {}, + "grants": {}, }, "original_file_path": normalize("models/incremental.sql"), "unique_id": "model.test.incremental", @@ -219,6 +223,7 @@ def expect_model_output(self): "schema": None, "alias": None, "meta": {}, + "grants": {}, }, "original_file_path": normalize("models/sub/inner.sql"), "unique_id": "model.test.inner", @@ -246,6 +251,7 @@ def expect_model_output(self): "schema": None, "alias": None, "meta": {}, + "grants": {}, }, "original_file_path": normalize("models/outer.sql"), "unique_id": "model.test.outer", @@ -288,6 +294,7 @@ def expect_model_ephemeral_output(self): "schema": None, "alias": None, "meta": {}, + "grants": {}, }, "unique_id": "model.test.ephemeral", "original_file_path": normalize("models/ephemeral.sql"), @@ -349,6 +356,7 @@ def expect_seed_output(self): "schema": None, "alias": None, "meta": {}, + "grants": {}, }, "unique_id": "seed.test.seed", "original_file_path": normalize("seeds/seed.csv"),