Built-in plugins

Every part of codegen_database’s dimension pipeline is a plugin. This page documents each built-in plugin: what it does, what context keys it reads and writes, and how to configure it.

For the plugin architecture itself (dependency declarations, topological sort, singletons, writing custom plugins), see Plugin architecture.

SerialPKPlugin

Adds an auto-incrementing integer primary key column.

Produces: pk_columns

Singleton group: __pk__

Parameters:

column_name

Name for the PK column (default "id").

Example:

from codegen_database.plugins.pk import SerialPKPlugin

# Default: adds "id SERIAL PRIMARY KEY"
SerialPKPlugin()

# Custom column name
SerialPKPlugin(column_name="user_id")

UUIDV4PKPlugin

Adds a UUIDv4 primary key column using PostgreSQL’s gen_random_uuid() as the server default.

Produces: pk_columns

Singleton group: __pk__

Parameters:

column_name

Name for the PK column (default "id").

Example:

from codegen_database.plugins.pk import UUIDV4PKPlugin

# Default: adds "id UUID PRIMARY KEY DEFAULT gen_random_uuid()"
UUIDV4PKPlugin()

# Custom column name
UUIDV4PKPlugin(column_name="user_id")

UUIDV7PKPlugin

Adds a UUIDv7 primary key column using PostgreSQL 18’s uuidv7() as the server default. UUIDv7 values are time-ordered, making them friendlier to B-tree indexes than random UUIDv4 values.

Requires PostgreSQL 18 or later (declared via @requires(MinPGVersion(18))). Use check_pg_version() to validate the server version before applying DDL.

Produces: pk_columns

Requires: MinPGVersion(18)

Singleton group: __pk__

Parameters:

column_name

Name for the PK column (default "id").

Example:

from codegen_database.plugins.pk import UUIDV7PKPlugin

# Default: adds "id UUID PRIMARY KEY DEFAULT uuidv7()"
UUIDV7PKPlugin()

# Custom column name
UUIDV7PKPlugin(column_name="ticket_id")

To validate the server version at runtime:

from codegen_database.plugin import check_pg_version

with engine.connect() as conn:
    major = conn.dialect.server_version_info[0]
    check_pg_version(major, factory.ctx.plugins)

construct_column_name_plugin

Publishes a column name string to the factory context under a caller-chosen ctx key. Built-in factories use it to register their timestamp columns ("created_at_column", "updated_at_column") so that other plugins can look them up without hard-coding names.

Produces: the ctx key passed as the first argument.

Example:

from codegen_database.plugins.column_name import (
    construct_column_name_plugin,
)

construct_column_name_plugin("created_at_column", "created_at")

SimpleTablePlugin

Creates a single backing table by combining the PK columns and schema items.

Produces: "primary" (via table_key), "__root__"

Requires: "pk_columns"

Singleton group: __table__

Parameters:

table_key

Context key to store the table under (default "primary").

Example:

from sqlalchemy import Column, MetaData, String

from codegen_database.factory import CodegenDatabaseSimple

metadata = MetaData()

CodegenDatabaseSimple(
    "users", "public", metadata,
    schema_items=[
        Column("name", String, nullable=False),
        Column("email", String),
    ],
)

This creates public.users with columns id, name, email.

RawTableProtectionPlugin

Prevents direct DML on raw backing tables by installing BEFORE triggers that raise an exception when called outside a trigger context. Included automatically in each factory’s _INTERNAL_PLUGINS.

Requires: the table keys passed to its constructor.

TableCheckPlugin

Resolves CodegenDatabaseCheck items into real CHECK constraints on the backing table.

Requires: "__root__"

Example:

from codegen_database.check import CodegenDatabaseCheck
from codegen_database.factory import CodegenDatabaseSimple

products = CodegenDatabaseSimple(
    "products", "public", metadata,
    schema_items=[
        Column("price", Integer),
        CodegenDatabaseCheck(
            "{price} > 0", name="positive_price"
        ),
    ],
)

TriggerCheckPlugin

Resolves CodegenDatabaseCheck items into trigger- based enforcement (RAISE EXCEPTION in INSTEAD OF triggers). Used with EAV dimensions where table-level checks cannot reference the pivot view.

Parameters:

table_key

Which view’s triggers to add checks to (default varies by dimension type).

