Skip to content

Commit c83c99b

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 a1e6c83 commit c83c99b

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
@@ -2444,7 +2444,7 @@ def _apply_grants_config_expr(
24442444
table: exp.Table,
24452445
grant_config: GrantsConfig,
24462446
table_type: DataObjectType = DataObjectType.TABLE,
2447-
) -> t.List[exp.Grant]:
2447+
) -> t.List[exp.Expression]:
24482448
"""Returns SQLGlot Grant expressions to apply grants to a table.
24492449
24502450
Args:
@@ -2453,7 +2453,7 @@ def _apply_grants_config_expr(
24532453
table_type: The type of database object (TABLE, VIEW, MATERIALIZED_VIEW).
24542454
24552455
Returns:
2456-
List of SQLGlot Grant expressions.
2456+
List of SQLGlot expressions for grant operations.
24572457
24582458
Raises:
24592459
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
@@ -573,6 +573,7 @@ def to_sqlmesh(
573573
if physical_properties:
574574
model_kwargs["physical_properties"] = physical_properties
575575

576+
# A falsy grants config (None or {}) is considered as unmanaged per dbt semantics
576577
if self.grants:
577578
model_kwargs["grants"] = self.grants
578579

tests/core/test_model.py

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

1164911649

11650+
def test_grants_validation_empty_grantees():
11651+
model = create_sql_model(
11652+
"db.table", parse_one("SELECT 1 AS id"), kind="FULL", grants={"select": []}
11653+
)
11654+
assert model.grants == {"select": []}
11655+
11656+
1165011657
def test_grants_table_type_view():
1165111658
model = create_sql_model("test_view", parse_one("SELECT 1 as id"), kind="VIEW")
1165211659
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
@@ -4631,15 +4631,20 @@ def test_grants_unsupported_engine(make_mocked_engine_adapter, mocker):
46314631
sync_grants_mock.assert_not_called()
46324632

46334633

4634-
def test_grants_clears_grants(make_mocked_engine_adapter, mocker):
4634+
def test_grants_revokes_permissions(make_mocked_engine_adapter, mocker):
46354635
adapter = make_mocked_engine_adapter(EngineAdapter)
46364636
adapter.SUPPORTS_GRANTS = True
46374637
sync_grants_mock = mocker.patch.object(adapter, "_sync_grants_config")
46384638
strategy = ViewStrategy(adapter)
4639-
model = create_sql_model("test_model", parse_one("SELECT 1 as id"), grants={})
4639+
model = create_sql_model("test_model", parse_one("SELECT 1 as id"), grants={"select": []})
4640+
model2 = create_sql_model("test_model2", parse_one("SELECT 1 as id"), grants={})
46404641

46414642
strategy._apply_grants(model, "test_table", GrantsTargetLayer.PHYSICAL)
4643+
sync_grants_mock.assert_called_once()
4644+
4645+
sync_grants_mock.reset_mock()
46424646

4647+
strategy._apply_grants(model2, "test_table", GrantsTargetLayer.PHYSICAL)
46434648
sync_grants_mock.assert_called_once()
46444649

46454650

0 commit comments

Comments
 (0)