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_nameName 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_nameName 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_nameName 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_keyContext 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_keyWhich 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_keyContext 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_keyContext 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_nameName of the soft-delete timestamp column (default
"deleted_at").table_keyBacking 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_columnsColumns that identify a unique entity across multiple temporal versions (e.g.
["order_id"]). Required.valid_from_columnRange-start column (default
"valid_from"). Must be a non-nullableDateTime.valid_to_columnRange-end column (default
"valid_to"). Must be a nullableDateTime.table_keyKey in
ctxfor 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_columnsColumns that form the grouping key for aggregation (e.g.
["customer_id", "product_id"]).snapshot_table_suffixSuffix appended to the tablename to form the snapshot table name (default
"_snapshot").scheduleOptional pg_cron expression for periodic refresh. Requires
PGCronExtensionto 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_columnsOne or more column names whose text content feeds the search vector. At least one required.
NULLvalues are coalesced to empty strings.vector_columnName of the
tsvectorcolumn to maintain (default"search_vector"). Must exist inschema_items.ts_configPostgreSQL text search configuration (default
"english").table_keyKey in
ctxfor 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:
nameThe SQL column name. Omit it in a declarative model (the attribute name is used); pass it positionally when building a
schema_itemslist.keyIdentifier kind — the
namekey into the database-wideautogenerated_identifier_pointerstable. Reuse a key to share one running counter across columns (in any schema); use distinct keys for independent sequences.paddingZero-pad the number to this many digits (
6→"000123").0(the default) emits the number unpadded. Numbers wider than padding are never truncated.startFirst number issued for key (default
1). Use e.g.100000so 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.