Skip to content

PostgreSQL Features

HyperDjango is PostgreSQL-native. The pg.zig driver provides first-class support for PostgreSQL-specific types and features.

Native Type Support

pg.zig handles 30+ PostgreSQL type OIDs natively — no Python-side parsing needed:

PostgreSQL Type Python Type Notes
int2, int4, int8 int 16/32/64-bit integers
float4, float8 float Single/double precision
bool bool Boolean
text, varchar, char str String types
timestamp, timestamptz datetime With/without timezone
date date Date only
time, timetz time Time with/without timezone
interval timedelta Time intervals
numeric Decimal Arbitrary precision
uuid UUID UUID type
bytea bytes Binary data
json, jsonb dict/list Auto-parsed JSON
inet, cidr str Network addresses
money Decimal Currency
bit, varbit str Bit strings
int[], text[], bool[], float[] list Array types

JSONB

JSONB fields are auto-parsed by pg.zig — no manual json.loads():

class Config(Model):
    class Meta:
        table = "configs"
    id: int = Field(primary_key=True, auto=True)
    settings: dict[str, str] = Field(default_factory=dict)  # JSONB column

# Insert
config = Config(settings={"theme": "dark", "lang": "en"})
await config.save()

# Query JSONB fields
rows = await db.query(
    "SELECT * FROM configs WHERE settings->>'theme' = $1", "dark"
)

# JSONB operators in raw SQL
await db.query(
    "SELECT * FROM configs WHERE settings ? $1", "theme"  # has key
)
await db.query(
    "SELECT * FROM configs WHERE settings @> $1::jsonb", '{"theme": "dark"}'
)

Arrays

# PostgreSQL array columns
await db.execute(
    "CREATE TABLE IF NOT EXISTS articles (id SERIAL PRIMARY KEY, tags TEXT[])"
)

# Insert with array
await db.execute(
    "INSERT INTO articles (tags) VALUES ($1)", ["python", "web", "async"]
)

# Query arrays
rows = await db.query(
    "SELECT * FROM articles WHERE $1 = ANY(tags)", "python"
)

# Array operators
await db.query(
    "SELECT * FROM articles WHERE tags @> ARRAY[$1]", "python"  # contains
)

PostgreSQL's built-in full-text search via raw SQL:

# Create search index
await db.execute("""
    ALTER TABLE articles ADD COLUMN IF NOT EXISTS
    search_vector tsvector GENERATED ALWAYS AS (
        to_tsvector('english', coalesce(title, '') || ' ' || coalesce(content, ''))
    ) STORED
""")
await db.execute(
    "CREATE INDEX IF NOT EXISTS idx_articles_search ON articles USING gin(search_vector)"
)

# Search
results = await db.query(
    "SELECT * FROM articles WHERE search_vector @@ plainto_tsquery('english', $1) "
    "ORDER BY ts_rank(search_vector, plainto_tsquery('english', $1)) DESC",
    "python async web"
)

# Headline (highlighted results)
results = await db.query(
    "SELECT *, ts_headline('english', content, plainto_tsquery('english', $1)) as headline "
    "FROM articles WHERE search_vector @@ plainto_tsquery('english', $1)",
    "python"
)

UNLOGGED Tables

HyperDjango uses UNLOGGED tables for cache, sessions, and rate limits — 2-3x faster writes:

CREATE UNLOGGED TABLE hyper_cache (
    key VARCHAR(255) PRIMARY KEY,
    value BYTEA,
    expires_at TIMESTAMPTZ
);

UNLOGGED tables skip WAL (Write-Ahead Log). Data survives normal operation but is lost on crash — acceptable for ephemeral data like sessions and cache.

LISTEN/NOTIFY

Real-time PostgreSQL notifications for the Channel layer:

from hyperdjango.channels import PgChannelLayer

layer = PgChannelLayer(db)

# Publish
await layer.publish("chat:room1", {"user": "Alice", "text": "Hello!"})

# Subscribe (in WebSocket handler)
async for message in layer.subscribe("chat:room1"):
    await ws.send_json(message)

The PgChannelLayer uses PostgreSQL's native LISTEN/NOTIFY protocol.

