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 :doc:`plugins`. SerialPKPlugin -------------- .. module:: codegen_database.plugins.pk :no-index: 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:** .. code-block:: python 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:** .. code-block:: python 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 :func:`~codegen_database.plugin.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:** .. code-block:: python 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: .. code-block:: python 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 ---------------------------- .. module:: codegen_database.plugins.column_name :no-index: 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:** .. code-block:: python from codegen_database.plugins.column_name import ( construct_column_name_plugin, ) construct_column_name_plugin("created_at_column", "created_at") SimpleTablePlugin ----------------- .. module:: codegen_database.factory.dimension.simple :no-index: 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:** .. code-block:: python 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 ---------------- .. module:: codegen_database.plugins.check :no-index: Resolves :class:`~codegen_database.check.CodegenDatabaseCheck` items into real ``CHECK`` constraints on the backing table. **Requires:** ``"__root__"`` **Example:** .. code-block:: python 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 :class:`~codegen_database.check.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 ---------------- .. module:: codegen_database.plugins.index :no-index: Resolves :class:`~codegen_database.index.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:** .. code-block:: python 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 ------------- .. module:: codegen_database.plugins.fk :no-index: Resolves inline :class:`~codegen_database.fk.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):** .. code-block:: python 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):** .. code-block:: python 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 :doc:`constraints_and_indices` for a walkthrough of the generated SQL. AppendOnlyTablePlugin --------------------- .. module:: codegen_database.factory.dimension.append_only :no-index: 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:** .. code-block:: python 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 -------------- .. module:: codegen_database.factory.dimension.eav :no-index: 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:** .. code-block:: python 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 ---------------- .. module:: codegen_database.plugins.soft_delete :no-index: 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:** .. code-block:: python 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 -------------- .. module:: codegen_database.plugins.temporal :no-index: 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. :class:`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:** .. code-block:: python 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 :func:`~codegen_database.plugins.temporal.construct_temporal_as_of_query` to build point-in-time queries against a temporal table. LedgerSnapshotPlugin -------------------- .. module:: codegen_database.plugins.snapshot :no-index: 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 :class:`~codegen_database.ext.refresh.plugin.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 :class:`~codegen_database.ext.pg_cron.PGCronExtension` to be registered. SearchVectorPlugin ------------------ .. module:: codegen_database.plugins.search_vector :no-index: 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 :func:`~codegen_database.pg_extension.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:** .. code-block:: python 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: .. code-block:: sql CREATE INDEX ON products_raw USING GIN (search_vector); Then query with: .. code-block:: sql SELECT * FROM app.products WHERE search_vector @@ plainto_tsquery('english', 'widget'); CodegenDatabaseAutogeneratedIdentifierColumn ------------------------------------ .. module:: codegen_database.plugins.autogenerated_identifier :no-index: 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`` :class:`~sqlalchemy.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:** .. code-block:: python 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: .. code-block:: python 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 :func:`~codegen_database.plugin.produces` / :func:`~codegen_database.plugin.requires` declarations. A typical simple dimension pipeline runs: .. code-block:: text 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.