TableIndexPlugin

Resolves CodegenDatabaseIndex items into real sqlalchemy.Index objects on the backing table. Both simple column references ("{col}") and functional expressions ("lower({col})") are supported. Extra keyword arguments on the CodegenDatabaseIndex are passed through to the underlying sqlalchemy.Index (e.g. postgresql_using, postgresql_where).

Requires: "primary" (via table_key)

Parameters:

table_key

Context key for the target table (default "primary"). Append-only dimensions use "attributes".

Example:

from sqlalchemy import Column, Integer, String

from codegen_database.factory import CodegenDatabaseSimple
from codegen_database.index import CodegenDatabaseIndex

products = CodegenDatabaseSimple(
    "products", "public", metadata,
    schema_items=[
        Column("name", String, nullable=False),
        Column("price", Integer, nullable=False),
        CodegenDatabaseIndex("idx_products_name", "{name}"),
        CodegenDatabaseIndex(
            "idx_products_price",
            "{price}",
            unique=True,
        ),
        CodegenDatabaseIndex(
            "idx_products_lower_name",
            "lower({name})",
            postgresql_using="btree",
        ),
    ],
)

TableFKPlugin

Resolves inline CodegenDatabaseForeignKey markers on Column constructors into ForeignKeyConstraint objects on the backing table.

The dimension registry is populated automatically when factories run — each factory registers its FK-targetable table (the root table for append-only dimensions, the primary table for simple dimensions).

Requires: "primary" (via table_key)

Parameters:

table_key

Context key for the target table (default "primary"). Append-only dimensions use "attributes".

Example (dimension reference):

from sqlalchemy import Column, Integer, String

from codegen_database.factory import CodegenDatabaseSimple
from codegen_database.fk import CodegenDatabaseForeignKey

customers = CodegenDatabaseSimple(
    "customers", "public", metadata,
    schema_items=[
        Column("name", String, nullable=False),
    ],
)

orders = CodegenDatabaseSimple(
    "orders", "public", metadata,
    schema_items=[
        Column(
            "customer_id", Integer,
            CodegenDatabaseForeignKey("customers.id", ondelete="CASCADE"),
            nullable=False,
        ),
        Column("total", Integer, nullable=False),
    ],
)

"customers.id" is resolved to the physical table via the dimension registry. If customers is append-only, this resolves to the root table automatically.

Example (raw three-part reference):

Column(
    "org_id", Integer,
    CodegenDatabaseForeignKey("public.organizations.id"),
)

Use a three-part "schema.table.column" reference for tables outside codegen_database or when you want full control over the FK target.

See Constraints and indices for a walkthrough of the generated SQL.

AppendOnlyTablePlugin

Creates the root table and attributes table for an append-only (SCD Type 2) dimension.

Produces: "root_table", "attributes"

Requires: "pk_columns"

Singleton group: __table__

AppendOnlyViewPlugin

Creates a join view that presents the current state by joining the root table to the latest attributes row.

Produces: "primary"

Requires: "root_table", "attributes"

AppendOnlyTriggerPlugin

Registers INSTEAD OF triggers that insert new attribute rows on update (preserving history) and handle deletes.

Requires: "root_table", "attributes", "api" (optional)

Example:

from sqlalchemy import Column, ForeignKey, String

from codegen_database.factory import CodegenDatabaseAppendOnly

students = CodegenDatabaseAppendOnly(
    "students", "private", metadata,
    schema_items=[
        Column("name", String),
        Column(
            "user_id",
            ForeignKey("public.users.id"),
        ),
    ],
)

This creates private.students (root), private.students_log (attributes), and a join view at private.students_current.

EAVTablePlugin

Creates the entity and attribute tables for an EAV dimension. Attributes are stored as typed rows (string_value, integer_value, etc.) with a check constraint enforcing exactly one non-null value per row.

Produces: "entity", "attribute", "eav_mappings"

Requires: "pk_columns"

Singleton group: __table__

EAVViewPlugin

Creates a pivot view that reconstructs the familiar columnar layout from the EAV rows.

Produces: "primary"

Requires: "entity", "attribute", "eav_mappings"

EAVTriggerPlugin

Registers INSTEAD OF triggers that decompose columnar inserts/ updates into individual EAV attribute rows.

