Back to changelog
New
3 minute read

Snowflake: Masking, Projection, Aggregation, and Join Policies

Atlas now manages Snowflake masking, projection, aggregation, and join policies next to row access policies. Declare the policy body in HCL or SQL, attach it to a table or a column with a policy block, and Atlas inspects, diffs, and plans the CREATE, ALTER, and attachment statements.

Snowflake governs data with schema-level policy objects: a masking policy rewrites a column value per query, a projection policy decides whether a column may be selected, an aggregation policy forces queries to aggregate over a minimum group, and a join policy forces a table to be joined before it is read. Atlas already managed row access policies, and now manages the other four the same way, including the attachments that bind them to a table or a column.

Declaring Policies

Each kind has its own block. A masking policy carries a signature, so it takes arg blocks and a return type, plus the optional exempt_other_policies flag. The other three are body-only: the expression in as is the whole policy. All four accept a comment, tag blocks, and depends_on.

masking_policy "MP_SSN" {  schema = schema.PUBLIC  arg "VAL" {    type = VARCHAR(100)  }  return                = VARCHAR(100)  as                    = "'***'"  exempt_other_policies = true}
projection_policy "PP" {  schema = schema.PUBLIC  as     = "PROJECTION_CONSTRAINT(ALLOW => FALSE)"}
aggregation_policy "AP" {  schema = schema.PUBLIC  as     = "AGGREGATION_CONSTRAINT(MIN_GROUP_SIZE => 11)"}
join_policy "JP" {  schema = schema.PUBLIC  as     = "JOIN_CONSTRAINT(JOIN_REQUIRED => TRUE)"}

Attaching a Policy

Attachments are written with a repeatable policy block whose label names the kind. A column takes masking and projection; a table, external table, or Iceberg table takes row_access, aggregation, and join. Kinds that read other columns name them: using for a conditional masking policy, columns for a row access policy, and entity_key for an aggregation policy.

table "T" {  schema = schema.PUBLIC  column "ID" {    null = false    type = VARCHAR(30)    policy "masking" {      ref   = masking_policy.MP_SSN      using = ["FLAG"]    }  }  column "PLAN_ID" {    null = false    type = VARCHAR(20)    policy "projection" {      ref = projection_policy.PP    }  }  policy "row_access" {    ref     = row_access_policy.RAP    columns = ["PLAN_ID"]  }  policy "aggregation" {    ref        = aggregation_policy.AP    entity_key = ["ID"]  }  policy "join" {    ref = join_policy.JP  }}

ref is normally a reference to a policy block. It also accepts a qualified string, for the case where the policy lives outside the inspected scope and has no block to point at. Both forms describe the same desired state, so schema apply and migrate diff accept either one as the --to URL.

Generated SQL

Attachments that Snowflake accepts at creation time are written into the CREATE TABLE statement, in the order the grammar requires:

migration.sql
-- Create "T" tableCREATE TABLE "PUBLIC"."T" (  "ID" VARCHAR(30) NOT NULL WITH MASKING POLICY "PUBLIC"."MP_SSN" USING ("ID", "FLAG"),  "PLAN_ID" VARCHAR(20) NOT NULL WITH PROJECTION POLICY "PUBLIC"."PP") WITH AGGREGATION POLICY "PUBLIC"."AP" ENTITY KEY ("ID")  WITH ROW ACCESS POLICY "PUBLIC"."RAP" ON ("PLAN_ID")  WITH JOIN POLICY "PUBLIC"."JP";

On an existing table they become ALTER TABLE ... SET and UNSET statements. Replacing an aggregation or join policy in place emits FORCE, which Snowflake requires to swap one for another, and every statement carries its inverse so migrate down works.

Altered in Place, or Recreated

Diffing separates what Snowflake can change from what it cannot. A new body, comment, or tag becomes ALTER ... SET BODY, SET COMMENT, or SET TAG. A changed signature on a masking policy, such as a renamed argument or a different argument or return type, recreates the policy, and so does a change to exempt_other_policies, which Snowflake fixes at creation time.

Policy bodies commonly query a mapping table or view, and Snowflake resolves the body when the policy is created, so Atlas orders the plan to create those objects first. depends_on covers the references the parser cannot see.

Getting Started

Policy management is opt-in, and the same flag covers all five kinds. Enable it in the mode "snowflake" block of your env:

atlas.hcl
env "snow" {  schema {    mode "snowflake" {      policies = true    }  }}

Snowflake support is part of Atlas Pro. Run atlas login to get started.

featuresnowflakepoliciessecuritydeclarative