All project configuration lives in a single project.yaml file at the root of your project.
name: my_project # Project name (required)
version: "0.1.0" # Optional version string
connection: # Database connection (required)
type: duckdb # Engine type: "duckdb"
path: dev.duckdb # Engine-specific parameters
# Other params depend on the engine type
schema: analytics # Default schema for all models (default: public)
materialization: view # Default: view, table, ephemeral, materialized_view, table_incremental
sources: # External data sources (optional)
source_name:
schema: raw_data # Schema where source tables live
database: my_db # Optional: database name (for 3-part names)
tables: # List of table names
- customers
- orders
vars: # Variables available in SQL templates (optional)
min_amount: "0"
trial_days: "14"
environments: # Per-environment overrides (optional)
local: # Empty = inherits all defaults
staging:
schema: analytics_staging
prod:
schema: analytics_prod
connection:
type: postgres
host: prod-db.example.com
user: ${PROD_USER}
password: ${PROD_PASSWORD}
vars:
min_amount: "100"Required. The project name. Used for display purposes.
Required. Database connection settings. The type field determines which engine is used.
connection:
type: duckdb
path: dev.duckdb # Path to the DuckDB file (use :memory: for in-memory)
init_sql: "INSTALL postgres; LOAD postgres" # Optional: SQL to run after connecting| Parameter | Required | Default | Description |
|---|---|---|---|
path |
no | :memory: |
Path to the DuckDB database file |
init_sql |
no | — | SQL statements to execute after connecting (semicolon-separated) |
The init_sql parameter is useful for loading DuckDB extensions or attaching external databases. For example, to read from a PostgreSQL source using DuckDB's postgres extension:
connection:
type: duckdb
path: analytics.duckdb
init_sql: "INSTALL postgres; LOAD postgres; ATTACH 'postgresql://user:pass@host:5432/db' AS pg (TYPE POSTGRES, READ_ONLY)"See the postgres_to_duckdb example for a complete working setup.
Requires uv add qraft[postgres].
connection:
type: postgres
host: localhost
port: 5432
user: ${DB_USER}
password: ${DB_PASSWORD}
database: analytics
sslmode: require # Optional: SSL mode| Parameter | Required | Default | Description |
|---|---|---|---|
host |
no | localhost |
Database hostname |
port |
no | 5432 |
Database port |
user |
no | postgres |
Username |
password |
no | — | Password |
database |
no | postgres |
Database name |
sslmode |
no | — | SSL mode (require, verify-ca, verify-full, etc.) |
Requires uv add qraft[mysql].
connection:
type: mysql
host: localhost
port: 3306
user: ${DB_USER}
password: ${DB_PASSWORD}
database: analytics
charset: utf8mb4 # Optional: character set| Parameter | Required | Default | Description |
|---|---|---|---|
host |
no | localhost |
Database hostname |
port |
no | 3306 |
Database port |
user |
no | root |
Username |
password |
no | — | Password |
database |
yes | — | Database name |
charset |
no | — | Character set (e.g., utf8mb4) |
ssl |
no | — | SSL configuration dict |
Requires uv add qraft[trino].
connection:
type: trino
host: localhost
port: 8080
user: ${TRINO_USER}
catalog: iceberg
schema: default # Optional: default schema
http_scheme: https # Optional: use HTTPS (default: http)
roles: # Optional: catalog roles
iceberg: admin| Parameter | Required | Default | Description |
|---|---|---|---|
host |
no | localhost |
Trino coordinator hostname |
port |
no | 8080 |
Trino coordinator port |
user |
no | trino |
Username for authentication |
catalog |
yes | — | Default catalog for queries |
schema |
no | — | Default schema for queries |
http_scheme |
no | http |
Use https for TLS connections |
roles |
no | — | Dict of catalog-to-role mappings for authorization |
The following versions are used in the example projects and are the versions Qraft is regularly tested against. Server versions come from the Docker images in each example's docker-compose.yml; driver versions are locked in uv.lock.
| Engine | Python Driver | Driver Version | Server Version |
|---|---|---|---|
| DuckDB | duckdb |
1.5.0 | (embedded) |
| PostgreSQL | psycopg |
3.3.3 | PostgreSQL 17 |
| MySQL/MariaDB | pymysql |
1.1.2 | MariaDB 11.7 |
| Trino | trino |
0.337.0 | Trino 480 |
Minimum driver requirements: duckdb >= 1.0, psycopg >= 3.1, pymysql >= 1.1, trino >= 0.328.
Optional. Default: public. The default schema where models are materialized. A model named stg_orders in schema analytics becomes analytics.stg_orders.
Default materialization strategy for all models. Can be overridden per model with front-matter.
view(default) —CREATE OR REPLACE VIEWtable—DROP TABLE IF EXISTS; CREATE TABLE AStable_incremental—INSERT INTO(append), orDELETE + INSERTwithunique_key(upsert). On first run, the table is created automatically.ephemeral— No database object created; inlined as a CTE into downstream modelsmaterialized_view—CREATE MATERIALIZED VIEW IF NOT EXISTS(Postgres, Trino only)
Not all engines support all materializations. See Materialization Types — Engine Support Matrix for the full compatibility table.
Dictionary of named data sources. Each source maps to external tables your models read from. Sources are read-only references — at compile time, source() calls resolve to fully qualified table names that the primary engine must be able to query. Qraft does not open separate database connections for sources. See Concepts — Cross-Database Access for details on how to read from external databases.
sources:
crm:
schema: crm_raw
tables:
- accounts
- contacts
billing:
database: billing_db # Optional 3-part name
schema: billing_raw
tables:
- invoices
- paymentsIn SQL: source('crm', 'accounts') resolves to crm_raw.accounts.
With a database: source('billing', 'invoices') resolves to billing_db.billing_raw.invoices.
Dictionary of string key-value pairs. Available in SQL as {{ key }}.
vars:
min_order_amount: "0"
currency: "USD"All values must be strings (wrap numbers in quotes). Variables can reference other variables (chaining) — the resolver runs multiple passes until all references are resolved. The maximum number of passes defaults to 10 and can be configured via the QRAFT_MAX_VAR_PASSES environment variable.
Dictionary of named environments. Each environment can override any top-level field. Overrides are deep-merged with the base configuration.
environments:
local: # Empty = use base config as-is
prod:
schema: analytics_prod # Override schema
connection: # Override connection
type: postgres
host: prod.example.com
vars: # Override specific vars
min_order_amount: "100"flowchart TD
A["project.yaml"] --> B["Parse YAML"]
B --> C["Resolve ${ENV_VAR} placeholders"]
C --> D["Validate config structure"]
D --> E{"--env flag"}
E --> F["Load base config<br/><small>connection, schema, materialization, sources, vars</small>"]
F --> G["Deep-merge environment overrides"]
G --> H["connection:<br/>type change → replace<br/>same type → merge params"]
G --> I["sources:<br/>per-source field merge"]
G --> J["vars:<br/>key-level merge"]
G --> K["schema, materialization:<br/>direct override"]
H --> L["EnvConfig"]
I --> L
J --> L
K --> L
When you run qraft run --env prod, the configuration is resolved as follows:
- Start with the base config (all top-level fields)
- Deep-merge the
prodenvironment overrides on top - For
varsandsources, individual keys are merged (not replaced entirely) - For
connection, if the engine type changes the override replaces the base connection entirely; if the type stays the same, individual parameters are merged
Base:
schema: analytics
vars:
min_amount: "0"
currency: "USD"Environment override:
schema: analytics_prod
vars:
min_amount: "100"Result:
schema: analytics_prod # Overridden
vars:
min_amount: "100" # Overridden
currency: "USD" # Inherited from baseUse ${ENV_VAR} syntax in project.yaml to reference shell environment variables. This is useful for secrets that shouldn't be committed to version control.
connection:
type: postgres
host: ${DB_HOST}
user: ${DB_USER}
password: ${DB_PASSWORD}Qraft loads .env files automatically via python-dotenv. Create a .env file in your project root:
DB_HOST=localhost
DB_USER=analytics
DB_PASSWORD=secret
Add .env to your .gitignore to keep secrets out of version control.
Models support YAML front-matter between --- delimiters to override settings and define tests. See Concepts — Front-matter for usage.
The columns block defines column-level metadata and data tests:
---
materialization: table
columns:
- name: order_id
description: Unique order identifier
tests:
- not_null
- unique
- name: status
description: Current order status
tests:
- accepted_values:
values: [pending, shipped, delivered, cancelled]
- name: customer_id
tests:
- not_null
- relationships:
to: ref('stg_customers')
field: customer_id
- name: amount
tests:
- accepted_range:
min_value: 0
---
SELECT ...Each column entry supports:
| Field | Description |
|---|---|
name |
Column name (required) |
description |
Human-readable column description (included in manifest) |
tests |
List of data tests to run on this column |
| Test | Parameters | Description |
|---|---|---|
not_null |
-- | Column has no NULL values |
unique |
-- | Column has no duplicate values |
accepted_values |
values (list) |
Column only contains values from the list |
relationships |
to, field |
Every value exists in the referenced model's column |
number_of_rows |
min_value (or min; default: 1) |
Table has at least the specified number of rows |
accepted_range |
min_value/max_value (or min/max; at least one) |
Column values fall within the numeric range |
unique_combination_of_columns |
combination or columns (list of column names) |
No duplicate rows for the given column combination |
Each test supports optional configuration:
columns:
- name: email
tests:
- not_null:
severity: warn # "error" (default) or "warn"
where: "active = true" # Filter rows before testingseverity: warn-- Test failures are reported but don't cause a non-zero exit codewhere-- SQL WHERE clause applied to the test results (the test query is wrapped in a subquery and this clause filters the output)