Requires: "entity", "attribute", "eav_mappings", "api" (optional)

Example:

from sqlalchemy import Column, Float, Integer, String

from codegen_database.check import CodegenDatabaseCheck
from codegen_database.factory import CodegenDatabaseEAV

products = CodegenDatabaseEAV(
    "products", "private", metadata,
    schema_items=[
        Column("color", String),
        Column("weight", Float),
        Column("price", Integer),
        CodegenDatabaseCheck(
            "{price} > 0", name="positive_price"
        ),
    ],
)

This creates private.products_entity, private.products_attribute, and a pivot view. Check constraints are enforced in the INSTEAD OF triggers.

SoftDeletePlugin

Converts physical DELETE operations into timestamp-based soft-deletes by intercepting the INSTEAD OF DELETE trigger on the factory’s dimension view. Active rows are filtered from the view via WHERE deleted_at IS NULL.

Works with Simple, AppendOnly, and EAV dimensions. The deleted_at column is injected automatically onto the backing table by the factory, so it does not need to be declared in schema_items (for Simple and AppendOnly you may declare it explicitly as a nullable DateTime if you want it on the model; for EAV you must not, since user columns become attributes – the flag lives on the entity table instead).

Produces: deleted_at_column, delete_trigger_override

Requires: primary, raw_table (Simple), attributes (AppendOnly), or entity (EAV)

Parameters:

column_name

Name of the soft-delete timestamp column (default "deleted_at").

table_key

Backing table the soft-delete column lives on (default "raw_table" for Simple; use "attributes" for AppendOnly and "entity" for EAV).

Example:

from codegen_database.plugins.soft_delete import SoftDeletePlugin

# Simple
extra_plugins=[SoftDeletePlugin()]

# AppendOnly
extra_plugins=[SoftDeletePlugin(table_key="attributes")]

# EAV
extra_plugins=[SoftDeletePlugin(table_key="entity")]

TemporalPlugin

Adds a GIST exclusion constraint that prevents overlapping validity ranges for the same entity. Works with valid_from / valid_to columns to enforce tstzrange(valid_from, COALESCE(valid_to, 'infinity'), '[)') non-overlap.

Requires the btree_gist PostgreSQL extension. TemporalPlugin automatically registers btree_gist in metadata so Alembic creates it when absent — no manual registration is needed.

Produces: temporal_columns

Requires: raw_table (or custom table_key)

Parameters:

subject_columns

Columns that identify a unique entity across multiple temporal versions (e.g. ["order_id"]). Required.

valid_from_column

Range-start column (default "valid_from"). Must be a non-nullable DateTime.

valid_to_column

Range-end column (default "valid_to"). Must be a nullable DateTime.

table_key

Key in ctx for the backing table (default "raw_table").

Example:

from sqlalchemy import Column, DateTime
from codegen_database.plugins.temporal import TemporalPlugin

schema_items=[
    Column("order_id", Integer, nullable=False),
    Column(
        "valid_from",
        DateTime(timezone=True),
        nullable=False,
        server_default="now()",
    ),
    Column("valid_to", DateTime(timezone=True), nullable=True),
],
extra_plugins=[
    TemporalPlugin(subject_columns=["order_id"]),
]

Use construct_temporal_as_of_query() to build point-in-time queries against a temporal table.

LedgerSnapshotPlugin

Maintains a denormalized aggregate snapshot of ledger event balances. Registers a AFTER INSERT trigger on the ledger raw table that upserts into a companion snapshot table, and an incremental refresh function built on top of IncrementalRefreshPlugin.

Produces: snapshot_table

Requires: raw_table, ledger_view

Parameters:

dimension_columns

Columns that form the grouping key for aggregation (e.g. ["customer_id", "product_id"]).

snapshot_table_suffix

Suffix appended to the tablename to form the snapshot table name (default "_snapshot").

schedule

Optional pg_cron expression for periodic refresh. Requires PGCronExtension to be registered.

SearchVectorPlugin

Maintains a tsvector column on the backing table via a BEFORE INSERT OR UPDATE trigger. Source columns are concatenated and fed through to_tsvector(config, ...) so the column stays in sync automatically.

