Skip to content

Commit 7a06f2a

Browse files
committed
fix: rework dbt grants to sqlmesh grants converstion
- sqlmesh grants uses None it explicitly means unmanaged and {} to mean managed with no grants (revoke all existing grants) - empty {} dbt grants config is always considered as unmanaged dbt manifest always parses None grants as {}
1 parent c6fe3c0 commit 7a06f2a

File tree

8 files changed

+58
-176
lines changed

8 files changed

+58
-176
lines changed

sqlmesh/core/engine_adapter/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2458,7 +2458,7 @@ def _apply_grants_config_expr(
24582458
table: exp.Table,
24592459
grant_config: GrantsConfig,
24602460
table_type: DataObjectType = DataObjectType.TABLE,
2461-
) -> t.List[exp.Grant]:
2461+
) -> t.List[exp.Expression]:
24622462
"""Returns SQLGlot Grant expressions to apply grants to a table.
24632463
24642464
Args:
@@ -2467,7 +2467,7 @@ def _apply_grants_config_expr(
24672467
table_type: The type of database object (TABLE, VIEW, MATERIALIZED_VIEW).
24682468
24692469
Returns:
2470-
List of SQLGlot Grant expressions.
2470+
List of SQLGlot expressions for grant operations.
24712471
24722472
Raises:
24732473
NotImplementedError: If the engine does not support grants.

sqlmesh/core/engine_adapter/postgres.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def _dcl_grants_config_expr(
145145
dcl_cmd: t.Type[DCL],
146146
relation: exp.Expression,
147147
grant_config: GrantsConfig,
148-
) -> t.Union[t.List[exp.Grant], t.List[exp.Revoke]]:
148+
) -> t.List[exp.Expression]:
149149
expressions = []
150150
for privilege, principals in grant_config.items():
151151
if not principals:
@@ -154,23 +154,20 @@ def _dcl_grants_config_expr(
154154
grant = dcl_cmd(
155155
privileges=[exp.GrantPrivilege(this=exp.Var(this=privilege))],
156156
securable=relation,
157-
principals=principals, # use original strings so user can to choose quote or not
157+
principals=principals, # use original strings; no quoting
158158
)
159159
expressions.append(grant)
160160

161-
return expressions
161+
return t.cast(t.List[exp.Expression], expressions)
162162

163163
def _apply_grants_config_expr(
164164
self,
165165
table: exp.Table,
166166
grant_config: GrantsConfig,
167167
table_type: DataObjectType = DataObjectType.TABLE,
168-
) -> t.List[exp.Grant]:
168+
) -> t.List[exp.Expression]:
169169
# https://www.postgresql.org/docs/current/sql-grant.html
170-
return t.cast(
171-
t.List[exp.Grant],
172-
self._dcl_grants_config_expr(exp.Grant, table, grant_config),
173-
)
170+
return self._dcl_grants_config_expr(exp.Grant, table, grant_config)
174171

175172
def _revoke_grants_config_expr(
176173
self,
@@ -179,10 +176,7 @@ def _revoke_grants_config_expr(
179176
table_type: DataObjectType = DataObjectType.TABLE,
180177
) -> t.List[exp.Expression]:
181178
# https://www.postgresql.org/docs/current/sql-revoke.html
182-
return t.cast(
183-
t.List[exp.Expression],
184-
self._dcl_grants_config_expr(exp.Revoke, table, grant_config),
185-
)
179+
return self._dcl_grants_config_expr(exp.Revoke, table, grant_config)
186180

187181
def _get_current_grants_config(self, table: exp.Table) -> GrantsConfig:
188182
"""Returns current grants for a Postgres table as a dictionary."""

sqlmesh/core/model/meta.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -545,8 +545,7 @@ def parse_exp_to_str(e: exp.Expression) -> str:
545545
if grantee: # skip empty strings
546546
grantee_list.append(grantee)
547547

548-
if grantee_list:
549-
grants_dict[permission_name.strip()] = grantee_list
548+
grants_dict[permission_name.strip()] = grantee_list
550549

551550
return grants_dict
552551

sqlmesh/dbt/basemodel.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,11 @@ def _validate_hooks(cls, v: t.Union[str, t.List[t.Union[SqlStr, str]]]) -> t.Lis
153153

154154
@field_validator("grants", mode="before")
155155
@classmethod
156-
def _validate_grants(cls, v: t.Dict[str, str]) -> t.Dict[str, t.List[str]]:
156+
def _validate_grants(
157+
cls, v: t.Optional[t.Dict[str, str]]
158+
) -> t.Optional[t.Dict[str, t.List[str]]]:
159+
if v is None:
160+
return None
157161
return {key: ensure_list(value) for key, value in v.items()}
158162

159163
_FIELD_UPDATE_STRATEGY: t.ClassVar[t.Dict[str, UpdateStrategy]] = {

sqlmesh/dbt/model.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,7 @@ def to_sqlmesh(
627627
if physical_properties:
628628
model_kwargs["physical_properties"] = physical_properties
629629

630+
# A falsy grants config (None or {}) is considered as unmanaged per dbt semantics
630631
if self.grants:
631632
model_kwargs["grants"] = self.grants
632633

tests/core/test_model.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11690,6 +11690,13 @@ def test_grants_validation_no_grants():
1169011690
assert model.grants is None
1169111691

1169211692

11693+
def test_grants_validation_empty_grantees():
11694+
model = create_sql_model(
11695+
"db.table", parse_one("SELECT 1 AS id"), kind="FULL", grants={"select": []}
11696+
)
11697+
assert model.grants == {"select": []}
11698+
11699+
1169311700
def test_grants_table_type_view():
1169411701
model = create_sql_model("test_view", parse_one("SELECT 1 as id"), kind="VIEW")
1169511702
assert model.grants_table_type == DataObjectType.VIEW

tests/core/test_snapshot_evaluator.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4634,15 +4634,20 @@ def test_grants_unsupported_engine(make_mocked_engine_adapter, mocker):
46344634
sync_grants_mock.assert_not_called()
46354635

46364636

4637-
def test_grants_clears_grants(make_mocked_engine_adapter, mocker):
4637+
def test_grants_revokes_permissions(make_mocked_engine_adapter, mocker):
46384638
adapter = make_mocked_engine_adapter(EngineAdapter)
46394639
adapter.SUPPORTS_GRANTS = True
46404640
sync_grants_mock = mocker.patch.object(adapter, "_sync_grants_config")
46414641
strategy = ViewStrategy(adapter)
4642-
model = create_sql_model("test_model", parse_one("SELECT 1 as id"), grants={})
4642+
model = create_sql_model("test_model", parse_one("SELECT 1 as id"), grants={"select": []})
4643+
model2 = create_sql_model("test_model2", parse_one("SELECT 1 as id"), grants={})
46434644

46444645
strategy._apply_grants(model, "test_table", GrantsTargetLayer.PHYSICAL)
4646+
sync_grants_mock.assert_called_once()
4647+
4648+
sync_grants_mock.reset_mock()
46454649

4650+
strategy._apply_grants(model2, "test_table", GrantsTargetLayer.PHYSICAL)
46464651
sync_grants_mock.assert_called_once()
46474652

46484653

0 commit comments

Comments
 (0)