PostgresIOManager
The PostgresIOManager is a Dagster IO manager for reading and writing Pandas DataFrames to PostgreSQL. It provides automatic schema/table creation, type mapping, partitioning, incremental updates, and PostGIS geometry support.
Overview
When you return a DataFrame from a Dagster asset with io_manager_key="postgres_io_manager", it automatically:
- Creates the schema and table if they don't exist
- Maps Pandas/Arrow types to PostgreSQL types
- Tracks materializations for lineage and partition support
- Grants SELECT privileges to specified users
Basic Usage
import pandas as pd
import dagster as dg
@dg.asset(
io_manager_key="postgres_io_manager",
metadata={
"schema": "analytics",
"table": "daily_metrics", # optional, defaults to asset key
},
)
def daily_metrics():
return pd.DataFrame({
"date": ["2024-01-01", "2024-01-02"],
"value": [100, 200],
})
Metadata Options
Control how data is written using metadata:
| Option | Type | Default | Description |
|---|---|---|---|
schema | str | "public" | Target PostgreSQL schema |
table | str | asset key | Custom table name |
primary_key | str | list[str] | None | Column(s) for primary key constraint |
incremental | bool | str | False | Enable upsert mode (requires primary_key) |
columns | dict | None | Column type definitions and constraints |
chunk_size | str | "300MB" | Data chunk size for large outputs |
users | list[str] | [] | Users to grant SELECT privileges |
environment | str | None | Set to "production" to load inputs from prod |
full_table | bool | str | False | Ignore materialization IDs, load entire table |
columns (input) | list[str] | None | Column names to load (reduces memory for wide tables) |
Example with All Options
@dg.asset(
io_manager_key="postgres_io_manager",
metadata={
"schema": "transport",
"table": "routes",
"primary_key": ("start_id", "end_id"),
"incremental": "true",
"columns": {
"start_id": ("int", "not null"),
"end_id": ("int", "not null"),
"distance": "float",
"geometry": ("geometry", "not null"),
},
"chunk_size": "500MB",
"users": ["felt", "readonly_user"],
},
)
def truck_routes():
...
Column Type Definitions
The columns metadata lets you specify PostgreSQL types and constraints:
metadata={
"columns": {
# Simple type specification
"id": "int",
"name": "string",
"price": "float",
"active": "bool",
"created": "date",
"updated": "datetime",
# With constraints (use tuple)
"user_id": ("int", "not null"),
"email": ("string", "unique"),
# PostGIS geometry
"location": ("geometry", "not null"),
},
}
Available Types
| Type | PostgreSQL Mapping |
|---|---|
bool | BOOLEAN |
int, int32 | INTEGER |
uint | INTEGER (unsigned) |
float | DOUBLE PRECISION |
string | TEXT |
date | DATE |
datetime | TIMESTAMP |
geometry | GEOMETRY (PostGIS) |
Available Modifiers
not null- Column cannot be NULLunique- Column values must be unique
TableSchema Helpers
For assets that both write to PostgreSQL and shape DataFrames in Python, prefer
defining one shared TableSchema and reusing it for both metadata and DataFrame
conformance.
import pandas as pd
import dagster as dg
from shared.db import table_schema
DAILY_METRICS_SCHEMA = table_schema(
{
"run_date": "date",
"plant": "string",
"throughput_tpd": "float",
"is_valid": "bool",
},
primary_key=["run_date", "plant"],
)
@dg.asset(
io_manager_key="postgres_io_manager",
metadata=DAILY_METRICS_SCHEMA.metadata(schema="analytics"),
)
def daily_metrics() -> pd.DataFrame:
rows = [
{
"run_date": "2024-01-01",
"plant": "A",
"throughput_tpd": "125.4",
"is_valid": "true",
}
]
return DAILY_METRICS_SCHEMA.finalize(pd.DataFrame(rows))
This gives you one canonical schema definition for:
metadata["columns"]andmetadata["primary_key"]- typed empty outputs with
schema.empty_df() - column reordering, missing-column filling, and dtype conversion with
schema.finalize()/schema.convert()
TableSchema.metadata(...) also marks primary-key columns as not null, so you
do not need to repeat that constraint in both places.