Statements let you run SQL commands at specific points during model execution. You can run code before your query, after it completes, or when views are created.
You can define statements at the model level (for specific needs) or at the project level via model_defaults (for consistent behavior across all models).
Statement types:
Pre-statements: Run before the main model query executes
Post-statements: Run after the main model query completes
On-virtual-update statements: Run when views are created or updated in the virtual layer
Concurrency Considerations
Pre-statements should generally only prepare the main query. Avoid creating or altering physical tables in pre-statements, if multiple models run concurrently, you could get race conditions or unpredictable behavior. Stick to session settings, UDFs, and temporary objects.
You can define statements at the project level using model_defaults in your configuration. Use this for setting up common behavior across all models, like session timeouts or default permissions.
How it works: Default statements run first, then model-specific statements. So if you set a default timeout in model_defaults and a model-specific timeout in a model, the model-specific one runs after (and can override the default).
fromvulcan.core.configimportConfig,ModelDefaultsConfigconfig=Config(model_defaults=ModelDefaultsConfig(dialect="snowflake",pre_statements=["SET query_timeout = 300000",],post_statements=["@IF(@runtime_stage = 'evaluating', ANALYZE @this_model)",],on_virtual_update=["GRANT SELECT ON @this_model TO ROLE analyst_role",],),)
MODEL(nameanalytics.orders,kindINCREMENTAL_BY_TIME_RANGE(time_columnorder_date),start'2020-01-01',cron'@daily');/* Pre-statement: Create table for anomaly tracking */CREATETABLEIFNOTEXISTSanalytics._orders_anomalies(anomaly_idBIGINTGENERATEDALWAYSASIDENTITY,order_idVARCHAR,anomaly_typeVARCHAR,detailsVARCHAR,captured_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP);/* Pre-statement: Set session variables using Jinja */JINJA_STATEMENT_BEGIN;{%ifstart_dateisnoneorend_dateisnone%}SETstart_date=DATE'{{ start }}';SETend_date=CURRENT_DATE;{%endif%}JINJA_END;/* Main model query */SELECTorder_id::VARCHARASorder_id,order_date::DATEASorder_date,customer_id::VARCHARAScustomer_id,total_amount::FLOATAStotal_amountFROMdemo.raw_data.ordersWHEREorder_dateBETWEEN@start_dateAND@end_date;
Post-statements run after your model query completes. They're great for cleanup, optimization, or validation tasks.
Important: When you use post-statements in SQL models, your main query must end with a semicolon. This tells Vulcan where the query ends and the statements begin.
Common use cases:
Creating indexes or clustering (for query performance)
fromvulcanimportExecutionContext,modelfromvulcanimportModelKindName@model("analytics.orders_py",columns={"order_id":"varchar","order_date":"date","customer_id":"varchar","total_amount":"float",},kind=dict(name=ModelKindName.INCREMENTAL_BY_TIME_RANGE,time_column="order_date",),post_statements=["@IF(@runtime_stage = 'creating', ALTER TABLE @this_model SET DATA_RETENTION_TIME_IN_DAYS = 30)","ALTER TABLE @this_model CLUSTER BY (order_date, customer_id)","""INSERT INTO analytics._orders_anomalies (order_id, anomaly_type, details) SELECT order_id, 'NEGATIVE_QUANTITY', CONCAT('Quantity=', quantity) FROM @this_model WHERE quantity < 0""",],)defexecute(context:ExecutionContext,start,end,**kwargs):query=f""" SELECT order_id, order_date, customer_id, total_amount FROM demo.raw_data.orders WHERE order_date BETWEEN '{start.date()}' AND '{end.date()}' """returncontext.fetchdf(query)
On-virtual-update statements run when views are created or updated in the virtual layer. This happens after your model's physical table is created and the view pointing to it is set up.
Common use cases:
Granting permissions on views (so users can query them)
Setting up access controls or row-level security
Applying column masking policies
Any view-level configuration
Think of it as: The "access control" phase, setting up who can see what.
Note: These statements run at the virtual layer, so table names (including @this_model) resolve to view names, not physical table names.
Use ON_VIRTUAL_UPDATE_BEGIN and ON_VIRTUAL_UPDATE_END to define these statements:
MODEL(nameanalytics.customers,kindINCREMENTAL_BY_UNIQUE_KEY(unique_keycustomer_id));SELECTcustomer_id,full_name,email,customer_segmentFROMdemo.raw_data.customers;/* Post-statement: Apply masking policy */JINJA_STATEMENT_BEGIN;ALTERTABLE{{this_model}}MODIFYCOLUMNemailSETMASKINGPOLICYdemo.security.mask_email_policy;JINJA_END;/* On-virtual-update: Grant permissions when view is created/updated */ON_VIRTUAL_UPDATE_BEGIN;JINJA_STATEMENT_BEGIN;GRANTSELECTONVIEW{{this_model}}TOROLEview_only_role;JINJA_END;ON_VIRTUAL_UPDATE_END;
Use the on_virtual_update argument in the @model decorator:
fromvulcanimportExecutionContext,modelfromvulcanimportModelKindName@model("analytics.customers_py",columns={"customer_id":"varchar","full_name":"varchar","email":"varchar","customer_segment":"varchar",},kind=dict(name=ModelKindName.INCREMENTAL_BY_UNIQUE_KEY,unique_key=["customer_id"],),post_statements=["@IF(@runtime_stage = 'creating', ALTER TABLE @this_model SET DATA_RETENTION_TIME_IN_DAYS = 7)",],on_virtual_update=["GRANT SELECT ON @this_model TO ROLE view_only_role",],)defexecute(context:ExecutionContext,**kwargs):query=""" SELECT customer_id, CONCAT(first_name, ' ', last_name) AS full_name, email, customer_segment FROM demo.raw_data.customers """returncontext.fetchdf(query)