PostgreSQL
SQLAnvil generates idiomatic PostgreSQL DDL/DML. A user who has never touched BigQuery should never encounter BigQuery-specific concepts (NOT ENFORCED primary keys, OPTIONS(...) table options, MERGE dialect).
Connection
Section titled “Connection”Non-secret settings go in workflow_settings.yaml (committed); the connection — including the
password — goes in .df-credentials.json (gitignored, never committed):
warehouse: postgresdefaultDataset: analytics # the schema your models build intodefaultAssertionDataset: sqlanvil_assertionssqlanvilCoreVersion: 1.26.2 # pin the release you installed — `sqlanvil init` writes this// .df-credentials.json (gitignored){ "host": "db.example.com", "port": 5432, "database": "analytics", "user": "sqlanvil_writer", "password": "your-password", "sslMode": "require", "defaultSchema": "public"}sslMode accepts disable | allow | prefer | require | verify-ca | verify-full. Pass a different
credentials path at run time with --credentials <path>.
Tables
Section titled “Tables”SQLAnvil creates Postgres tables using a transactional drop-and-recreate pattern — no BigQuery CREATE OR REPLACE TABLE:
-- definitions/orders.sqlxconfig { type: "table"}
SELECT order_id, customer_id, created_atFROM ${ref("raw_orders")}Generated SQL (simplified):
BEGIN;DROP TABLE IF EXISTS public.orders;CREATE TABLE public.orders AS SELECT ...;COMMIT;Incremental tables (upserts)
Section titled “Incremental tables (upserts)”Postgres uses INSERT ... ON CONFLICT — not BigQuery’s MERGE:
-- definitions/events.sqlxconfig { type: "incremental", uniqueKey: ["event_id"]}
SELECT event_id, user_id, event_timestamp, event_nameFROM ${ref("raw_events")}${when(incremental(), `WHERE event_timestamp > (SELECT MAX(event_timestamp) FROM ${self()})`)}Generated SQL for incremental runs:
INSERT INTO public.events (event_id, user_id, event_timestamp, event_name)SELECT ...ON CONFLICT (event_id) DO UPDATE SET user_id = EXCLUDED.user_id, event_timestamp = EXCLUDED.event_timestamp, event_name = EXCLUDED.event_name;Adding a column. An incremental run writes only the columns the table already has, so a column
you add to the query is silently not written. onSchemaChange has no effect on Postgres or
Supabase; it is BigQuery only. Rebuild with
--full-refresh, or run ALTER TABLE … ADD COLUMN before the run. An operation in the same run is
too late: the insert’s column list is fixed when the run starts.
Postgres-specific config (postgres: block)
Section titled “Postgres-specific config (postgres: block)”config { type: "table", postgres: { tablespace: "fast_ssd", fillfactor: 80, partition: { kind: "range", // "range", "list" or "hash" columns: ["order_date"] }, indexes: [ { name: "ix_orders_customer", columns: ["customer_id"] // method omitted → btree }, { name: "ix_orders_search", columns: ["description"], method: "gin" } ] }}SELECT ...postgres: field reference
Section titled “postgres: field reference”| Field | Type | Description |
|---|---|---|
tablespace | string | Postgres tablespace for the table |
fillfactor | number | Storage fillfactor (1–100) |
unlogged | boolean | Create as UNLOGGED TABLE (faster writes, not crash-safe) |
partition.kind | "range", "list" or "hash" | Declarative partitioning strategy |
partition.columns | string[] | Partition key columns |
indexes | Index[] | Indexes to create after the table |
with_data | boolean | WITH DATA vs WITH NO DATA for materialized views |
Index field reference
Section titled “Index field reference”| Field | Type | Description |
|---|---|---|
name | string | Index name |
columns | string[] | Indexed columns |
method | "btree" (the default), "hash", "gin", "gist" or "brin" | Index access method |
where | string | Partial index predicate (e.g. "status = 'active'") |
unique | boolean | Create a UNIQUE index |
include | string[] | Non-key columns to include (covering index) |
method and partition.kind take names in any case; an unknown name is a compile error. Before
1.33 a name was silently replaced by the first value (btree, or RANGE), so on an older
sqlanvilCoreVersion write the number instead: method 0–4 in the order above, kind
0 RANGE, 1 LIST, 2 HASH.
Native partitioning
Section titled “Native partitioning”config { type: "table", postgres: { partition: { kind: "range", columns: ["order_date"] } }}SELECT order_id, customer_id, order_date FROM ${ref("raw_orders")}Generates:
CREATE TABLE public.orders ( order_id BIGINT, customer_id BIGINT, order_date DATE) PARTITION BY RANGE (order_date);Configuration reference
Section titled “Configuration reference”workflow_settings.yaml (committed):
| Field | Required | Description |
|---|---|---|
warehouse | ✓ | postgres |
defaultDataset | ✓ | Default schema for actions (e.g. public) |
defaultAssertionDataset | Schema for generated assertions | |
sqlanvilCoreVersion | ✓ | Pin the @sqlanvil/core version (e.g. 1.0.1) |
.df-credentials.json (gitignored — the PostgresConnection):
| Field | Required | Description |
|---|---|---|
host | ✓ | Database host |
port | ✓ | Database port (default: 5432) |
database | ✓ | Database name |
user | ✓ | Database user |
password | ✓ | Password |
sslMode | disable, allow, prefer, require, verify-ca, verify-full | |
defaultSchema | Connection search-path schema (e.g. public) |