Skip to content

Conversation

newtonapple
Copy link
Contributor

@newtonapple newtonapple commented Sep 2, 2025

Native grants for SQLMesh models

SQLMesh now supports configuring model-level grants directly in model definitions. Use the new grants and grants_target_layer properties to assign privileges to specific roles.

  • grants: maps each privilege to the list of grantees.
  • grants_target_layer: selects the data layer object that receives the grant (virtual default view, physical table, or all).

Note: When virtual_environment_mode is dev_only, grants_target_layer is ignored for the prod environment. Per-environment plans (e.g. sqlmesh plan staging) still honor the configured target layer.

This PR only has grants support for the Postgres engine adapter.

Model configuration example

MODEL (
    name prod.user_ids,
    kind FULL,
    grants (
        'select' = ['readonly_user', 'dashboard_user']
    ),
    grants_target_layer virtual,
);
SELECT 1 AS id;

See tests/core/test_model.py for additional scenarios.

Fixes #569.

@newtonapple newtonapple requested a review from Copilot September 2, 2025 08:50
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces experimental support for grants configuration, allowing users to specify database permissions for SQLMesh models. The feature includes grants parsing, validation, and application across different evaluation strategies.

  • Adds grants configuration support to models with validation for compatible model types
  • Implements grants application logic in snapshot evaluators for physical and virtual layers
  • Provides PostgreSQL engine adapter implementation with grants management functionality

Reviewed Changes

Copilot reviewed 19 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/dbt/test_model.py Tests dbt model grants conversion to SQLMesh format and validation
tests/core/test_snapshot_evaluator.py Tests grants application in snapshot evaluation strategies
tests/core/test_model.py Tests model grants parsing, validation, and rendering functionality
tests/core/engine_adapter/test_postgres.py Unit tests for PostgreSQL grants operations (apply, revoke, sync)
tests/core/engine_adapter/test_base_postgres.py Tests PostgreSQL current schema retrieval functionality
tests/core/engine_adapter/test_base.py Tests base engine adapter grants configuration diffing logic
tests/core/engine_adapter/integration/test_integration_postgres.py Integration tests for PostgreSQL grants with real database operations
sqlmesh/dbt/model.py Adds grants field to SQLMesh model conversion
sqlmesh/dbt/basemodel.py Updates grants field validation to handle optional grants
sqlmesh/core/snapshot/evaluator.py Implements grants application in evaluation strategies
sqlmesh/core/model/meta.py Defines grants target layer enum and grants property parsing
sqlmesh/core/model/definition.py Adds grants to model metadata hash and table type determination
sqlmesh/core/model/common.py Registers grants field for property parsing
sqlmesh/core/engine_adapter/postgres.py Implements PostgreSQL-specific grants operations
sqlmesh/core/engine_adapter/base_postgres.py Adds current schema retrieval for grants queries
sqlmesh/core/engine_adapter/base.py Defines base grants interface and configuration diffing
sqlmesh/core/engine_adapter/_typing.py Adds GrantsConfig type definition
sqlmesh/core/_typing.py Minor formatting change
pyproject.toml Updates sqlglot dependency version

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@newtonapple newtonapple force-pushed the ddai/grants branch 2 times, most recently from 1019d57 to ada2040 Compare September 2, 2025 22:26
@newtonapple newtonapple force-pushed the ddai/grants branch 3 times, most recently from 8a93de3 to ef2ca9b Compare September 9, 2025 05:14
@newtonapple newtonapple force-pushed the ddai/grants branch 6 times, most recently from 54ca124 to a7e72f5 Compare September 16, 2025 17:02
render_kwargs,
skip_grants=True, # Skip grants; they're applied after data insertion
**kwargs,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this create is even needed? We do the create a few lines down with the replace query. I wonder if we did this before to maintain the contract of creating empty table first but we don't do that anymore. I bring it up since I then think you could just have it grant and not have to do the skip grants here.

@newtonapple newtonapple force-pushed the ddai/grants branch 3 times, most recently from 653e987 to 9da034d Compare September 18, 2025 08:25
- 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 {}
@newtonapple newtonapple changed the title feat(experimental): add official support for grants feat(experimental): add official support for model grants Sep 18, 2025
@newtonapple newtonapple marked this pull request as ready for review September 18, 2025 18:13
@@ -126,6 +161,14 @@ def _normalize(value: t.Any) -> t.Any:

return v

@classmethod
def _validate_str_enum_value(cls, v: t.Any) -> t.Any:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dead code?

@@ -465,6 +520,67 @@ def custom_materialization_properties(self) -> CustomMaterializationProperties:
return self.kind.materialization_properties
return {}

@cached_property
def grants(self) -> t.Optional[GrantsConfig]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems a little weird to go through the trouble of converting t.Dict[str, t.List[str]] into exp.Tuple just to convert it back again here.

def normalize_to_string_list(value_expr: exp.Expression) -> t.List[str]:
result = []

def process_expression(expr: exp.Expression) -> None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 level of nested function definitions is probably a bit too much. Any opportunity to move exp_to_string and normalize_to_string_list out?


model_grants_target_layer = model.grants_target_layer

is_prod_and_dev_only = is_snapshot_deployable and model.virtual_environment_mode.is_dev_only
Copy link
Member

@izeigerman izeigerman Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the name is a bit misleading. Without reading the expression itself it reads as "Is Prod and Dev only?"


is_prod_and_dev_only = is_snapshot_deployable and model.virtual_environment_mode.is_dev_only

if not (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid a somewhat confusing inversion can we just put the grant application under the if and put the skip debug log message in else?

is_snapshot_deployable = deployability_index.is_deployable(snapshot)

# Apply grants to the physical layer (referenced table / view) after promotion
self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable)
Copy link
Member

@izeigerman izeigerman Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we applying the physical grants as part of promotion?

**kwargs,
)

if not skip_grants:
# Apply grants after SCD Type 2 table creation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we recreate a table for SCD 2 every time? Shouldn't we add grants on insert then too?

if self.adapter.table_exists(table_name):
# Make sure we don't recreate the view to prevent deletion of downstream views in engines with no late
# binding support (because of DROP CASCADE).
logger.info("View '%s' already exists", table_name)

if not skip_grants:
# Always apply grants when present, even if view exists, to handle grants updates
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't migration taking care of that?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support dbt grants config
3 participants