@@ -504,29 +504,24 @@ def test_grants_plan(engine_adapter: PostgresEngineAdapter, ctx: TestContext, tm
504504 assert virtual_grants == {"SELECT" : [roles ["analyst" ]["username" ]]}
505505
506506 # Update model with query change and new grants
507- updated_model_def = """
508- MODEL (
509- name test_schema.grant_model,
510- kind FULL,
511- grants (
512- 'select' = ['test_analyst', 'test_etl_user'],
513- 'insert' = ['test_etl_user']
514- ),
515- grants_target_layer 'all'
516- );
517- SELECT 1 as id, CURRENT_DATE as created_date, 'v2' as version
518- """
519-
520- (tmp_path / "models" / "grant_model.sql" ).write_text (updated_model_def )
521-
522- context = ctx .create_context (path = tmp_path )
507+ existing_model = context .get_model ("test_schema.grant_model" )
508+ from sqlglot import parse_one
509+
510+ updated_query = parse_one ("SELECT 1 as id, CURRENT_DATE as created_date, 'v2' as version" )
511+ context .upsert_model (
512+ existing_model ,
513+ query = updated_query ,
514+ grants = {
515+ "select" : [roles ["analyst" ]["username" ], roles ["etl_user" ]["username" ]],
516+ "insert" : [roles ["etl_user" ]["username" ]],
517+ },
518+ )
523519 plan_result = context .plan (auto_apply = True , no_prompts = True )
524- assert len (plan_result .directly_modified ) == 1
525520
526- modified_snapshot_id = next (iter (plan_result .directly_modified ))
527- new_snapshot = context .get_snapshot (modified_snapshot_id .name )
528- assert new_snapshot is not None
521+ assert len (plan_result .new_snapshots ) == 1
522+ new_snapshot = plan_result .new_snapshots [0 ]
529523
524+ assert new_snapshot is not None
530525 new_table_name = new_snapshot .table_name ()
531526 final_grants = engine_adapter ._get_current_grants_config (
532527 exp .to_table (new_table_name , dialect = engine_adapter .dialect )
@@ -697,7 +692,6 @@ def test_grants_plan_incremental_model(
697692
698693 context = ctx .create_context (path = tmp_path )
699694
700- # First plan
701695 plan_result = context .plan (
702696 "dev" , start = "2020-01-01" , end = "2020-01-01" , auto_apply = True , no_prompts = True
703697 )
@@ -777,72 +771,109 @@ def test_grants_plan_clone_environment(
777771 assert dev_view_grants == prod_grants
778772
779773
774+ @pytest .mark .parametrize (
775+ "model_name,kind_config,query,extra_config,needs_seed" ,
776+ [
777+ (
778+ "grants_full" ,
779+ "FULL" ,
780+ "SELECT 1 as id, 'unchanged_query' as data" ,
781+ "" ,
782+ False ,
783+ ),
784+ (
785+ "grants_view" ,
786+ "VIEW" ,
787+ "SELECT 1 as id, 'unchanged_query' as data" ,
788+ "" ,
789+ False ,
790+ ),
791+ (
792+ "grants_incr_time" ,
793+ "INCREMENTAL_BY_TIME_RANGE (time_column event_date)" ,
794+ "SELECT '2025-09-01'::date as event_date, 1 as id, 'unchanged_query' as data" ,
795+ "start '2025-09-01'," ,
796+ False ,
797+ ),
798+ (
799+ "grants_seed" ,
800+ "SEED (path '../seeds/grants_seed.csv')" ,
801+ "" ,
802+ "" ,
803+ True ,
804+ ),
805+ ],
806+ )
780807def test_grants_metadata_only_changes (
781- engine_adapter : PostgresEngineAdapter , ctx : TestContext , tmp_path : Path
808+ engine_adapter : PostgresEngineAdapter ,
809+ ctx : TestContext ,
810+ tmp_path : Path ,
811+ model_name : str ,
812+ kind_config : str ,
813+ query : str ,
814+ extra_config : str ,
815+ needs_seed : bool ,
782816):
783817 with create_users (engine_adapter , "reader" , "writer" , "admin" ) as roles :
784818 (tmp_path / "models" ).mkdir (exist_ok = True )
785819
820+ if needs_seed :
821+ (tmp_path / "seeds" ).mkdir (exist_ok = True )
822+ csv_content = "id,data\\ n1,unchanged_query"
823+ (tmp_path / "seeds" / f"{ model_name } .csv" ).write_text (csv_content )
824+
786825 initial_model_def = f"""
787826 MODEL (
788- name test_schema.metadata_grants_model,
789- kind FULL,
827+ name test_schema.{ model_name } ,
828+ kind { kind_config } ,
829+ { extra_config }
790830 grants (
791831 'select' = ['{ roles ["reader" ]["username" ]} ']
792832 ),
793833 grants_target_layer 'all'
794834 );
795- SELECT 1 as id, 'unchanged_query' as data
835+ { query }
796836 """
837+ (tmp_path / "models" / f"{ model_name } .sql" ).write_text (initial_model_def )
797838
798- (tmp_path / "models" / "metadata_grants_model.sql" ).write_text (initial_model_def )
799-
800- # Create initial model with grants
801839 context = ctx .create_context (path = tmp_path )
802840 initial_plan_result = context .plan (auto_apply = True , no_prompts = True )
803841
804842 assert len (initial_plan_result .new_snapshots ) == 1
805843 initial_snapshot = initial_plan_result .new_snapshots [0 ]
806844
807845 physical_table_name = initial_snapshot .table_name ()
846+ virtual_view_name = f"test_schema.{ model_name } "
847+
808848 initial_physical_grants = engine_adapter ._get_current_grants_config (
809849 exp .to_table (physical_table_name , dialect = engine_adapter .dialect )
810850 )
811851 assert initial_physical_grants == {"SELECT" : [roles ["reader" ]["username" ]]}
812852
813- virtual_view_name = f"test_schema.metadata_grants_model"
814853 initial_virtual_grants = engine_adapter ._get_current_grants_config (
815854 exp .to_table (virtual_view_name , dialect = engine_adapter .dialect )
816855 )
817856 assert initial_virtual_grants == {"SELECT" : [roles ["reader" ]["username" ]]}
818857
819- # Update grants ONLY (same SQL query, replace SELECT with writer and admin, add admin to INSERT)
820- updated_model_def = f"""
821- MODEL (
822- name test_schema.metadata_grants_model,
823- kind FULL,
824- grants (
825- 'select' = ['{ roles ["writer" ]["username" ]} ', '{ roles ["admin" ]["username" ]} '],
826- 'insert' = ['{ roles ["admin" ]["username" ]} ']
827- ),
828- grants_target_layer 'all'
829- );
830- SELECT 1 as id, 'unchanged_query' as data
831- """
832-
833- (tmp_path / "models" / "metadata_grants_model.sql" ).write_text (updated_model_def )
834-
835- context = ctx .create_context (path = tmp_path )
858+ # Metadata-only change: update grants only using upsert_model
859+ existing_model = context .get_model (f"test_schema.{ model_name } " )
860+ context .upsert_model (
861+ existing_model ,
862+ grants = {
863+ "select" : [roles ["writer" ]["username" ], roles ["admin" ]["username" ]],
864+ "insert" : [roles ["admin" ]["username" ]],
865+ },
866+ )
836867 context .plan (auto_apply = True , no_prompts = True )
837868
838- # Grants should be updated regardless of how the change is categorized
839- updated_physical_grants = engine_adapter ._get_current_grants_config (
840- exp .to_table (physical_table_name , dialect = engine_adapter .dialect )
841- )
842869 expected_grants = {
843870 "SELECT" : [roles ["writer" ]["username" ], roles ["admin" ]["username" ]],
844871 "INSERT" : [roles ["admin" ]["username" ]],
845872 }
873+
874+ updated_physical_grants = engine_adapter ._get_current_grants_config (
875+ exp .to_table (physical_table_name , dialect = engine_adapter .dialect )
876+ )
846877 assert set (updated_physical_grants .get ("SELECT" , [])) == set (expected_grants ["SELECT" ])
847878 assert updated_physical_grants .get ("INSERT" , []) == expected_grants ["INSERT" ]
848879
@@ -852,34 +883,6 @@ def test_grants_metadata_only_changes(
852883 assert set (updated_virtual_grants .get ("SELECT" , [])) == set (expected_grants ["SELECT" ])
853884 assert updated_virtual_grants .get ("INSERT" , []) == expected_grants ["INSERT" ]
854885
855- # Test removing grants (remove INSERT, replace SELECT with reader)
856- minimal_grants_model_def = f"""
857- MODEL (
858- name test_schema.metadata_grants_model,
859- kind FULL,
860- grants (
861- 'select' = ['{ roles ["reader" ]["username" ]} ']
862- ),
863- grants_target_layer 'all'
864- );
865- SELECT 1 as id, 'unchanged_query' as data
866- """
867-
868- (tmp_path / "models" / "metadata_grants_model.sql" ).write_text (minimal_grants_model_def )
869-
870- context = ctx .create_context (path = tmp_path )
871- context .plan (auto_apply = True , no_prompts = True )
872-
873- final_physical_grants = engine_adapter ._get_current_grants_config (
874- exp .to_table (physical_table_name , dialect = engine_adapter .dialect )
875- )
876- assert final_physical_grants == {"SELECT" : [roles ["reader" ]["username" ]]}
877-
878- final_virtual_grants = engine_adapter ._get_current_grants_config (
879- exp .to_table (virtual_view_name , dialect = engine_adapter .dialect )
880- )
881- assert final_virtual_grants == {"SELECT" : [roles ["reader" ]["username" ]]}
882-
883886
884887def _vde_dev_only_config (gateway : str , config : Config ) -> None :
885888 config .virtual_environment_mode = VirtualEnvironmentMode .DEV_ONLY
@@ -974,7 +977,6 @@ def test_grants_incremental_model_with_vde_dev_only(
974977 (tmp_path / "models" / "vde_incremental_model.sql" ).write_text (model_def )
975978
976979 context = ctx .create_context (path = tmp_path , config_mutator = _vde_dev_only_config )
977-
978980 context .plan ("prod" , auto_apply = True , no_prompts = True )
979981
980982 prod_table = "test_schema.vde_incremental_model"
0 commit comments