Custom Enum Types

Register PostgreSQL custom enums for automatic type conversion:

await db.execute("CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral')")

# Discover and register
await db.discover_enums()

# Now enum values are returned as Python strings automatically
row = await db.query_one("SELECT current_mood FROM users WHERE id = $1", 1)
# row["current_mood"] = "happy" (str, not raw bytes)

COPY Protocol

Bulk import at 536K rows/sec (43x faster than INSERT):

# COPY IN — bulk import
data = [
    (1, "Alice", "alice@example.com"),
    (2, "Bob", "bob@example.com"),
    # ... thousands of rows ...
]
await db.copy_in("users", ["id", "name", "email"], data)

Connection Pipelining

Send multiple queries in a single round-trip:

# 20 queries in 0.24ms vs 1.40ms sequential (5.74x faster)
results = await db.pipeline([
    ("SELECT * FROM users WHERE id = $1", [1]),
    ("SELECT * FROM users WHERE id = $1", [2]),
    ("SELECT COUNT(*) FROM orders", []),
])

Prepared Statement Caching

pg.zig automatically caches prepared statements. Repeated queries skip the Parse phase, reducing latency by ~33%.

# First call: Parse + Bind + Execute
user = await db.query_one("SELECT * FROM users WHERE id = $1", 1)

# Subsequent calls: Bind + Execute only (Parse cached)
user = await db.query_one("SELECT * FROM users WHERE id = $1", 2)

Statement Timeout

Prevent runaway queries:

db = Database("postgres://localhost/mydb?statement_timeout=30000")
# Queries exceeding 30 seconds are automatically cancelled

Performance

Operation pg.zig psycopg3 Speedup
SELECT by PK 21K ops/sec 10K ops/sec 2.06x
SELECT range 4.18x faster baseline 4.18x
UPDATE 1.52x faster baseline 1.52x
COPY bulk import 536K rows/sec 12K rows/sec 42.8x
Micro-bench 50 rows 69μs 25ms 365x

JSONB Lookups

PostgreSQL JSONB operators via raw SQL:

# Key exists
await db.query("SELECT * FROM configs WHERE settings ? $1", "theme")

# Key-value match
await db.query("SELECT * FROM configs WHERE settings->>'theme' = $1", "dark")

# Contains (superset check)
await db.query("SELECT * FROM configs WHERE settings @> $1::jsonb", '{"theme":"dark"}')

# Contained by (subset check)
await db.query("SELECT * FROM configs WHERE settings <@ $1::jsonb", '{"theme":"dark","lang":"en"}')

# Nested key access
await db.query("SELECT settings->'display'->>'font_size' FROM configs WHERE id = $1", 1)

# Array of keys exists (any)
await db.query("SELECT * FROM configs WHERE settings ?| ARRAY['theme', 'lang']")

# Array of keys exists (all)
await db.query("SELECT * FROM configs WHERE settings ?& ARRAY['theme', 'lang']")

Array Lookups

# Contains element
await db.query("SELECT * FROM articles WHERE $1 = ANY(tags)", "python")

# Array contains (superset)
await db.query("SELECT * FROM articles WHERE tags @> ARRAY[$1, $2]", "python", "web")

# Array overlap (any shared elements)
await db.query("SELECT * FROM articles WHERE tags && ARRAY[$1, $2]", "python", "rust")

# Array length
await db.query("SELECT * FROM articles WHERE array_length(tags, 1) > $1", 3)

# Unnest (expand array to rows)
await db.query("SELECT DISTINCT unnest(tags) as tag FROM articles ORDER BY tag")

Range Types

# Integer range
await db.execute("CREATE TABLE events (id SERIAL PRIMARY KEY, during INT4RANGE)")
await db.execute("INSERT INTO events (during) VALUES ('[1, 10)')")

# Contains value
await db.query("SELECT * FROM events WHERE during @> $1", 5)

# Date range
await db.execute("CREATE TABLE bookings (id SERIAL PRIMARY KEY, dates DATERANGE)")
await db.query("SELECT * FROM bookings WHERE dates && daterange($1, $2)", start, end)