Skip to content

Commit c09e484

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 5c33b54 commit c09e484

8 files changed

Lines changed: 58 additions & 176 deletions

File tree

sqlmesh/core/engine_adapter/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2498,7 +2498,7 @@ def _apply_grants_config_expr(
24982498
table: exp.Table,
24992499
grant_config: GrantsConfig,
25002500
table_type: DataObjectType = DataObjectType.TABLE,
2501-
) -> t.List[exp.Grant]:
2501+
) -> t.List[exp.Expression]:
25022502
"""Returns SQLGlot Grant expressions to apply grants to a table.
25032503
25042504
Args:
@@ -2507,7 +2507,7 @@ def _apply_grants_config_expr(
25072507
table_type: The type of database object (TABLE, VIEW, MATERIALIZED_VIEW).
25082508
25092509
Returns:
2510-
List of SQLGlot Grant expressions.
2510+
List of SQLGlot expressions for grant operations.
25112511
25122512
Raises:
25132513
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
@@ -548,8 +548,7 @@ def parse_exp_to_str(e: exp.Expression) -> str:
548548
if grantee: # skip empty strings
549549
grantee_list.append(grantee)
550550

551-
if grantee_list:
552-
grants_dict[permission_name.strip()] = grantee_list
551+
grants_dict[permission_name.strip()] = grantee_list
553552

554553
return grants_dict
555554

sqlmesh/dbt/basemodel.py

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

160160
@field_validator("grants", mode="before")
161161
@classmethod
162-
def _validate_grants(cls, v: t.Dict[str, str]) -> t.Dict[str, t.List[str]]:
162+
def _validate_grants(
163+
cls, v: t.Optional[t.Dict[str, str]]
164+
) -> t.Optional[t.Dict[str, t.List[str]]]:
165+
if v is None:
166+
return None
163167
return {key: ensure_list(value) for key, value in v.items()}
164168

165169
_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
@@ -651,6 +651,7 @@ def to_sqlmesh(
651651
if physical_properties:
652652
model_kwargs["physical_properties"] = physical_properties
653653

654+
# A falsy grants config (None or {}) is considered as unmanaged per dbt semantics
654655
if self.grants:
655656
model_kwargs["grants"] = self.grants
656657

tests/core/test_model.py

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

1189511895

11896+
def test_grants_validation_empty_grantees():
11897+
model = create_sql_model(
11898+
"db.table", parse_one("SELECT 1 AS id"), kind="FULL", grants={"select": []}
11899+
)
11900+
assert model.grants == {"select": []}
11901+
11902+
1189611903
def test_grants_table_type_view():
1189711904
model = create_sql_model("test_view", parse_one("SELECT 1 as id"), kind="VIEW")
1189811905
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
@@ -4900,15 +4900,20 @@ def test_grants_unsupported_engine(make_mocked_engine_adapter, mocker):
49004900
sync_grants_mock.assert_not_called()
49014901

49024902

4903-
def test_grants_clears_grants(make_mocked_engine_adapter, mocker):
4903+
def test_grants_revokes_permissions(make_mocked_engine_adapter, mocker):
49044904
adapter = make_mocked_engine_adapter(EngineAdapter)
49054905
adapter.SUPPORTS_GRANTS = True
49064906
sync_grants_mock = mocker.patch.object(adapter, "_sync_grants_config")
49074907
strategy = ViewStrategy(adapter)
4908-
model = create_sql_model("test_model", parse_one("SELECT 1 as id"), grants={})
4908+
model = create_sql_model("test_model", parse_one("SELECT 1 as id"), grants={"select": []})
4909+
model2 = create_sql_model("test_model2", parse_one("SELECT 1 as id"), grants={})
49094910

49104911
strategy._apply_grants(model, "test_table", GrantsTargetLayer.PHYSICAL)
4912+
sync_grants_mock.assert_called_once()
4913+
4914+
sync_grants_mock.reset_mock()
49114915

4916+
strategy._apply_grants(model2, "test_table", GrantsTargetLayer.PHYSICAL)
49124917
sync_grants_mock.assert_called_once()
49134918

49144919

0 commit comments

Comments
 (0)