Skip to content

Commit cc82ddc

Browse files
committed
fix: allow dbt grants to be None
1 parent a1e6c83 commit cc82ddc

File tree

3 files changed

+36
-160
lines changed

3 files changed

+36
-160
lines changed

sqlmesh/dbt/basemodel.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ class BaseModelConfig(GeneralConfig):
126126
pre_hook: t.List[Hook] = Field([], alias="pre-hook")
127127
post_hook: t.List[Hook] = Field([], alias="post-hook")
128128
full_refresh: t.Optional[bool] = None
129-
grants: t.Dict[str, t.List[str]] = {}
129+
grants: t.Optional[t.Dict[str, t.List[str]]] = None
130130
columns: t.Dict[str, ColumnConfig] = {}
131131
quoting: t.Dict[str, t.Optional[bool]] = {}
132132

@@ -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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,7 @@ def to_sqlmesh(
573573
if physical_properties:
574574
model_kwargs["physical_properties"] = physical_properties
575575

576-
if self.grants:
576+
if self.grants is not None:
577577
model_kwargs["grants"] = self.grants
578578

579579
kind = self.model_kind(context)

tests/dbt/test_model.py

Lines changed: 29 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
from pathlib import Path
44

55
from sqlmesh import Context
6+
from sqlmesh.core.config.common import VirtualEnvironmentMode
67
from sqlmesh.core.model.meta import GrantsTargetLayer
78
from sqlmesh.dbt.common import Dependencies
89
from sqlmesh.dbt.context import DbtContext
910
from sqlmesh.dbt.model import ModelConfig
10-
from sqlmesh.dbt.target import PostgresConfig
11+
from sqlmesh.dbt.target import BigQueryConfig, DuckDbConfig, PostgresConfig
1112
from sqlmesh.dbt.test import TestConfig
1213
from sqlmesh.utils.yaml import YAML
1314

@@ -168,14 +169,7 @@ def test_load_invalid_ref_audit_constraints(
168169
assert context.snapshots[fqn].model.audits == []
169170

170171

171-
def test_model_grants_to_native_config() -> None:
172-
"""Test that dbt grants configuration is converted to SQLMesh native grants."""
173-
from sqlmesh.dbt.context import DbtContext
174-
from sqlmesh.dbt.target import DuckDbConfig
175-
from sqlmesh.core.config.common import VirtualEnvironmentMode
176-
from pathlib import Path
177-
178-
# Create a model with grants configuration
172+
def test_model_grants_to_sqlmesh_grants_config() -> None:
179173
grants_config = {
180174
"select": ["user1", "user2"],
181175
"insert": ["admin_user"],
@@ -203,74 +197,66 @@ def test_model_grants_to_native_config() -> None:
203197

204198

205199
def test_model_grants_empty_permissions() -> None:
206-
"""Test that empty grants lists are filtered out in native grants."""
207-
from sqlmesh.dbt.context import DbtContext
208-
from sqlmesh.dbt.target import DuckDbConfig
209-
from sqlmesh.core.config.common import VirtualEnvironmentMode
210-
from pathlib import Path
211-
212-
# Create a model with empty grants lists (should be filtered out)
213200
model_config = ModelConfig(
214201
name="test_model_empty",
215202
sql="SELECT 1 as id",
216-
grants={"select": [], "insert": ["admin_user"]}, # select empty, insert granted
203+
grants={"select": [], "insert": ["admin_user"]},
217204
path=Path("test_model_empty.sql"),
218205
)
219206

220-
# Create minimal context for conversion with proper target setup
221207
context = DbtContext()
222208
context.project_name = "test_project"
223209
context.target = DuckDbConfig(name="target", schema="test_schema")
224210

225-
# Convert to SQLMesh model
226211
sqlmesh_model = model_config.to_sqlmesh(
227212
context, virtual_environment_mode=VirtualEnvironmentMode.FULL
228213
)
229214

230-
# Verify that only non-empty grants are preserved
231215
model_grants = sqlmesh_model.grants
232-
expected_grants = {"insert": ["admin_user"]} # select should be filtered out
216+
expected_grants = {"insert": ["admin_user"]}
233217
assert model_grants == expected_grants
234218

235219

236220
def test_model_no_grants() -> None:
237-
"""Test that models without grants don't generate grant post_statements."""
238-
from sqlmesh.dbt.context import DbtContext
239-
from sqlmesh.dbt.target import DuckDbConfig
240-
from sqlmesh.core.config.common import VirtualEnvironmentMode
241-
from pathlib import Path
242-
243-
# Create a model without grants
244221
model_config = ModelConfig(
245222
name="test_model_no_grants",
246223
sql="SELECT 1 as id",
247-
grants={}, # No grants
248224
path=Path("test_model_no_grants.sql"),
249225
)
250226

251-
# Create minimal context for conversion with proper target setup
252227
context = DbtContext()
253228
context.project_name = "test_project"
254229
context.target = DuckDbConfig(name="target", schema="test_schema")
255230

256-
# Convert to SQLMesh model
257231
sqlmesh_model = model_config.to_sqlmesh(
258232
context, virtual_environment_mode=VirtualEnvironmentMode.FULL
259233
)
260234

261-
# Verify that no grants configuration is set
262235
grants_config = sqlmesh_model.grants
263236
assert grants_config is None
264237

265238

266-
def test_model_grants_valid_special_characters() -> None:
267-
"""Test that valid special characters in grantee names are accepted."""
268-
from sqlmesh.dbt.context import DbtContext
269-
from sqlmesh.dbt.target import DuckDbConfig
270-
from sqlmesh.core.config.common import VirtualEnvironmentMode
271-
from pathlib import Path
239+
def test_model_empty_grants() -> None:
240+
model_config = ModelConfig(
241+
name="test_model_empty_grants",
242+
sql="SELECT 1 as id",
243+
grants={},
244+
path=Path("test_model_empty_grants.sql"),
245+
)
246+
247+
context = DbtContext()
248+
context.project_name = "test_project"
249+
context.target = DuckDbConfig(name="target", schema="test_schema")
250+
251+
sqlmesh_model = model_config.to_sqlmesh(
252+
context, virtual_environment_mode=VirtualEnvironmentMode.FULL
253+
)
272254

273-
# Test various valid grantee formats
255+
grants_config = sqlmesh_model.grants
256+
assert grants_config == {}
257+
258+
259+
def test_model_grants_valid_special_characters() -> None:
274260
valid_grantees = [
275261
"user@domain.com",
276262
"service-account@project.iam.gserviceaccount.com",
@@ -292,35 +278,29 @@ def test_model_grants_valid_special_characters() -> None:
292278
context.project_name = "test_project"
293279
context.target = DuckDbConfig(name="target", schema="test_schema")
294280

295-
# Should not raise any errors
296281
sqlmesh_model = model_config.to_sqlmesh(
297282
context, virtual_environment_mode=VirtualEnvironmentMode.FULL
298283
)
299284

300-
# Verify that native grants configuration contains all grantees
301285
grants_config = sqlmesh_model.grants
302286
assert grants_config is not None
303287
assert "select" in grants_config
304288
assert grants_config["select"] == valid_grantees
305289

306290

307291
def test_model_grants_engine_specific_bigquery() -> None:
308-
"""Test BigQuery-specific grant syntax."""
309-
from sqlmesh.dbt.context import DbtContext
310-
from sqlmesh.dbt.target import BigQueryConfig
311-
from sqlmesh.core.config.common import VirtualEnvironmentMode
312-
from pathlib import Path
313-
314292
model_config = ModelConfig(
315293
name="test_model_bigquery",
316294
sql="SELECT 1 as id",
317-
grants={"bigquery.dataviewer": ["user@domain.com"], "select": ["analyst@company.com"]},
295+
grants={
296+
"bigquery.dataviewer": ["user@domain.com"],
297+
"select": ["analyst@company.com"],
298+
},
318299
path=Path("test_model.sql"),
319300
)
320301

321302
context = DbtContext()
322303
context.project_name = "test_project"
323-
# Create a BigQuery target
324304
context.target = BigQueryConfig(
325305
name="bigquery_target",
326306
project="test-project",
@@ -334,115 +314,7 @@ def test_model_grants_engine_specific_bigquery() -> None:
334314
context, virtual_environment_mode=VirtualEnvironmentMode.FULL
335315
)
336316

337-
# Verify that native grants configuration contains BigQuery permissions
338317
grants_config = sqlmesh_model.grants
339318
assert grants_config is not None
340319
assert grants_config["bigquery.dataviewer"] == ["user@domain.com"]
341320
assert grants_config["select"] == ["analyst@company.com"]
342-
343-
344-
def test_model_grants_engine_specific_snowflake() -> None:
345-
"""Test Snowflake-specific grant syntax."""
346-
from sqlmesh.dbt.context import DbtContext
347-
from sqlmesh.dbt.target import SnowflakeConfig
348-
from sqlmesh.core.config.common import VirtualEnvironmentMode
349-
from pathlib import Path
350-
351-
model_config = ModelConfig(
352-
name="test_model_snowflake",
353-
sql="SELECT 1 as id",
354-
grants={"select": ["role1"], "all": ["admin_role"]},
355-
path=Path("test_model.sql"),
356-
)
357-
358-
context = DbtContext()
359-
context.project_name = "test_project"
360-
# Create a Snowflake target
361-
context.target = SnowflakeConfig(
362-
name="snowflake_target",
363-
account="test_account",
364-
warehouse="test_warehouse",
365-
database="test_db",
366-
schema="test_schema",
367-
user="test_user",
368-
password="test_password", # Add required authentication
369-
)
370-
371-
sqlmesh_model = model_config.to_sqlmesh(
372-
context, virtual_environment_mode=VirtualEnvironmentMode.FULL
373-
)
374-
375-
# Verify that native grants configuration contains Snowflake permissions
376-
grants_config = sqlmesh_model.grants
377-
assert grants_config is not None
378-
assert grants_config["select"] == ["role1"]
379-
assert grants_config["all"] == ["admin_role"]
380-
381-
382-
def test_model_grants_project_level_inheritance() -> None:
383-
"""Test that grants from dbt_project.yml are inherited by models."""
384-
from sqlmesh.dbt.context import DbtContext
385-
from sqlmesh.dbt.target import DuckDbConfig
386-
from sqlmesh.core.config.common import VirtualEnvironmentMode
387-
from pathlib import Path
388-
389-
# Test model with project-level grants inheritance
390-
# Simulate what would happen when dbt processes grants from dbt_project.yml
391-
model_config = ModelConfig(
392-
name="test_model_inheritance",
393-
sql="SELECT 1 as id",
394-
# This simulates grants that come from dbt_project.yml (+grants) being merged
395-
# with model-specific grants through SQLMesh's KEY_EXTEND strategy
396-
grants={"select": ["project_user"], "insert": ["model_user"]}, # project + model merged
397-
path=Path("test_model.sql"),
398-
)
399-
400-
context = DbtContext()
401-
context.project_name = "test_project"
402-
context.target = DuckDbConfig(name="target", schema="test_schema")
403-
404-
sqlmesh_model = model_config.to_sqlmesh(
405-
context, virtual_environment_mode=VirtualEnvironmentMode.FULL
406-
)
407-
408-
# Verify both project-level and model-level grants were processed in native grants
409-
grants_config = sqlmesh_model.grants
410-
assert grants_config is not None
411-
assert grants_config["select"] == ["project_user"]
412-
assert grants_config["insert"] == ["model_user"]
413-
414-
415-
def test_model_grants_additive_syntax() -> None:
416-
"""Test that additive grants (+prefix) work correctly."""
417-
from sqlmesh.dbt.context import DbtContext
418-
from sqlmesh.dbt.target import DuckDbConfig
419-
from sqlmesh.core.config.common import VirtualEnvironmentMode
420-
from pathlib import Path
421-
422-
# Test model that would receive +grants from dbt_project.yml
423-
# This simulates the result after dbt processes +grants syntax
424-
model_config = ModelConfig(
425-
name="test_model_additive",
426-
sql="SELECT 1 as id",
427-
# This simulates what the final grants dict looks like after dbt processes
428-
# +grants from dbt_project.yml and merges with model grants
429-
grants={
430-
"select": ["base_user", "additional_user"], # base + additive merged
431-
"insert": ["admin_user"], # model-specific
432-
},
433-
path=Path("test_model.sql"),
434-
)
435-
436-
context = DbtContext()
437-
context.project_name = "test_project"
438-
context.target = DuckDbConfig(name="target", schema="test_schema")
439-
440-
sqlmesh_model = model_config.to_sqlmesh(
441-
context, virtual_environment_mode=VirtualEnvironmentMode.FULL
442-
)
443-
444-
# Verify that native grants configuration contains all merged grants
445-
grants_config = sqlmesh_model.grants
446-
assert grants_config is not None
447-
assert grants_config["select"] == ["base_user", "additional_user"]
448-
assert grants_config["insert"] == ["admin_user"]

0 commit comments

Comments
 (0)