Skip to content

Views

File location: schema/views/<name>.yaml

name: active_users
query: |
SELECT id, email, name
FROM users
WHERE deleted_at IS NULL
options:
security_invoker: true
check_option: cascaded
triggers:
- name: active_users_instead_insert
timing: INSTEAD OF
events: [INSERT]
function: redirect_active_users_insert
for_each: ROW
grants:
- to: app_readonly
privileges: [SELECT]
comment: 'Users who have not been soft-deleted'
FieldTypeRequiredDescription
namestringyesView name
querystringyesSQL SELECT query
optionsobjectnoView options (WITH clause)
triggersobject[]noINSTEAD OF triggers on the view
grantsobject[]noAccess grants
commentstringnoDescription

Views are created with CREATE OR REPLACE VIEW. When the query changes, the view is replaced.

The options field maps to the WITH (...) clause on the view. Supported keys:

KeyTypeDescription
security_barrierbooleanPrevents leaking rows through user-defined functions in WHERE clauses. Useful for security-defining views.
security_invokerbooleanRuns the view query with the permissions of the calling user, not the view owner (PostgreSQL 15+).
check_optionlocal | cascadedRejects INSERT/UPDATE through the view if the resulting row would not be visible. cascaded checks nested views too.
options:
security_barrier: true
security_invoker: true
check_option: cascaded

Regular views support INSTEAD OF triggers, which intercept INSERT, UPDATE, or DELETE operations on the view and redirect them to the underlying tables.

Each trigger entry uses the same format as table triggers, but the timing field must be INSTEAD OF.

triggers:
- name: active_users_instead_insert
timing: INSTEAD OF
events: [INSERT]
function: redirect_active_users_insert
for_each: ROW
- name: active_users_instead_update
timing: INSTEAD OF
events: [UPDATE]
function: redirect_active_users_update
for_each: ROW
FieldTypeRequiredDescription
namestringyesTrigger name
timingstringyesMust be INSTEAD OF for view triggers
eventsstring[]yesINSERT, UPDATE, DELETE (one or more)
functionstringyesTrigger function to execute
for_eachstringnoROW (default) or STATEMENT
whenstringnoSQL condition (not supported with INSTEAD OF)
commentstringnoDescription
name: user_stats
materialized: true
query: |
SELECT user_id, count(*) AS order_count
FROM orders
GROUP BY user_id
indexes:
- columns: [user_id]
unique: true
grants:
- to: app_readonly
privileges: [SELECT]
comment: 'Aggregated user order statistics'
FieldTypeRequiredDescription
materializedtrueyesMust be true
indexesobject[]noIndexes on the materialized view
  • When a materialized view’s query changes, the tool drops and recreates it, then refreshes the data
  • Indexes, grants, and comments are applied after creation
  • Index format is the same as table indexes