PostgreSQL’s full-text search primitives (tsvector, to_tsvector, tsquery, @@) are built into the database engine — no extension is required for basic usage. If the text search configuration depends on a PostgreSQL extension (e.g. unaccent), register that extension separately via register_pg_extension().

Produces: search_vector_column

Requires: raw_table (or custom table_key)

Parameters:

source_columns

One or more column names whose text content feeds the search vector. At least one required. NULL values are coalesced to empty strings.

vector_column

Name of the tsvector column to maintain (default "search_vector"). Must exist in schema_items.

ts_config

PostgreSQL text search configuration (default "english").

table_key

Key in ctx for the backing table (default "raw_table").

Example:

from sqlalchemy import Column, String
from sqlalchemy.dialects.postgresql import TSVECTOR
from codegen_database.plugins.search_vector import SearchVectorPlugin

schema_items=[
    Column("name", String, nullable=False),
    Column("description", String),
    Column("search_vector", TSVECTOR, nullable=True),
],
extra_plugins=[
    SearchVectorPlugin(
        source_columns=["name", "description"],
    ),
]

Combine with a GIN index for fast full-text queries:

CREATE INDEX ON products_raw USING GIN (search_vector);

Then query with:

SELECT * FROM app.products
WHERE search_vector @@ plainto_tsquery('english', 'widget');

CodegenDatabaseAutogeneratedIdentifierColumn

A text column that codegen_database fills with a sequential, human-readable identifier — the 100001 in “Order #100001” — via a BEFORE INSERT trigger. When the column is left NULL the trigger draws the next number from the pointer table; an explicit value passes through untouched, so backfills and imports still work.

Unlike most behaviour, this is declared as a column, not a hand-placed plugin: CodegenDatabaseAutogeneratedIdentifierColumn returns an ordinary TEXT NOT NULL Column carrying its configuration in Column.info. The dimension factories always run the scanning AutoIdentifierPlugin, which finds these columns and wires the trigger — so there is nothing to add to extra_plugins / __plugins__.

Every autogenerated identifier in the database draws from one table — autogenerated_identifier_pointers — in the codegen_database utility schema (CodegenDatabaseConfig.utility_schema, default codegen_database), keyed by identifier name (the “enum”). It is not per dimension schema. Each insert upserts a single row under a row lock, so concurrent inserts get distinct, gapless numbers.

The column is NOT NULL: the BEFORE INSERT trigger fills it before the constraint is checked, so it reads as non-null on every response model while still being optional on create.

Parameters:

name

The SQL column name. Omit it in a declarative model (the attribute name is used); pass it positionally when building a schema_items list.

key

Identifier kind — the name key into the database-wide autogenerated_identifier_pointers table. Reuse a key to share one running counter across columns (in any schema); use distinct keys for independent sequences.

padding

Zero-pad the number to this many digits (6"000123"). 0 (the default) emits the number unpadded. Numbers wider than padding are never truncated.

start

First number issued for key (default 1). Use e.g. 100000 so the first identifier reads "100000".

Example:

from sqlalchemy import Column, String
from codegen_database.plugins.autogenerated_identifier import (
    CodegenDatabaseAutogeneratedIdentifierColumn,
)

schema_items=[
    Column("customer", String, nullable=False),
    CodegenDatabaseAutogeneratedIdentifierColumn(
        "order_no", key="order", padding=6, start=100000,
    ),
]

Or, in a declarative model, the attribute name supplies the column name:

class Orders(Base):
    customer = Column(String, nullable=False)
    order_no = CodegenDatabaseAutogeneratedIdentifierColumn(
        key="order", padding=6, start=100000,
    )

The first insert yields order_no = '100000', the next '100001', and so on. The column is populated server-side, so it should not be a required input on create forms: the companion ingot.identifiers.autogenerated_identifier_field helper builds the matching create-model field as optional and read-only for the front end.

Plugin execution order

The factory topologically sorts plugins by their produces() / requires() declarations. A typical simple dimension pipeline runs:

SerialPKPlugin              -> pk_columns
SimpleTablePlugin           -> primary, __root__
TableCheckPlugin            (reads __root__)
TableIndexPlugin            (reads primary)
TableFKPlugin               (reads primary)
RawTableProtectionPlugin    (reads primary)

All context key names are overridable via constructor arguments, so two independent pipelines can coexist in one factory.