Constraints¶
PostgreSQL constraints ensure data integrity by restricting the values that can be stored in tables.
PostgreSQL Specification¶
Constraint Types¶
PRIMARY KEY¶
FOREIGN KEY¶
ALTER TABLE table_name ADD CONSTRAINT constraint_name
FOREIGN KEY (column_name [, ...]) REFERENCES referenced_table (column_name [, ...])
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ]
[ ON DELETE action ] [ ON UPDATE action ]
[ [ NOT ] DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ];
UNIQUE¶
CHECK¶
EXCLUDE¶
ALTER TABLE table_name ADD CONSTRAINT constraint_name
EXCLUDE [ USING method ] ( element WITH operator [, ...] )
[ WHERE predicate ];
References: - PostgreSQL 17 ALTER TABLE - PostgreSQL 17 CREATE TABLE
pgdelta Support¶
✅ Currently Supported¶
PRIMARY KEY Constraints¶
- Single column primary keys
- Multi-column composite primary keys
- Automatic index creation
FOREIGN KEY Constraints¶
- Single column foreign keys
- Multi-column foreign keys
- ON DELETE/UPDATE actions (CASCADE, RESTRICT, SET NULL, SET DEFAULT)
- Constraint deferrability options
ALTER TABLE "public"."orders" ADD CONSTRAINT "orders_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "public"."users" ("id") ON DELETE CASCADE;
UNIQUE Constraints¶
- Single column unique constraints
- Multi-column unique constraints
- Partial unique constraints with WHERE clause
CHECK Constraints¶
- Column-level check constraints
- Table-level check constraints
- Complex expressions
EXCLUSION Constraints¶
- Basic exclusion constraints
- Custom operator specifications
- Spatial exclusion constraints
ALTER TABLE "public"."reservations" ADD CONSTRAINT "reservations_overlap_excl"
EXCLUDE USING gist (room_id WITH =, during WITH &&);
DROP CONSTRAINT¶
- Constraint deletion
- Cascade behavior through dependency resolution
❌ Not Yet Supported¶
ALTER CONSTRAINT¶
- Constraint modifications
- Deferrability changes
- Constraint validation
Advanced Features¶
- MATCH FULL/PARTIAL for foreign keys
- Complex exclusion constraint expressions
🚫 Intentionally Not Supported¶
Environment-Specific Features¶
- Constraint timing (some aspects are runtime)
- Performance-related constraint options
Usage Examples¶
Primary Key Constraints¶
# Single column primary key
target_sql = """
CREATE TABLE users (
id SERIAL,
email TEXT
);
ALTER TABLE users ADD CONSTRAINT users_pkey PRIMARY KEY (id);
"""
# Multi-column primary key
target_sql = """
CREATE TABLE order_items (
order_id INTEGER,
product_id INTEGER,
quantity INTEGER
);
ALTER TABLE order_items ADD CONSTRAINT order_items_pkey
PRIMARY KEY (order_id, product_id);
"""
Foreign Key Constraints¶
# Basic foreign key
target_sql = """
CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER,
total DECIMAL(10,2)
);
ALTER TABLE orders ADD CONSTRAINT orders_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users (id);
"""
# Foreign key with cascade
target_sql = """
ALTER TABLE orders ADD CONSTRAINT orders_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE;
"""
Unique Constraints¶
# Single column unique
target_sql = """
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT
);
ALTER TABLE users ADD CONSTRAINT users_email_key UNIQUE (email);
"""
# Multi-column unique
target_sql = """
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT,
category TEXT
);
ALTER TABLE products ADD CONSTRAINT products_name_category_key
UNIQUE (name, category);
"""
Check Constraints¶
# Simple check constraint
target_sql = """
CREATE TABLE users (
id SERIAL PRIMARY KEY,
age INTEGER
);
ALTER TABLE users ADD CONSTRAINT users_age_check CHECK (age >= 0);
"""
# Complex check constraint
target_sql = """
CREATE TABLE products (
id SERIAL PRIMARY KEY,
price DECIMAL(10,2),
discount_price DECIMAL(10,2)
);
ALTER TABLE products ADD CONSTRAINT products_price_check
CHECK (price >= 0 AND discount_price >= 0 AND discount_price <= price);
"""
Implementation Details¶
Constraint Models¶
@dataclass(frozen=True)
class CreateConstraint:
stable_id: str # Format: "c:schema.constraint_name"
constraint: PgConstraint # Constraint definition
@dataclass(frozen=True)
class PgConstraint:
conname: str # Constraint name
contype: str # Constraint type (p, f, u, c, x)
table_name: str # Table name
schema_name: str # Schema name
constraint_definition: str # Complete constraint definition
@property
def constraint_type_name(self) -> str:
type_map = {
'p': 'PRIMARY KEY',
'f': 'FOREIGN KEY',
'u': 'UNIQUE',
'c': 'CHECK',
'x': 'EXCLUDE'
}
return type_map.get(self.contype, 'UNKNOWN')
SQL Generation¶
def generate_create_constraint_sql(change: CreateConstraint) -> str:
"""Generate ADD CONSTRAINT SQL."""
constraint = change.constraint
quoted_schema = f'"{constraint.schema_name}"'
quoted_table = f'"{constraint.table_name}"'
quoted_constraint = f'"{constraint.conname}"'
return (
f"ALTER TABLE {quoted_schema}.{quoted_table} "
f"ADD CONSTRAINT {quoted_constraint} "
f"{constraint.constraint_definition};"
)
Constraint Types Detail¶
Primary Key Constraints¶
-- Single column
ALTER TABLE users ADD CONSTRAINT users_pkey PRIMARY KEY (id);
-- Multi-column
ALTER TABLE order_items ADD CONSTRAINT order_items_pkey
PRIMARY KEY (order_id, product_id);
Characteristics: - Automatically creates unique B-tree index - Implies NOT NULL on all columns - Only one per table - Referenced by foreign keys
Foreign Key Constraints¶
-- Basic foreign key
ALTER TABLE orders ADD CONSTRAINT orders_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users (id);
-- With actions
ALTER TABLE orders ADD CONSTRAINT orders_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users (id)
ON DELETE CASCADE ON UPDATE RESTRICT;
-- Multi-column
ALTER TABLE order_items ADD CONSTRAINT order_items_product_fkey
FOREIGN KEY (product_id, variant_id) REFERENCES products (id, variant_id);
Actions:
- CASCADE
: Delete/update dependent rows
- RESTRICT
: Prevent delete/update if dependents exist
- SET NULL
: Set foreign key to NULL
- SET DEFAULT
: Set foreign key to default value
- NO ACTION
: Same as RESTRICT (default)
Unique Constraints¶
-- Single column
ALTER TABLE users ADD CONSTRAINT users_email_key UNIQUE (email);
-- Multi-column
ALTER TABLE products ADD CONSTRAINT products_name_category_key
UNIQUE (name, category);
-- Partial unique (via partial unique index)
CREATE UNIQUE INDEX users_active_email_key ON users (email) WHERE is_active = true;
Characteristics: - Automatically creates unique B-tree index - Allows multiple NULL values (unless NOT NULL constraint) - Can be referenced by foreign keys
Check Constraints¶
-- Simple check
ALTER TABLE users ADD CONSTRAINT users_age_check CHECK (age >= 0);
-- Complex check
ALTER TABLE products ADD CONSTRAINT products_price_check
CHECK (price >= 0 AND discount_price >= 0 AND discount_price <= price);
-- Check with function
ALTER TABLE users ADD CONSTRAINT users_email_check
CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');
Characteristics: - Evaluated on INSERT/UPDATE - Can reference multiple columns - Can use functions and operators - NOT NULL is a special case of CHECK
Exclusion Constraints¶
-- Basic exclusion
ALTER TABLE reservations ADD CONSTRAINT reservations_overlap_excl
EXCLUDE USING gist (room_id WITH =, during WITH &&);
-- With WHERE clause
ALTER TABLE reservations ADD CONSTRAINT reservations_active_overlap_excl
EXCLUDE USING gist (room_id WITH =, during WITH &&) WHERE (is_active = true);
Characteristics: - Prevents overlapping values - Uses GiST or SP-GiST indexes - Common for time ranges and geometric data - Can combine multiple operators
Testing¶
Unit Tests¶
def test_create_primary_key():
"""Test primary key constraint creation."""
constraint = PgConstraint(
conname="users_pkey",
contype="p",
table_name="users",
schema_name="public",
constraint_definition="PRIMARY KEY (id)"
)
change = CreateConstraint(
stable_id="c:public.users_pkey",
constraint=constraint
)
sql = generate_create_constraint_sql(change)
assert 'ALTER TABLE "public"."users"' in sql
assert 'ADD CONSTRAINT "users_pkey"' in sql
assert 'PRIMARY KEY (id)' in sql
Integration Tests¶
def test_foreign_key_roundtrip(postgres_session):
"""Test foreign key constraint roundtrip fidelity."""
# Create tables and foreign key
postgres_session.execute(text("""
CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER,
total DECIMAL(10,2)
);
ALTER TABLE orders ADD CONSTRAINT orders_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE;
"""))
postgres_session.commit()
# Extract catalog
catalog = extract_catalog(postgres_session)
# Find constraint
constraint = next(c for c in catalog.constraints
if c.conname == "orders_user_id_fkey")
assert constraint.contype == "f"
assert "ON DELETE CASCADE" in constraint.constraint_definition
Error Handling¶
Common Errors¶
# Constraint violations
try:
# Foreign key reference to non-existent table
sql = """
ALTER TABLE orders ADD CONSTRAINT orders_user_id_fkey
FOREIGN KEY (user_id) REFERENCES nonexistent_table (id);
"""
# pgdelta validates references during extraction
except Exception as e:
pass
# Name conflicts
try:
# Duplicate constraint names
sql = """
ALTER TABLE users ADD CONSTRAINT users_check CHECK (age >= 0);
ALTER TABLE users ADD CONSTRAINT users_check CHECK (email IS NOT NULL);
"""
# pgdelta tracks constraint names
except Exception as e:
pass
Validation¶
def validate_constraint_name(name: str) -> bool:
"""Validate constraint name."""
# Must be valid identifier
if not name.replace('_', '').isalnum():
return False
# Reasonable length
if len(name) > 63: # PostgreSQL limit
return False
return True
def validate_check_expression(expression: str) -> bool:
"""Validate check constraint expression."""
# No subqueries
if "SELECT" in expression.upper():
return False
# Balanced parentheses
if expression.count("(") != expression.count(")"):
return False
return True
Future Enhancements¶
Planned Features (v0.2.0)¶
ALTER CONSTRAINT¶
-- Constraint modifications
ALTER TABLE users ALTER CONSTRAINT users_age_check NOT DEFERRABLE;
ALTER TABLE users RENAME CONSTRAINT old_name TO new_name;
VALIDATE CONSTRAINT¶
Enhanced Foreign Keys¶
-- MATCH options
ALTER TABLE orders ADD CONSTRAINT orders_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users (id) MATCH FULL;
Best Practices¶
Constraint Naming¶
# Good constraint names
naming_patterns = {
"primary_key": "{table}_pkey",
"foreign_key": "{table}_{column}_fkey",
"unique": "{table}_{column}_key",
"check": "{table}_{column}_{purpose}_check",
"exclude": "{table}_{purpose}_excl"
}
# Examples
examples = {
"users_pkey": "Primary key on users table",
"orders_user_id_fkey": "Foreign key from orders.user_id to users.id",
"users_email_key": "Unique constraint on users.email",
"users_age_positive_check": "Check that age is positive",
"reservations_overlap_excl": "Exclude overlapping reservations"
}
Performance Considerations¶
# Constraint performance impact
performance_notes = {
"primary_key": "Minimal impact, creates efficient index",
"foreign_key": "Requires index on referenced columns",
"unique": "Creates index, minimal impact",
"check": "Evaluated on every INSERT/UPDATE",
"exclude": "Uses GiST index, moderate impact"
}
# Optimization tips
optimization_tips = {
"foreign_keys": "Ensure referenced columns are indexed",
"check_constraints": "Use simple expressions when possible",
"exclusion_constraints": "Consider partial constraints with WHERE",
"deferrability": "Use DEFERRABLE for complex transactions"
}
Data Integrity Strategy¶
# Constraint hierarchy
integrity_levels = {
"essential": ["NOT NULL", "PRIMARY KEY", "FOREIGN KEY"],
"important": ["UNIQUE", "CHECK (basic validations)"],
"optional": ["CHECK (complex business rules)", "EXCLUDE"]
}
# When to use each type
use_cases = {
"primary_key": "Every table needs a primary key",
"foreign_key": "Maintain referential integrity",
"unique": "Prevent duplicate values",
"check": "Enforce business rules at database level",
"exclude": "Prevent conflicting reservations/schedules"
}