Skip to content

Triggers

PostgreSQL triggers are special functions that automatically execute in response to database events.

PostgreSQL Specification

CREATE TRIGGER Syntax

CREATE [ OR REPLACE ] [ CONSTRAINT ] TRIGGER name { BEFORE | AFTER | INSTEAD OF } { event [ OR ... ] }
    ON table_name
    [ FROM referenced_table_name ]
    [ NOT DEFERRABLE | [ DEFERRABLE ] [ INITIALLY IMMEDIATE | INITIALLY DEFERRED ] ]
    [ REFERENCING { { OLD | NEW } TABLE [ AS ] transition_relation_name } [ ... ] ]
    [ FOR [ EACH ] { ROW | STATEMENT } ]
    [ WHEN ( condition ) ]
    EXECUTE { FUNCTION | PROCEDURE } function_name ( arguments )

Reference: PostgreSQL 17 CREATE TRIGGER

pgdelta Support

✅ Currently Supported (via pg_get_triggerdef)

  • CREATE TRIGGER with complete definition
  • DROP TRIGGER
  • All trigger types (BEFORE, AFTER, INSTEAD OF)
  • All trigger events (INSERT, UPDATE, DELETE, TRUNCATE)
  • Row-level and statement-level triggers
  • CONSTRAINT triggers
  • Transition tables (REFERENCING clause)
  • WHEN conditions
  • All trigger features
CREATE TRIGGER "update_user_modified_time"
BEFORE UPDATE ON "public"."users"
FOR EACH ROW
EXECUTE FUNCTION update_modified_time();

❌ Not Yet Supported

  • ALTER TRIGGER operations (planned)
  • ENABLE/DISABLE TRIGGER (planned)

Usage Examples

Basic Update Trigger

target_sql = """
CREATE FUNCTION update_modified_time()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
    NEW.modified_at = NOW();
    RETURN NEW;
END;
$$;

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email TEXT,
    created_at TIMESTAMP DEFAULT NOW(),
    modified_at TIMESTAMP
);

CREATE TRIGGER update_user_modified_time
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_modified_time();
"""

Audit Trigger

target_sql = """
CREATE TABLE audit_log (
    id SERIAL PRIMARY KEY,
    table_name TEXT,
    operation TEXT,
    old_values JSON,
    new_values JSON,
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE FUNCTION audit_changes()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
    INSERT INTO audit_log (table_name, operation, old_values, new_values)
    VALUES (
        TG_TABLE_NAME,
        TG_OP,
        CASE WHEN TG_OP = 'DELETE' THEN row_to_json(OLD) ELSE NULL END,
        CASE WHEN TG_OP = 'INSERT' THEN row_to_json(NEW) ELSE NULL END
    );
    RETURN COALESCE(NEW, OLD);
END;
$$;

CREATE TRIGGER audit_users_changes
AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW
EXECUTE FUNCTION audit_changes();
"""

Implementation Details

Trigger Models

@dataclass(frozen=True)
class CreateTrigger:
    stable_id: str              # Format: "t:schema.trigger_name"
    namespace: str              # Schema name
    tgname: str                 # Trigger name
    table_name: str             # Table name
    trigger_definition: str     # Complete trigger definition

SQL Generation

def generate_create_trigger_sql(change: CreateTrigger) -> str:
    """Generate CREATE TRIGGER SQL."""
    return change.trigger_definition + ";"

Trigger Types

Row-Level Triggers

-- Execute for each affected row
CREATE TRIGGER row_trigger
BEFORE INSERT ON table_name
FOR EACH ROW
EXECUTE FUNCTION trigger_function();

Statement-Level Triggers

-- Execute once per statement
CREATE TRIGGER statement_trigger
AFTER DELETE ON table_name
FOR EACH STATEMENT
EXECUTE FUNCTION trigger_function();

INSTEAD OF Triggers

-- For views (make them updatable)
CREATE TRIGGER view_insert_trigger
INSTEAD OF INSERT ON view_name
FOR EACH ROW
EXECUTE FUNCTION handle_view_insert();

Future Enhancements

Planned Features (v0.2.0)

  • CONSTRAINT triggers
  • Transition tables support
  • Better trigger dependency tracking

Best Practices

Trigger Naming

# Good trigger names
good_names = [
    "update_modified_time",       # Action-based
    "audit_changes",              # Purpose-based
    "validate_email_format",      # Validation triggers
    "sync_derived_fields",        # Synchronization triggers
]

# Naming patterns
patterns = {
    "before_triggers": "validate_*, check_*, update_*",
    "after_triggers": "audit_*, log_*, sync_*",
    "instead_of": "handle_*, process_*"
}

Performance Considerations

# Trigger performance impact
performance_impact = {
    "row_triggers": "Execute for each affected row",
    "statement_triggers": "Execute once per statement",
    "complex_logic": "Can significantly impact INSERT/UPDATE performance",
    "cascading_triggers": "Avoid triggers that fire other triggers"
}

# Optimization tips
optimization_tips = {
    "use_when_clause": "Limit trigger execution with WHEN conditions",
    "minimize_work": "Keep trigger functions lightweight",
    "avoid_exceptions": "Exceptions in triggers are expensive",
    "consider_alternatives": "Sometimes application logic is better"
}