API reference¶
The API is split into the three buckets described in
Module layout — declarative primitives at codegen_database,
migration glue at codegen_database.alembic, and pre-built
features at codegen_database.ext.
Declarative primitives (codegen_database.*)¶
codegen_database: configuration-driven PostgreSQL framework.
This top-level module exposes only the declarative primitives you compose to describe a schema (bases, views, columns, foreign keys, indexes, checks, types, plugin/extension base classes, config).
Three-bucket boundary:
Declarative primitives — right here at
codegen_database.Migration glue — see
codegen_database.alembic(wired into your project’senv.py).Pre-built features composed from the primitives — see
codegen_database.ext(ledgers, audit, state machines, refresh, cron, row-level security, etc.).
These are the building blocks you compose to describe a schema.
Declarative base classes for codegen_database-managed models and views.
Subclass CodegenDatabaseBase once to create a project-level base,
then define model classes by inheriting from it. The codegen_database plugin
pipeline runs automatically when each class body is executed.
Subclass CodegenDatabaseView once to create a project-level
view base, then define views by inheriting from it. The view is
registered for Alembic and the class is ORM-mapped so that
select(MyView) works.
- class CodegenDatabaseBase[source]¶
Base class for declarative codegen_database models.
Define a project-level base by subclassing. Metadata is auto-created for your model with codegen_database naming conventions if you don’t provide it:
class Base(CodegenDatabaseBase): pass # Base.metadata created automatically
Override
metadatato use custom naming conventions:class Base(CodegenDatabaseBase): metadata = MetaData(naming_convention=...)
Define tables and views in the same inheritance tree. A class with
__query__is treated as a view; without it, the codegen_database plugin pipeline runs and creates a table:class Locations(Base): __tablename__ = "locations" __table_args__ = {"schema": "public"} name = Column(String, nullable=False) class LocationStats(Base): __tablename__ = "location_stats" __table_args__ = {"schema": "public"} __query__ = select( Locations.name, func.count().label("count"), ).group_by(Locations.name)
Columns may be declared with either
Column(...)or the SQLAlchemy 2.0mapped_column(...)form (optionally annotated withMapped[X]for type checkers).mapped_columnalways requires an explicit SQL type — codegen_database does not run SA’s annotation-driven inference pass.When the class body executes the appropriate pipeline runs and the class is ORM-mapped so that
select(LocationStats)works.Base-level attributes (set on your
Basesubclass):metadata: Optional. Auto-created with codegen_database naming conventions if not provided.codegen_database_config: OptionalCodegenDatabaseConfig. If omitted, falls back tometadata.info["codegen_database_config"]so you only need to set the config once.
Per-model attributes resolved through the MRO:
__factory__: Factory class whose_INTERNAL_PLUGINSare used (defaultCodegenDatabaseSimple). Set to e.g.CodegenDatabaseAppendOnlyfor append-only semantics.__plugins__: List of plugin instances appended to the resolved plugin list.
- class CodegenDatabaseFunctionMixin[source]¶
Mark a
CodegenDatabaseBasesubclass as a PostgreSQL function.Combine with your project base to define functions alongside tables and views:
class InventoryAdjust(CodegenDatabaseFunctionMixin, Base): __table_args__ = {"schema": "private"} __funcspec__ = ledger_event_function(Inventory, adjust_event)
__funcspec__must be aCodegenDatabaseFunctionSpec(returned byledger_event_function(), chart-function builders, etc.). The schema is taken from__table_args__, not from the spec.Subclasses gain a
call()classmethod that wraps the generated SQL function in aselect(func.<schema>.<name>(...))and dispatches viadb.execute(...)– so callers never have to spell out the schema, function name, or param order:await InventoryAdjust.call( db, warehouse="w1", sku="abc", value=10, )
- classmethod call(db, **kwargs)[source]¶
Invoke the generated SQL function.
Builds
select(func.<schema>.<name>(*args))from the declared__funcspec__and dispatches it viadb.execute(...). Kwargs are keyed by the declared param names (without thep_prefix that the SQL function uses internally);enum.Enumvalues are coerced to.valueso callers can pass typed enums directly.- Parameters:
- Return type:
- Returns:
Whatever
db.execute(stmt)returns – aResultfor sync sessions, anAwaitable[Result]for async ones.- Raises:
KeyError – If a declared param has no matching kwarg.
- class CodegenDatabaseView[source]¶
Base class for declarative codegen_database views.
Define a project-level view base by subclassing and setting
metadatato the same instance used by your model base. Views and tables must share metadata so that Alembic sees everything in one place:class ViewBase(CodegenDatabaseView): metadata = Base.metadata
Then define view classes by inheriting from that base:
# Columns inferred from the query (names and best-effort types). class CustomerStats(ViewBase): __tablename__ = "customer_order_stats" __table_args__ = {"schema": "public"} __query__ = ( select( orders.c.customer_id, func.count().label("order_count"), func.sum(orders.c.total).label("order_total"), ) .group_by(orders.c.customer_id) ) # Columns declared explicitly (accurate types, self-documenting). class InvoiceStats(ViewBase): __tablename__ = "customer_invoice_stats" __table_args__ = {"schema": "public"} customer_id = Column(Integer, nullable=False) invoice_count = Column(Integer) invoiced_total = Column(Numeric(10, 2)) __query__ = ( select( invoices.c.customer_id, func.count().label("invoice_count"), func.sum(invoices.c.amount).label("invoiced_total"), ) .group_by(invoices.c.customer_id) ) # Materialized view. class ProductSummary(ViewBase): __tablename__ = "product_summary" __table_args__ = {"schema": "public"} __options__ = ViewOptions(materialized=True) __query__ = select(...)
When the class body executes the view DDL is registered on the metadata for Alembic autogeneration, and the class is ORM-mapped so that
select(CustomerStats)works.Column resolution:
If the class declares
Columnattributes they are used as-is — this is the preferred form because it gives accurate types for the ORM mapper and is self-documenting.If no columns are declared the list is derived from
__query__.selected_columns. Types are inherited from the source columns where possible (e.g. direct column references), but aggregates and expressions that lose type information are mapped asNullType. A surrogate primary key is chosen automatically: the column namedidif present, otherwise the first column.Per-view ``__options__`` (own class dict only):
Set
__options__ = ViewOptions(materialized=True)to create a PostgreSQL materialized view.
- class CodegenDatabaseViewMixin[source]¶
Mixin that marks a
CodegenDatabaseBasesubclass as a SQL view.Combine with your project base to define views in the same class hierarchy as tables:
class OrderStats(CodegenDatabaseViewMixin, Base): __tablename__ = "order_stats" __table_args__ = {"schema": "public"} __query__ = select(Orders.customer_id, func.count().label("n"))
- class ViewOptions(materialized=False)[source]¶
Options for declarative views.
Set on a
CodegenDatabaseViewMixinorCodegenDatabaseViewsubclass via__options__.- materialized¶
If
True, creates a PostgreSQL materialized view instead of a regular view.
- map_model(cls, parent_cls=None)[source]¶
Run the codegen_database plugin pipeline for cls and ORM-map it.
Called from
CodegenDatabaseBase.__init_subclass__()once per model class (those that have a__tablename__), and ad hoc for classes declared outside that hierarchy.- Parameters:
cls (
type) – The model class being defined.parent_cls (
type|None) – an optional parent class to merge with the input cls. can be helpful when the input class cannot inherit from the parent class directly. This is particularly helpful for places where the “child class” model is defined in one library and would otherwise have to subclass the codegen database class at runtime, which would cause issues when trying to use that class in a query, since the class would not be equivalent to the sqla-ified class.
- Raises:
CodegenDatabaseValidationError – On missing schema or plugin misconfiguration.
- Return type:
Global codegen_database configuration.
- class CodegenDatabaseConfig(plugins=<factory>, extensions=<factory>, auto_discover=True, utility_schema='codegen_database')[source]¶
Global plugin and extension registry.
Plugins registered here are prepended to every factory’s resolved plugin list, so they run before factory-specific plugins.
Extensions bundle plugins, metadata hooks, Alembic hooks, and CLI commands into a single unit.
Example:
config = CodegenDatabaseConfig() config.register(TimestampPlugin(), TenantPlugin()) CodegenDatabaseSimple( "users", "public", metadata, ..., config=config )
- Parameters:
plugins (
list[Plugin]) – Global plugins prepended to every factory.extensions (
list[CodegenDatabaseExtension]) – Manually registered extension instances.auto_discover (
bool) – Whether to discover extensions via entry points. Defaults toTrue.utility_schema (
str) – PostgreSQL schema for codegen_database-managed utility functions (e.g.ledger_apply_state). Defaults to"codegen_database". Override only if your project already uses a schema named"codegen_database".
- property all_plugins: list[Plugin]¶
Return extension plugins + direct plugins.
Extension plugins are prepended before direct plugins.
- Returns:
Combined plugin list.
- register(*plugins)[source]¶
Register one or more plugins globally.
- Parameters:
*plugins (
Plugin) – Plugin instances to add.- Return type:
- Returns:
selffor chaining.
- use(*extensions)[source]¶
Register one or more extensions.
- Parameters:
*extensions (
CodegenDatabaseExtension) – Extension instances to add.- Return type:
- Returns:
selffor chaining.
- DEFAULT_UTILITY_SCHEMA = 'codegen_database'¶
Default schema for codegen_database-managed utility objects – the
CodegenDatabaseConfig.utility_schemadefault and the fallbackresolve_utility_schema()uses when no config is present.
- resolve_utility_schema(metadata)[source]¶
Return the codegen_database utility schema for metadata.
Reads
utility_schemaoff theCodegenDatabaseConfigregistered atmetadata.info["codegen_database_config"], falling back toDEFAULT_UTILITY_SCHEMAwhen no config is present.Any codegen_database-managed object that needs a shared home – utility functions, the autogenerated-identifier pointer table – routes through here so a single
utility_schemasetting governs them all.
Check constraint support for codegen_database dimensions.
Provides CodegenDatabaseCheck, a declarative check constraint that uses
{column_name} markers in its expression. Plugins resolve these
markers to the appropriate column references depending on the
dimension type (table-level for simple/append-only, NEW.col for
EAV trigger-based enforcement).
- class CodegenDatabaseCheck(expression, name)[source]¶
A declarative check constraint with
{col}markers.- Parameters:
- collect_checks(schema_items)[source]¶
Filter
CodegenDatabaseCheckinstances from a schema items list.- Parameters:
schema_items (
list) – Mixed list ofColumn,CodegenDatabaseCheck, and other schema items.- Return type:
- Returns:
Only the
CodegenDatabaseCheckitems, in their original order.
Index support for codegen_database dimensions.
Provides CodegenDatabaseIndex, a declarative index definition
that mirrors sqlalchemy.Index and uses {column_name}
markers for column references.
- class CodegenDatabaseIndex(name, *expressions, unique=False, **kw)[source]¶
A declarative index definition with
{col}markers.Mirrors the
sqlalchemy.Indexconstructor signature:CodegenDatabaseIndex("idx_name", "{col1}", "{col2}", unique=True, postgresql_using="btree")
Simple column references (
"{name}") and functional expressions ("lower({name})") are both supported. Extra keyword arguments are passed through to the underlyingsqlalchemy.Index.- Parameters:
- collect_indices(schema_items)[source]¶
Filter
CodegenDatabaseIndexinstances from schema items.- Parameters:
schema_items (
list) – Mixed list of schema items.- Return type:
- Returns:
Only the
CodegenDatabaseIndexitems, in original order.
- trigram_indexes(*columns, table=None, method='gin')[source]¶
Build one
pg_trgmindex per column for fuzzy text search.A
pg_trgmoperator-class index is what makes the%similarity operator andILIKEsubstring matches fast – without one, a trigram search falls back to a sequential scan. Pass the text columns a resource searches and splat the result into a dimension’sschema_items; theTableIndexPluginmaterializes each into a real index:schema_items=[ Column("name", String), Column("sku", String), *trigram_indexes("name", "sku", table="product"), ]
Every index is
USING <method> (<col> <method>_trgm_ops)– the operator class is what binds the index to the trigram operators, so a plainUSING gin (col)would not serve%/ILIKElookups.The
pg_trgmextension is registered by default –configure_metadataadds it viacodegen_database.pg_extension.register_default_pg_extensions(), so the next autogenerated migration emits theCREATE EXTENSIONwhen the database lacks it.- Parameters:
*columns (
str) – Text column names to index for trigram search.table (
str|None) – Optional table name woven into each index name so it stays unique schema-wide – index names are database-global. Omitted yieldsix__<col>__trgm; given,ix__<table>__<col>__trgm.method (
str) – Index access method –"gin"(default, the usual choice) or"gist". The matching<method>_trgm_opsoperator class is applied.
- Return type:
- Returns:
One
CodegenDatabaseIndexper column, in input order.- Raises:
CodegenDatabaseValidationError – If columns is empty or contains a duplicate, or method is neither
"gin"nor"gist".
Foreign key support for codegen_database dimensions.
Provides CodegenDatabaseForeignKey — an inline single-column FK
passed directly to a Column(...) constructor, analogous to
SQLAlchemy’s ForeignKey. Accepts either a two-part
"dimension.column" reference (resolved via the registry) or a
fully-qualified "schema.table.column" reference.
- class CodegenDatabaseForeignKey(reference, *, ondelete=None, onupdate=None)[source]¶
Inline single-column FK with codegen_database dimension resolution.
Pass directly to a
Columnconstructor like SQLAlchemy’sForeignKey. The reference string accepts two formats:"dimension.column"— resolved via the dimension registry at factory time (two dot-separated parts):Column("user_id", Integer, CodegenDatabaseForeignKey("users.id"))
"schema.table.column"— passed through directly:Column("user_id", Integer, CodegenDatabaseForeignKey("public.users_raw.id"))
- class DimensionRef(schema, table)[source]¶
Registry entry for a dimension’s FK-targetable table.
Stored in
metadata.info["codegen_database_dimensions"]keyed by dimension name (tablename).
- defer_fk_resolution(metadata, dimension, table, column_name, fk)[source]¶
Park an FK declaration until dimension is registered.
Factories run at class-creation time, so a two-part reference can name a dimension whose model simply hasn’t been imported yet.
register_dimension()materializes parked entries the moment the dimension arrives;validate_fks_resolved()reports any that never do.- Parameters:
metadata (
MetaData) – SQLAlchemy MetaData instance.dimension (
str) – The (not yet registered) dimension name.table (
Table) – The table the constraint will be appended to.column_name (
str) – The referencing column on table.fk (
CodegenDatabaseForeignKey) – The original inline FK marker.
- Return type:
- materialize_fk(metadata, table, column_name, fk, resolved_ref)[source]¶
Append the
ForeignKeyConstraintfor an inline FK marker.- Parameters:
metadata (
MetaData) – SQLAlchemy MetaData instance (for target validation).table (
Table) – The table to append the constraint to.column_name (
str) – The referencing column on table.fk (
CodegenDatabaseForeignKey) – The inline FK marker carrying referential actions.resolved_ref (
str) – Fully qualified"schema.table.column"target.
- Return type:
- register_dimension(metadata, name, ref)[source]¶
Register a dimension for FK resolution.
Also materializes any FK declarations that were deferred because they referenced name before it was registered – model import order doesn’t constrain who may reference whom.
- Parameters:
metadata (
MetaData) – SQLAlchemy MetaData instance.name (
str) – Dimension name (tablename).ref (
DimensionRef) – The dimension’s FK target info.
- Return type:
- resolve_fk_reference(metadata, reference)[source]¶
Resolve a
"dimension.column"reference.Looks up the dimension name in the registry and expands it to
"schema.table.column".- Parameters:
- Return type:
- Returns:
Fully qualified
"schema.table.column"string.- Raises:
CodegenDatabaseValidationError – If the reference does not contain exactly one dot, or names an unknown dimension.
- validate_fk_target(resolved_ref, metadata)[source]¶
Validate a fully-qualified FK target against the metadata.
Only validates when the target table is present in metadata (i.e. it is a codegen_database-managed table). External tables are silently skipped.
- Parameters:
- Raises:
CodegenDatabaseValidationError – If the target column does not exist on a known table.
- Return type:
- validate_fks_resolved(metadata)[source]¶
Fail if any deferred FK never found its dimension.
Call after every model module is imported (the alembic
configure_metadatahook does) – a leftover entry means the reference names a dimension that doesn’t exist, and silently omitting the constraint would be far worse than failing here.- Parameters:
metadata (
MetaData) – SQLAlchemy MetaData instance.- Raises:
CodegenDatabaseValidationError – A two-part FK reference names a dimension that was never registered.
- Return type:
Declarative and imperative PostgreSQL function registration.
CodegenDatabaseFunction works both imperatively and as a declarative
base class, mirroring how
CodegenDatabaseView
and CodegenDatabasePlainView work
for views.
- class CodegenDatabaseFunction(name, schema, definition, *, metadata=None, returns='void', language='sql', parameters=None, security=FunctionSecurity.invoker, volatility=FunctionVolatility.VOLATILE)[source]¶
Register a PostgreSQL function on SQLAlchemy metadata.
Works both imperatively (direct instantiation) and as a declarative base class (subclassing).
Imperative usage (
metadataread from class when omitted):class FunctionBase(CodegenDatabaseFunction): metadata = Base.metadata spec = ledger_event_function(source, event) FunctionBase( spec["name"], source.ctx.schemaname, spec["definition"], parameters=spec["parameters"], returns=spec["returns"], security=spec["security"], )
Or with explicit metadata:
CodegenDatabaseFunction( name="inventory_adjust", schema="private", definition="...", metadata=metadata, returns="SETOF private.inventory_raw", parameters=[...], security=FunctionSecurity.definer, )
Declarative usage:
class FunctionBase(CodegenDatabaseFunction): metadata = metadata class InventoryAdjust(FunctionBase): __funcname__ = "inventory_adjust" __table_args__ = {"schema": "private"} __definition__ = "..." __options__ = FunctionOptions( returns="SETOF private.inventory_raw", parameters=[...], security=FunctionSecurity.definer, )
In both cases a
sqlalchemy_declarative_extensions.dialects.postgresql.Functionis registered on the providedMetaDatainstance so that Alembic autogeneration picks it up.- Parameters:
name (
str) – Unqualified function name.schema (
str) – PostgreSQL schema.metadata (
MetaData|None) – SQLAlchemyMetaDatato register on.definition (
str) – SQL (or PL/pgSQL) function body.returns (
str) – Return type string (default"void").language (
str) – Function language (default"sql").parameters (
list[FunctionParam] |None) – List ofFunctionParaminstances fromsqlalchemy_declarative_extensions.dialects.postgresql(default[]).security (
FunctionSecurity) – Security mode —FunctionSecurity.invokerorFunctionSecurity.definer(defaultFunctionSecurity.invoker).volatility (
FunctionVolatility) – Volatility classification (defaultFunctionVolatility.VOLATILE).
- class CodegenDatabaseFunctionSpec[source]¶
Specification dict for a PostgreSQL function.
Returned by spec-builders such as
ledger_event_function(),construct_ledger_chart_function(), andconstruct_date_bin_function(). Pass directly as__funcspec__on aCodegenDatabaseFunctionMixinsubclass, or unpack intoCodegenDatabaseFunctionfor imperative registration:class InventoryAdjust(CodegenDatabaseFunctionMixin, Base): __table_args__ = {"schema": "private"} __funcspec__ = ledger_event_function(inventory, adjust_event)
- name¶
Unqualified function name.
- definition¶
SQL function body.
- language¶
Function language (
"sql","plpgsql", …).
- parameters¶
List of
FunctionParaminstances.
- returns¶
Return type string (e.g.
"SETOF schema.table").
- security¶
Security mode.
- volatility¶
Optional volatility (
STABLE/IMMUTABLE/VOLATILE). Defaults toVOLATILEwhen omitted.
Note
Schema is intentionally excluded. Pass it via
__table_args__(declarative) or read it from the source’sctx.schemaname(imperative).
- class FunctionOptions(returns='void', language='sql', parameters=<factory>, security=FunctionSecurity.invoker, volatility=FunctionVolatility.VOLATILE)[source]¶
Options for
CodegenDatabaseFunctiondeclarative subclasses.Set on a subclass via
__options__.- returns¶
Return type string.
- language¶
Function language (
"sql","plpgsql", …).
- parameters¶
List of
FunctionParaminstances fromsqlalchemy_declarative_extensions.dialects.postgresql.
- security¶
FunctionSecurity.invokerorFunctionSecurity.definer.
- volatility¶
Volatility classification.
Custom SQLAlchemy column types for codegen_database.
TextEnum/IntEnum– persist a Python enum asTEXT/INTEGER.EncryptedText– a Fernet-encrypted-at-restTEXTcolumn (requires theencryptedextra forcryptography).COORDINATE/Coordinate– a geographiclatitude/longitudevalue stored as a PostGISgeography(Point,4326), round-tripping as aCoordinatefrozen dataclass.VALUE_TYPES/build_value_type()– theinteger/numeric/decimalvalue-column vocabulary the ledger factories type their amount columns from, with precision / scale pass-through toNUMERIC.STDADDR/StdAddr– the PostGISstdaddrcomposite type (from theaddress_standardizerextension), round-tripping as aStdAddrfrozen dataclass ofstrparts (""= NULL sub-field).
All names are re-exported here so callers keep importing from
codegen_database.types regardless of which submodule owns them.
- class COORDINATE(*args, **kwargs)[source]¶
A geographic coordinate stored as a PostGIS
geographypoint.The underlying column is
geography(Point,4326)(WGS84). OnlyCoordinateis accepted on input.Coordinates are written with
ST_GeogFromTextand read withST_AsText.Requires the
postgisextension: registerPostgisExtensionon your config so Alembic creates it.Example:
from sqlalchemy import Column from codegen_database.types import COORDINATE, Coordinate Column("coordinate", COORDINATE(), nullable=False)
- bind_expression(bindvalue)[source]¶
Wrap the bound WKT in
ST_GeogFromTexton the way in.- Return type:
- process_bind_param(value, dialect)[source]¶
Serialize a Coordinate to
POINT(longitude latitude)WKT.
- process_literal_param(value, dialect)[source]¶
Render as an inline SQL literal (for
literal_binds).Returns the quoted WKT (well known text)
bind_expression()then wraps it inST_GeogFromText(...).- Return type:
- class Coordinate(*, latitude, longitude)[source]¶
A geographic coordinate:
latitude/longitudein degrees.Values are
Decimalfor exact handling in Python. This is a Python-side representation only: PostGIS stores the point asfloat8(double precision), so a value read back is aDecimalof the rounded double, not necessarily the exact value originally written.
- class EncryptedText(key)[source]¶
Store a string Fernet-encrypted in a
TEXTcolumn.- Parameters:
key (
str|Callable[[],str]) – The encryption key material, or a zero-argument callable returning it. A callable is invoked on every encrypt / decrypt, so keys read from the environment resolve lazily – pass e.g.lambda: os.environ["TOKEN_KEY"].
Example:
import os from sqlalchemy.orm import Mapped, mapped_column from codegen_database.types import EncryptedText class Connection(Base): ... access_token: Mapped[str] = mapped_column( EncryptedText(key=lambda: os.environ["TOKEN_KEY"]), )
Reading a value encrypted under a different key raises
cryptography.fernet.InvalidToken– rotating the key makes previously stored values undecryptable, so treat the key like a database credential.
- class IntEnum(enum_class)[source]¶
Store a Python enum as an integer in PostgreSQL.
Values are persisted as the enum member’s
.value(which must be an integer) and coerced back to the corresponding Python enum member on load. The underlying column isINTEGER.- Parameters:
enum_class (
type[Enum]) – Theenum.Enumsubclass to coerce values to and from. All member values must be integers.
Example:
import enum from sqlalchemy import Column from codegen_database.types import IntEnum class Priority(enum.Enum): LOW = 1 MEDIUM = 2 HIGH = 3 Column("priority", IntEnum(Priority), nullable=False)
- class STDADDR(*args, **kwargs)[source]¶
A PostGIS
stdaddrcomposite column.The underlying column is the native
stdaddrtype from theaddress_standardizerextension. Values round-trip asStdAddrfrozen dataclasses; onlyStdAddr(orNone) is accepted on input.""parts are stored as NULL sub-fields and come back as"".Conversion is delegated to PostgreSQL via JSON, so there is no hand-written composite (de)serialization:
On read,
column_expression()wraps the column into_jsonb(...); the driver returns adictthatprocess_result_value()splats into aStdAddr.On write,
process_bind_param()emits a JSON object andbind_expression()feeds it throughjsonb_populate_record(NULL::stdaddr, ...).
Requires the extension to be installed:
CREATE EXTENSION IF NOT EXISTS address_standardizer
Example:
from sqlalchemy import Column from codegen_database.types import STDADDR, StdAddr Column("address", STDADDR(), nullable=True) # store StdAddr(house_num="123", name="MAIN", suftype="ST")
- bind_expression(bindvalue)[source]¶
Build the
stdaddrcomposite from the bound JSON object.- Return type:
- column_expression(colexpr)[source]¶
Read the column as
jsonbso the driver returns a mapping.type_=selfkeeps the wrapped expression typed asSTDADDRsoprocess_result_value()still runs on the decoded value (the driver decodesjsonbto adictby its server OID regardless of the SQLAlchemy type).- Return type:
- class StdAddr(*, building='', house_num='', predir='', qual='', pretype='', name='', suftype='', sufdir='', ruralroute='', extra='', city='', state='', country='', postcode='', box='', unit='')[source]¶
A parsed PostGIS
stdaddrvalue.Each attribute maps to one column of the PostgreSQL
stdaddrcomposite type, in declaration order, and is astrwhere""means the sub-field is absent (stored as NULL). All fields default to"".The field set and order mirror the composite type defined by
address_standardizer:CREATE TYPE stdaddr AS ( building text, house_num text, predir text, qual text, pretype text, name text, suftype text, sufdir text, ruralroute text, extra text, city text, state text, country text, postcode text, box text, unit text );
- class TextEnum(enum_class)[source]¶
Store a Python enum as plain text in PostgreSQL.
Values are persisted as the enum member’s
.value(which must be a string) and coerced back to the corresponding Python enum member on load. No PostgreSQLENUMtype is created — the underlying column isTEXT.Example:
import enum from sqlalchemy import Column from codegen_database.types import TextEnum class Color(enum.Enum): RED = "red" GREEN = "green" BLUE = "blue" Column("color", TextEnum(Color), nullable=False)
- build_value_type(value_type, *, precision=None, scale=None)[source]¶
Instantiate the SQLAlchemy type for a named value column.
- Parameters:
value_type (
Literal['integer','numeric','decimal']) – One ofValueType.precision (
int|None) – Total number of digits for aNUMERICcolumn (theNUMERIC(precision, scale)first argument). Only valid for the"numeric"/"decimal"types.scale (
int|None) – Number of digits after the decimal point. Requires precision –NUMERICcannot fix a scale without a precision.
- Return type:
- Returns:
A configured
TypeEngineinstance ready to drop into aColumn.- Raises:
CodegenDatabaseValidationError – If value_type is unknown, if precision/scale are supplied for a non-
NUMERICtype, or if scale is given without precision.
Generic view factories.
- class CodegenDatabaseMaterializedView(name, schema, metadata, query)[source]¶
Create a materialized view with an auto-generated refresh.
After construction,
self.tableis a joinable SQLAlchemyTablewhose columns mirror the query.
- class CodegenDatabasePlainView(name, schema, metadata, query)[source]¶
Create a plain PostgreSQL view from a SQLAlchemy select.
Prefer the declarative
CodegenDatabaseViewbase class for new views; this class exists for cases where an imperative registration with aself.tablehandle is needed.After construction,
self.tableis a joinable SQLAlchemyTablewhose columns mirror the query.
Plugin base class and helpers for codegen_database factory extensions.
- class Dynamic(attr)[source]¶
Reference to an instance attribute that holds the actual ctx key.
Use inside
produces()/requires()decorators when the ctx key name is determined at construction time rather than being a fixed string:@produces(Dynamic("table_key")) @requires("raw") class MyPlugin(Plugin): def __init__(self, table_key: str = "result") -> None: self.table_key = table_key def run(self, ctx: FactoryContext) -> None: ctx[self.table_key] = build(ctx["raw"])
The factory resolves each
Dynamicviagetattr(instance, attr)when building the dependency graph.
- class MinPGVersion(version)[source]¶
Minimum PostgreSQL version requirement for a plugin.
Use inside
requires()to declare that a plugin needs a specific PostgreSQL major version:@requires(MinPGVersion(18)) @produces("pk_columns") class UUIDV7PKPlugin(Plugin): ...
The factory stores the requirement as
min_pg_versionon the class. Callcheck_pg_version()with the connected server’s major version to validate before applying DDL.
- class Plugin[source]¶
Base class for codegen_database factory plugins.
Each plugin implements
runto perform its work. Execution order is determined by topological sort using theproduces()andrequires()class decorators.Declaring dependencies:
@produces(Dynamic("out_key")) @requires("primary") class MyPlugin(Plugin): def __init__(self, out_key: str = "result") -> None: self.out_key = out_key def run(self, ctx: FactoryContext) -> None: ctx[self.out_key] = transform(ctx["primary"])
Plugins communicate through
ctxusing string keys. Use thesingleton()decorator to declare that at most one plugin of a given group may appear in any resolved plugin list.- resolved_produces()[source]¶
Return the ctx keys this plugin writes, with Dynamic refs resolved.
Reads the
_produceslist set by theproduces()decorator and substitutes eachDynamicwithgetattr(self, attr).
- resolved_requires()[source]¶
Return the ctx keys this plugin reads, with Dynamic refs resolved.
Reads the
_requireslist set by therequires()decorator and substitutes eachDynamicwithgetattr(self, attr).
- run(ctx)[source]¶
Execute this plugin’s work against ctx.
The factory calls this once per plugin, after topological sorting by
produces()/requires().- Parameters:
ctx (
FactoryContext) – The factory context.- Return type:
- class PluginCollection[source]¶
Generator for multiple plugins
This is often useful if you want to define a plugin that needs to run at two different points in the dependency graph. For example, a plugin that needs to modify both the view and the backing table might need to run before and after the index plugins.
- check_pg_version(server_version, plugins)[source]¶
Raise if any plugin requires a newer PostgreSQL version.
Call this with the server’s major version (e.g.
conn.dialect.server_version_info[0]) to get an early, clear error instead of a cryptic “function does not exist” from PostgreSQL.- Parameters:
- Raises:
CodegenDatabaseValidationError – When a plugin’s
min_pg_versionexceeds server_version.- Return type:
- produces(*keys)[source]¶
Declare the ctx keys this plugin’s
runmethod writes.Applied as a class decorator, alongside
requires()andsingleton():@produces(Dynamic("table_key")) class MyTablePlugin(Plugin): ...
- Parameters:
*keys (
str|Dynamic) – Ctx key strings orDynamicreferences to instance attributes that hold the actual key names.- Return type:
Callable[[TypeVar(T, bound=type[Plugin])],TypeVar(T, bound=type[Plugin])]- Returns:
A class decorator that attaches
_producesto the class.- Raises:
TypeError – If a Dynamic attr name is not an
__init__parameter.
- requires(*keys)[source]¶
Declare the ctx keys this plugin’s
runmethod reads.Applied as a class decorator, alongside
produces()andsingleton(). AcceptsMinPGVersionsentinels to declare a minimum PostgreSQL version requirement:@requires(MinPGVersion(18), "pk_columns") class MyPlugin(Plugin): ...
- Parameters:
*keys (
str|Dynamic|MinPGVersion) – Ctx key strings,Dynamicreferences, orMinPGVersionversion requirements.- Return type:
Callable[[TypeVar(T, bound=type[Plugin])],TypeVar(T, bound=type[Plugin])]- Returns:
A class decorator that attaches
_requiresto the class and setsmin_pg_versionif anyMinPGVersionsentinel is present.- Raises:
TypeError – If a Dynamic attr name is not an
__init__parameter.
- singleton(group)[source]¶
Declare that at most one plugin of group may appear.
The factory raises
CodegenDatabaseValidationErrorat construction time if two plugins with the same group name are present in the resolved plugin list.Example:
@singleton("__pk__") class MyPKPlugin(Plugin): ...
- Parameters:
group (
str) – Arbitrary group identifier. By convention, built-in groups use dunder names ("__pk__","__table__").- Return type:
Callable[[TypeVar(T, bound=type[Plugin])],TypeVar(T, bound=type[Plugin])]- Returns:
A class decorator that sets
singleton_groupon the class and registers the singleton validator.
Extension base class and discovery for codegen_database.
- class CodegenDatabaseExtension(name)[source]¶
Base class for codegen_database extensions.
An extension bundles plugins, metadata hooks, Alembic hooks, and CLI commands into a single installable unit.
Subclasses override hook methods to participate in the codegen_database lifecycle. Extensions declare inter-extension dependencies via the
depends_onclass variable.Example:
@dataclass class MyExtension(CodegenDatabaseExtension): name: str = "my-ext" def plugins(self) -> list[Plugin]: return [MyGlobalPlugin()] def configure_metadata(self, metadata: MetaData) -> None: # register roles, grants, schemas, etc. ...
- configure_alembic()[source]¶
Register custom Alembic renderers or rewriters.
Override to hook into Alembic setup. Called by
alembic_hook.- Return type:
- configure_metadata(metadata)[source]¶
Configure metadata-level objects.
Override to register roles, grants, schemas, or other metadata-level objects. Called by
configure_metadata.
- discover_extensions()[source]¶
Discover extensions via the
codegen_database.extentry point group.- Return type:
- Returns:
Mapping of extension name to extension class.
- validate_extension_deps(extensions)[source]¶
Check that every extension’s
depends_onis satisfied.- Parameters:
extensions (
list[CodegenDatabaseExtension]) – The resolved list of extension instances.- Raises:
CodegenDatabaseValidationError – If a dependency is missing.
- Return type:
Declarative PostgreSQL extension management for codegen_database.
PGExtension describes a PostgreSQL extension that the
application requires. register_pg_extension() stores it in
metadata.info so the Alembic comparator can emit
CREATE EXTENSION IF NOT EXISTS when the extension is missing from
the database.
Extensions are never dropped automatically. Removing an extension from metadata simply stops codegen_database from ensuring it is present; the extension itself remains installed until a DBA drops it manually.
DEFAULT_PG_EXTENSIONS (currently pg_trgm) are registered
on every metadata by
codegen_database.alembic.register.configure_metadata() – no
opt-in needed; a manual registration of the same name wins.
Usage:
from codegen_database.pg_extension import PGExtension, register_pg_extension
# Standalone registration
register_pg_extension(metadata, PGExtension("btree_gist"))
register_pg_extension(metadata, PGExtension("pg_cron", schema="cron"))
# Alembic wiring (call once in env.py)
from codegen_database.pg_extension import (
register_pg_extension_alembic_events,
)
register_pg_extension_alembic_events()
Alembic autogenerate then emits:
op.execute("CREATE EXTENSION IF NOT EXISTS btree_gist")
- class CreateExtensionOp(extension)[source]¶
Alembic operation: create a PostgreSQL extension.
Inherits from
alembic.operations.ops.MigrateOperationso that codegen_database’s Alembic rewriter passes it through duringprocess_revision_directivestraversal without error.
- DEFAULT_PG_EXTENSIONS: tuple[PGExtension, ...] = (PGExtension(name='pg_trgm', schema=None, cascade=False),)¶
Extensions every codegen_database project gets without opting in.
pg_trgmbacks the%similarity operator that trigram text search (the default list-endpoint search strategy upstack) andtrigram_indexesrely on; it ships with contrib and is harmless when unused, so registering it by default removes a foot-gun – a project that declares searchable text columns no longer 500s at first search because nobody rememberedCREATE EXTENSION.
- ExtensionOwnedRelations¶
Relations owned by an installed extension, keyed by
(schema, name).schemais always a concrete name (neverNone);publicfor the default schema.
- class PGExtension(name, schema=None, cascade=False)[source]¶
Describe a PostgreSQL extension to install.
- Parameters:
- class PGExtensions(extensions=<factory>)[source]¶
Container for all extensions registered on a MetaData.
Stored under
metadata.info["pg_extensions"]byregister_pg_extension().- classmethod extract(metadata)[source]¶
Return registered extensions or
Noneif none exist.- Parameters:
metadata (
object) – SQLAlchemyMetaData, orNone.- Return type:
- Returns:
The
PGExtensionsholder orNone.
- assert_pg_extension_declared(metadata, name)[source]¶
Raise if name has not been declared as a required extension.
Call this inside a plugin’s
run()method to give a clear error when the user forgot to register the extension in metadata (e.g. via aPGCronExtension).In the declarative model flow, extensions are configured via
configure_metadatawhich runs after all models are imported. If a plugin calls this check at class-definition time and the extension is not yet in metadata but aCodegenDatabaseConfigis present onmetadata.info, the config’s extension hooks are run eagerly so that the check can succeed. The fullconfigure_metadatacall inenv.pywill overwrite any interim state with the final correct values.- Parameters:
- Raises:
CodegenDatabaseValidationError – If name is not declared and cannot be resolved from the registered config.
- Return type:
- fetch_extension_owned_relations(connection)[source]¶
Return relations created and owned by installed extensions.
A PostgreSQL extension may create tables, views, sequences, and other relations that it owns (recorded in
pg_dependwithdeptype = 'e'). The canonical example is PostGIS’sspatial_ref_systable. These relations are part of the extension, not the application schema, so Alembic autogenerate must not emitDROP TABLE/CREATE TABLEfor them – dropping one fails outright (cannot drop table spatial_ref_sys because extension postgis requires it).- Parameters:
connection (
Connection) – Active database connection.- Return type:
- Returns:
A set of
(schema, name)pairs for every relation owned by an installed extension. Empty when no extensions own relations.
- register_default_pg_extensions(metadata)[source]¶
Register
DEFAULT_PG_EXTENSIONSon metadata.Called by
codegen_database.alembic.register.configure_metadata()so every project wired through the standardenv.pyhooks gets the defaults; safe to call repeatedly – an extension already declared (by any path) is not added twice.
- register_pg_extension(metadata, ext)[source]¶
Store ext in metadata for Alembic autogenerate.
- Parameters:
metadata (
MetaData) – SQLAlchemyMetaDatato register on.ext (
PGExtension) – The extension to register.
- Return type:
- register_pg_extension_alembic_events()[source]¶
Register the extension comparator and renderer with Alembic.
Call once in
env.pyalongsidealembic_hook():from codegen_database.pg_extension import ( register_pg_extension_alembic_events, ) register_pg_extension_alembic_events()
Safe to call multiple times; subsequent calls are no-ops.
- Return type:
Shared column-reference validation and marker helpers.
All codegen_database constraint types
(CodegenDatabaseCheck,
CodegenDatabaseIndex)
use {column_name} markers. This module provides the regex, extraction,
resolution, and validation helpers they share.
- COLUMN_MARKER_RE = re.compile('\\{(\\w+)\\}')¶
Regex matching
{column_name}markers in expressions.
- validate_column_references(label, columns, known_columns)[source]¶
Raise if any column is not in known_columns.
Shared by check, index, and foreign-key plugins to ensure that user-provided column names actually exist on the target table or view.
- Parameters:
- Raises:
CodegenDatabaseValidationError – If a column is not in known_columns.
- Return type:
Cave exception types.
- exception CodegenDatabaseValidationError[source]¶
Raised when a codegen_database validation check fails.
Schema item validators for dimension factories.
- frozen_function_default(server_default)[source]¶
Return the SQL of a
server_defaultthat PostgreSQL will freeze.A
server_defaultgiven as a plainstris emitted as a quoted SQL literal (DEFAULT 'now()'), which PostgreSQL evaluates once at DDL time – every row then shares that single frozen value. When the string is shaped like a function call (now(),gen_random_uuid(),nextval(...)…) that is almost never what was meant. Wrap it infunc.<fn>()ortext(...)so it renders as a live call re-evaluated per row.Returns the offending string, or
Nonewhen the default is safe (a SQL element /text()clause, or a plain literal constant such as"0"/"true").
- is_schema_item_not_primary_key(item)[source]¶
Return True if the item is not a primary key column.
- Parameters:
item (
SchemaItem|CodegenDatabaseCheck|CodegenDatabaseIndex) – The schema item to inspect.- Return type:
- Returns:
Trueif item is not a primary key column.
- reject_frozen_function_defaults(tables)[source]¶
Raise if any column defaults a function call passed as a plain string.
Guards against the silent
DEFAULT 'now()'bug (frozen at migration time) across every column in tables – both user-supplied columns and the timestamp columns the built-in plugins inject.- Return type:
- validate_schema_items(items, *, validators=None)[source]¶
Validate a list of SchemaItems against the given validators.
- Parameters:
- Raises:
CodegenDatabaseValidationError – If any item fails a validator.
- Return type:
Factory internals¶
The resource factories that plugins compose into. Most users interact with these through the built-in plugins rather than directly.
Core ResourceFactory: plugin runner.
- class ResourceFactory(tablename, schemaname, metadata, schema_items, *, config=None, plugins=None, extra_plugins=None)[source]¶
Core factory: resolves plugins and runs them in dependency order.
Subclasses declare
DEFAULT_PLUGINSfor user-facing defaults and_INTERNAL_PLUGINSfor always-present built-in logic. Callers can override or extend the plugin list viaplugins/extra_plugins, and inject global plugins viaconfig.Plugin execution order is determined by each plugin’s
producesandrequiresdeclarations. Plugins with no declared dependencies run in the order they appear in the list.Resolution order:
global_plugins + user_plugins + internal_plugins, then topological sort.If no user or global plugin produces
pk_columnsand internal plugins are present, aSerialPKPluginis auto-prepended.- Parameters:
tablename (
str) – Name of the dimension table.schemaname (
str) – PostgreSQL schema for all generated objects.metadata (
MetaData) – SQLAlchemyMetaDatathe objects are bound to.schema_items (
list[SchemaItem|CodegenDatabaseCheck|CodegenDatabaseIndex]) – Column and constraint definitions. Must not include a primary key column.config (
object|None) – Optional global config supplying prepended plugins.plugins (
list[Plugin|PluginCollection] |None) – If given, replacesDEFAULT_PLUGINSentirely.extra_plugins (
list[Plugin|PluginCollection] |None) – Appended to the resolved plugin list.
- Raises:
CodegenDatabaseValidationError – If any schema item fails validation, two plugins share a singleton group, two plugins produce the same ctx key, or a plugin dependency cycle is detected.
- ctx: FactoryContext¶
The factory context after plugin execution.
Downstream view factories and query builders read this to access tables, columns, and other plugin outputs.
- table: FromClause¶
The root selectable created by the factory.
This is the
__root__context value set by the table plugin, exposing the column metadata for use in queries, foreign key references, and ledger event lambdas.
Factory context dataclass.
- class ContextSource(*args, **kwargs)[source]¶
Structural type for objects that expose a factory context.
Both
ResourceFactoryinstances andCodegenDatabaseBase/CodegenDatabaseViewsubclasses satisfy this protocol — the latter as class objects (ctxis a class attribute set by the declarative base’s class-construction hook).Query builders (
construct_ledger_balance_query(),ledger_event_function()) accept anyContextSource, making imperative and declarative styles interchangeable.
- class FactoryContext(tablename, schemaname, metadata, schema_items, plugins)[source]¶
Carries inputs and accumulates plugin outputs.
Typed input fields (set by the factory, read-only for plugins):
tablename,schemaname,metadata,schema_items,plugins– as passed to the factory constructor.
Plugin store (read/write via item syntax):
Plugins communicate by storing and retrieving arbitrary values using string keys. The key names a plugin reads and writes are explicit constructor arguments on that plugin (with sensible defaults), so multiple independent pipelines can coexist by using distinct keys.
ctx["key"] = valueStore a value. Raises
KeyErrorif key is already set – two plugins writing the same key is almost certainly a mistake. Usectx.set("key", value, force=True)to override intentionally.ctx["key"]Retrieve a value. Raises
KeyErrorwith a plugin-ordering hint when the key is absent."key" in ctxTest whether a key has been set without raising.
Injected columns (append-only list):
Plugins that provide columns for table construction (e.g.
CreatedAtPlugin,UUIDEntryIDPlugin,DoubleEntryPlugin) appendColumnobjects toctx.injected_columns. Table plugins spread this list into the table definition alongside PK and dimension columns.- property columns: list[Column]¶
Return only
Columninstances from schema_items.Useful when a plugin needs to iterate over column definitions (e.g. to extract column names or types).
- property dim_column_names: list[str]¶
Return writable (non-PK, non-computed) column names.
Filters out primary-key and computed columns from
schema_items, leaving only the user-defined dimension columns that a plugin should read or write. Equivalent to the_dim_column_nameshelper that was previously duplicated across multiple plugin modules.
- property pk_column_name: str¶
Return the primary key column name.
Shorthand for
ctx["pk_columns"].first_key. Requires a PK plugin (e.g.SerialPKPlugin) to have run first.- Raises:
KeyError – If
pk_columnshas not been set yet.
- set(key, value, *, force=False)[source]¶
Store value under key, with optional override.
- Parameters:
- Raises:
KeyError – If key is already set and
forceisFalse.- Return type:
- property table_items: list[SchemaItem]¶
Return schema items suitable for table creation.
Filters out
CodegenDatabaseCheck(which are handled by dedicated check plugins) but keeps all real SQLAlchemySchemaItemobjects: columns, constraints, indexes, computed columns, etc.
Simple dimension resource factory.
- class CodegenDatabaseSimple(tablename, schemaname, metadata, schema_items, *, config=None, plugins=None, extra_plugins=None)[source]¶
Create a simple dimension: one table with optional checks.
Internal plugins (always present):
SimpleTablePlugin– raw backing table ({tablename}_raw).SimpleViewPlugin– writable view ({tablename}).TableCheckPlugin– check constraints on the raw table.TableIndexPlugin– indexes on the raw table.TableFKPlugin– foreign keys on the raw table.RawTableProtectionPlugin– blocks direct DML on the raw table.InsteadOfTriggerPlugin– INSTEAD OF triggers on the dimension view.
A
SerialPKPluginis auto-added when no user plugin producespk_columns.- Parameters:
tablename (
str) – Name of the dimension table.schemaname (
str) – PostgreSQL schema for generated objects.metadata (
MetaData) – SQLAlchemyMetaDatato register on.schema_items (
list[SchemaItem|CodegenDatabaseCheck|CodegenDatabaseIndex]) – Column and constraint definitions.plugins (
list[Plugin|PluginCollection] |None) – Behaviour-modifying plugins (e.g.UUIDV4PKPlugin).extra_plugins (
list[Plugin|PluginCollection] |None) – Appended to the resolved plugin list.
- class SimpleTablePlugin[source]¶
Create a single backing table for a simple dimension.
Creates
{tablename}_rawand stores it inctx["raw_table"]. The writable view{tablename}is created bySimpleViewPlugin().
- SimpleViewPlugin()[source]¶
Create a configured ViewPlugin for simple dimensions.
Registers
{tablename}as a view over{tablename}_rawand stores the proxy inctx["primary"].- Return type:
- Returns:
A
ViewPluginconfigured for simple passthrough views.
Append-only dimension resource factory.
- class AppendOnlyTablePlugin(root_key='root_table', attributes_key='attributes')[source]¶
Create the root and attributes tables for an append-only dim.
- Parameters:
- AppendOnlyViewPlugin(root_key='root_table', attributes_key='attributes', primary_key='primary')[source]¶
Create a configured ViewPlugin for append-only dimensions.
- Parameters:
- Return type:
- Returns:
A
ViewPluginconfigured for append-only join views.
- class CodegenDatabaseAppendOnly(tablename, schemaname, metadata, schema_items, *, config=None, plugins=None, extra_plugins=None)[source]¶
Create an append-only (SCD Type 2) dimension.
Internal plugins (always present), in order:
The column-name plugin
construct_column_name_plugin()sets thecreated_atcolumn name. Then:UniqueColumnCheckPlugin– lifts column-levelunique=Trueinto INSTEAD OF trigger checks (must run before the table is built so the unique flag can be stripped in time).AppendOnlyTablePlugin– root + attributes tables.AppendOnlyViewPlugin– join view proxy.TableIndexPlugin– indices on the attributes table.TableFKPlugin– foreign keys on the attributes table.InsteadOfTriggerPlugin– INSTEAD OF triggers (activates when a view plugin produces"primary").
TableCheckPluginis auto-added by the base factory when not already present.A
SerialPKPluginis auto-added when no user plugin producespk_columns.
- class UniqueColumnCheckPlugin[source]¶
Lift column-level
unique=Trueinto INSTEAD OF trigger checks.SQLAlchemy column-level uniqueness propagates onto the underlying attributes log. An append-only UPDATE rewrites into an INSERT that carries every column value forward, so the new revision collides with its own predecessor on any unique column even when the value is unchanged.
For each column declared with
unique=Truethis plugin:Strips the
uniqueflag beforeAppendOnlyTablePluginattaches the column to the attributes table – once attached, SQLAlchemy materialises aUniqueConstrainteagerly and clearing the flag no longer suppresses it.Registers an INSTEAD OF trigger on the public view that rejects duplicates among the current revisions only.
The trigger is best-effort: it does not take a row lock, so two concurrent inserts can both pass the check before either commits. Use
SERIALIZABLEisolation or an explicit advisory lock if airtight uniqueness is required.
EAV dimension resource factory.
- class CodegenDatabaseEAV(tablename, schemaname, metadata, schema_items, *, config=None, plugins=None, extra_plugins=None)[source]¶
Create an EAV (Entity-Attribute-Value) dimension.
Internal plugins (always present), in order:
The column-name plugin
construct_column_name_plugin()sets thecreated_atcolumn name. Then:EAVTablePlugin– entity + attribute tables.EAVViewPlugin– pivot view proxy.TriggerCheckPlugin– trigger-based checks on the pivot view.InsteadOfTriggerPlugin– INSTEAD OF triggers (activates when a view plugin produces"primary").
A
SerialPKPluginis auto-added when no user plugin producespk_columns.
- class EAVTablePlugin(entity_key='entity', attribute_key='attribute', mappings_key='eav_mappings')[source]¶
Create entity and attribute tables for an EAV dimension.
- Parameters:
- EAVViewPlugin(entity_key='entity', attribute_key='attribute', mappings_key='eav_mappings', primary_key='primary')[source]¶
Create a configured ViewPlugin for EAV dimensions.
- Parameters:
entity_key (
str) – Key inctxfor the entity root table (default"entity").attribute_key (
str) – Key inctxfor the attribute log (default"attribute").mappings_key (
str) – Key inctxfor the EAV mappings list (default"eav_mappings").primary_key (
str) – Key inctxto store the view proxy under (default"primary").
- Return type:
- Returns:
A
ViewPluginconfigured for EAV pivot views.
- class TriggerCheckPlugin(table_key='primary')[source]¶
Enforce checks via INSTEAD OF triggers (EAV dimensions).
Generates a single trigger function per view per operation (INSERT/UPDATE) that validates all
CodegenDatabaseCheckitems. Triggers use a_check_prefix to fire before the main dimension triggers (Postgres fires multiple INSTEAD OF triggers in alphabetical order by name).- Parameters:
table_key (
str) – Key inctxfor the trigger target view (default"primary").
Ledger resource factory.
- class CodegenDatabaseLedger(tablename, schemaname, metadata, schema_items, *, config=None, plugins=None, extra_plugins=None)[source]¶
Create a ledger: append-only table with a value column.
Internal plugins (always present), in order:
UUIDEntryIDPlugin– UUID entry ID for correlating related entries.
Next, the column-name plugin
construct_column_name_plugin()sets thecreated_atcolumn name. Then:LedgerTablePlugin– raw backing table ({tablename}_raw).LedgerViewPlugin– writable view ({tablename}).RawTableProtectionPlugin– blocks direct DML on the raw table.InsteadOfTriggerPlugin– INSTEAD OF triggers on the dimension view.
A
SerialPKPluginis auto-added when no user plugin producespk_columns.Use
construct_ledger_balance_query(),construct_ledger_latest_query(), andledger_event_function()withCodegenDatabaseFunctionfor derived views and event functions.
- class LedgerTablePlugin(value_type='integer', *, precision=None, scale=None)[source]¶
Create a ledger raw table with a value column.
Combines
ctx["pk_columns"],ctx.injected_columns(provided by upstream plugins likeUUIDEntryIDPlugin,CreatedAtPlugin, andDoubleEntryPlugin), avaluecolumn, andctx.table_items(dimension columns) into a single append-only table named{tablename}_raw.The writable view
{tablename}is created byLedgerViewPlugin().- Parameters:
value_type (
Literal['integer','numeric','decimal']) – Type for the value column. One of"integer","numeric", or"decimal"(default"integer").precision (
int|None) – Total number of digits when value_type is"numeric"/"decimal"– theNUMERIC(precision, scale)first argument.Noneleaves the column unconstrained.scale (
int|None) – Digits after the decimal point. Requires precision.
- Raises:
CodegenDatabaseValidationError – If value_type is not a recognised type, or precision/scale are misconfigured.
- LedgerViewPlugin()[source]¶
Create a configured ViewPlugin for ledger dimensions.
Registers
{tablename}as a view over{tablename}_rawand stores the proxy inctx["primary"].- Return type:
- Returns:
A
ViewPluginconfigured for ledger passthrough views.
- class UUIDEntryIDPlugin(column_name='entry_id')[source]¶
Provide a UUIDv4 entry ID column for ledger tables.
Stores a
Columninctx["entry_id_column"]that downstream table plugins splice into the table definition. The column uses PostgreSQL’sgen_random_uuid()as a server default so callers can omit it for single-entry inserts while still providing an explicit value to correlate multi-row entries.- Parameters:
column_name (
str) – Name of the entry ID column (default"entry_id").
Built-in plugins¶
Plugins that ship with codegen_database and power the default factories.
Primary key plugins.
- class SerialPKPlugin(column_name='id')[source]¶
Provide an auto-increment integer primary key column.
- Parameters:
column_name (
str) – Name of the PK column (default"id").
- class UUIDV4PKPlugin(column_name='id')[source]¶
Provide a UUIDv4 primary key column.
Uses PostgreSQL’s
gen_random_uuid()as the server default so rows get a unique identifier without client-side generation.- Parameters:
column_name (
str) – Name of the PK column (default"id").
- class UUIDV7PKPlugin(column_name='id')[source]¶
Provide a UUIDv7 primary key column.
Uses PostgreSQL 18’s
uuidv7()as the server default to generate time-ordered UUIDs. These sort chronologically, making them friendlier to B-tree indexes than random UUIDv4 values.Requires PostgreSQL 18 or later (declared via
@requires(MinPGVersion(18))). Usecheck_pg_version()to validate the server version before applying DDL.- Parameters:
column_name (
str) – Name of the PK column (default"id").
Column name registry plugin.
- construct_column_name_plugin(ctx_key, column_name)[source]¶
Create a column-name-registry plugin for ctx_key.
Check constraint plugins for codegen_database dimensions.
TableCheckPlugin converts
CodegenDatabaseCheck
items into real SQLAlchemy CheckConstraint objects on a table
(for simple and append-only dimensions).
- class TableCheckPlugin(table_key='primary')[source]¶
Materialize check items as table constraints.
Turns
CodegenDatabaseCheckitems into SQLAlchemyCheckConstraintobjects.Reads
CodegenDatabaseCheckitems fromctx.schema_items, resolves{col}markers to plain column names (identity), and appends realCheckConstraintobjects to the target table.- Parameters:
table_key (
str) – Key inctxfor the target table (default"primary").
Index plugin for codegen_database dimensions.
TableIndexPlugin converts
CodegenDatabaseIndex
items into real SQLAlchemy Index objects on a table.
- class TableIndexPlugin(table_key='primary')[source]¶
Materialize index items as table indexes.
Turns
CodegenDatabaseIndexitems into SQLAlchemyIndexobjects.Reads
CodegenDatabaseIndexitems fromctx.schema_items, validates column names, and createsIndexobjects on the target table. Extra keyword arguments on eachCodegenDatabaseIndexare passed through to the underlyingsqlalchemy.Index.- Parameters:
table_key (
str) – Key inctxfor the target table (default"primary").
Foreign key plugin for codegen_database dimensions.
TableFKPlugin converts inline
CodegenDatabaseForeignKey column markers into real
SQLAlchemy ForeignKeyConstraint objects on a table. Two-part
references ("dimension.column") are resolved via the dimension
registry in metadata.info.
- class TableFKPlugin(table_key='primary')[source]¶
Materialize FK declarations as
ForeignKeyConstraintobjects.Handles inline
CodegenDatabaseForeignKeymarkers attached toColumnconstructors (single-column).Two-part
"dimension.column"references are resolved via the dimension registry. Factories run at class-creation time, so a reference may name a dimension whose model hasn’t been imported yet – those are deferred and materialize when the dimension registers (seeregister_dimension()); import order never constrains who may reference whom. Three-part"schema.table.column"references are passed through directly.- Parameters:
table_key (
str) – Key inctxfor the target table (default"primary").
User-facing plugins for ledger (append-only value) tables.
- class DoubleEntryPlugin(column_name='direction')[source]¶
Add debit/credit semantics to a ledger table.
Adds a
directioncolumn ('debit'or'credit') to the schema items so thatLedgerTablePluginincludes it in the table. Also registers an AFTER INSERT constraint trigger that validates all rows sharing anentry_idhave equal total debits and credits.This plugin must appear before
LedgerTablePluginin the plugin list so its column is included in the table definition.- Parameters:
column_name (
str) – Name of the direction column (default"direction").
- class DoubleEntryTriggerPlugin(table_key='raw_table')[source]¶
Register an AFTER INSERT trigger enforcing balanced entries.
Validates that for every
entry_idin the inserted batch, the sum of debit values equals the sum of credit values. Raises a PostgreSQL exception if any entry is unbalanced.Uses a statement-level trigger with a
REFERENCING NEW TABLEtransition table so that multi-row inserts are checked as a whole, not row-by-row.Must run after
LedgerTablePlugin(needs the table) and afterDoubleEntryPlugin(needs column name).- Parameters:
table_key (
str) – Key inctxfor the backing table (default"primary").
- class LedgerBalanceCheckPlugin(dimensions, min_balance=0, table_key='raw_table')[source]¶
Enforce a minimum balance per dimension group.
Registers an
AFTER INSERT FOR EACH STATEMENTtrigger that checksSUM(value) >= min_balancefor every dimension group affected by the inserted rows. If any group violates the constraint the entire statement is rejected.Uses the same
REFERENCING NEW TABLEtransition-table pattern asDoubleEntryTriggerPlugin.- Parameters:
- Raises:
CodegenDatabaseValidationError – If dimensions is empty.
Plugin that prevents direct DML on raw backing tables.
Direct INSERT/UPDATE/DELETE on raw backing tables bypasses the INSTEAD OF triggers on the dimension views, which can corrupt dimension state (e.g. breaking SCD Type 2 history in append-only dimensions, leaving orphaned EAV rows).
RawTableProtectionPlugin installs BEFORE triggers on every raw
table it is given. The triggers raise an exception unless one of two
conditions holds: the insert arrives via the INSTEAD OF trigger on the
dimension view (pg_trigger_depth() >= 2), or the caller has set the
transaction-local
config parameter codegen_database.event_fn_active = 'true' via
set_config('codegen_database.event_fn_active', 'true', true). Trusted event
functions such as those built by
ledger_event_function() set this
flag so they can insert directly into the raw table in a single
batched statement, which is required for correct double-entry
validation.
- class RawTableProtectionPlugin(*table_keys)[source]¶
Prevent direct DML on raw backing tables.
Installs BEFORE INSERT/UPDATE/DELETE triggers on every raw table specified by table_keys. The triggers raise an exception when called at trigger depth 0 (i.e. directly, not from within another trigger), so mutations must go through the dimension view.
All mutations through the dimension view arrive via an INSTEAD OF trigger at depth >= 1, which the protection triggers allow through.
- Parameters:
*table_keys (
str) – One or morectxkeys whose values are the rawTableobjects to protect.
Example:
RawTableProtectionPlugin("root_table", "attributes")
Soft-delete plugin for codegen_database dimensions.
Intercepts the INSTEAD OF DELETE trigger on the dimension view and converts it into a timestamp-based soft-delete. The dimension view is filtered to hide soft-deleted rows, keeping the interface clean.
The plugin itself is factory-agnostic: it only records intent in the
context (the soft-delete column name and the delete-trigger template to
use). Each factory’s table plugin injects the deleted_at column on
the correct backing table, and each factory’s delete template performs
the timestamp update. The exact behaviour per factory type:
Simple (
table_key="raw_table"):UPDATE raw_table SET deleted_at = now() WHERE id = OLD.id. The view appendsWHERE deleted_at IS NULL.AppendOnly (
table_key="attributes"): inserts a new attributes version withdeleted_at = now()and updates the root FK pointer. The view appendsWHERE deleted_at IS NULL.EAV (
table_key="entity"):UPDATE entity SET deleted_at = now() WHERE id = OLD.id. Thedeleted_atcolumn lives on the auto-generated entity table and the pivot view filters it in-query (a plainWHEREappend would land after the pivot’sGROUP BY), so the view-patching step is skipped.Ledger: not applicable – ledger rows are immutable by design.
Usage:
# Simple dimension
class Users(Base):
__tablename__ = "users"
__table_args__ = {"schema": "public"}
__plugins__ = [SoftDeletePlugin()]
name = Column(String, nullable=False)
# AppendOnly dimension
class Students(Base):
__tablename__ = "students"
__table_args__ = {"schema": "public"}
__factory__ = CodegenDatabaseAppendOnly
__plugins__ = [SoftDeletePlugin(table_key="attributes")]
name = Column(String, nullable=False)
# EAV dimension
class Products(Base):
__tablename__ = "products"
__table_args__ = {"schema": "public"}
__factory__ = CodegenDatabaseEAV
__plugins__ = [SoftDeletePlugin(table_key="entity")]
sku = Column(String, nullable=False)
The deleted_at column is injected automatically onto the backing
table – you do not need to declare it on the model. For Simple and
AppendOnly dimensions you may declare it explicitly (as a
nullable=True DateTime column) if you want it visible on the
model; the injection is skipped when the column already exists. For EAV
you must not declare it: user columns become EAV attributes, and the
soft-delete flag must live on the entity table instead.
After soft-deletion the row remains in the backing table with
deleted_at set. The view {tablename} only shows rows where
deleted_at IS NULL.
- class SoftDeletePlugin(column_name='deleted_at', table_key='raw_table')[source]¶
Soft delete plugin.
- class SoftDeletePluginAfterRoot(column_name='deleted_at', table_key='raw_table')[source]¶
Patch the view to filter out soft-deleted rows.
- run(ctx)[source]¶
Append
WHERE deleted_at IS NULLto the registered view.Skipped when the factory already filters soft-deleted rows inside its view query (it sets
soft_delete_view_filtered). A blindWHEREappend cannot be used there – e.g. EAV’s pivot view ends inGROUP BYand the clause would be misplaced.- Return type:
- class SoftDeletePluginBeforeRoot(column_name='deleted_at')[source]¶
Record soft-delete intent for the factory’s table/trigger plugins.
Stores the soft-delete column name under
deleted_at_column(read by the factory’s table plugin to create the column and by EAV’s view builder to filter the pivot) and injects aDeleteTriggerOverrideso the factory’sInsteadOfTriggerPluginrenders a soft-delete body instead of a physical DELETE – without the factory’s ops builder needing any soft-delete-specific code. This plugin runs before the table plugin (which declaresdeleted_at_columnin itsrequires).- Parameters:
column_name (
str) – Name of the soft-delete timestamp column (default"deleted_at").
- soft_delete_columns(ctx)[source]¶
Return the soft-delete column(s) to inject onto a backing table.
Factory table plugins splat this into their
Table(...)call so thedeleted_atcolumn is created whenever aSoftDeletePluginrecorded its intent inctx.Returns an empty list when no soft-delete is configured, or when the user already declared the column among
schema_items(Simple and AppendOnly allow that), so it is safe to call unconditionally.- Parameters:
ctx (
FactoryContext) – The factory context.- Return type:
- Returns:
A one-element list with the nullable
DateTimesoft-delete column, or an empty list.
Row-level security plugins for codegen_database dimensions.
Each plugin registers one or more PostgreSQL RLS policies on the raw
backing table so that queries are automatically filtered by the
current session’s app.* settings.
Four pre-built policies cover the most common patterns:
OwnerRLSPlugin– generic: any column matched against anyapp.*setting.UserRLSPlugin– per-user isolation (app.user_id).TenantRLSPlugin– per-tenant isolation (app.tenant_id).TenantUserRLSPlugin– tenant isolation AND per-user visibility within the tenant.
All plugins accept an optional bypass_roles list. Roles in that
list receive an unconditional USING (true) policy so they can
read and write every row – useful for service accounts and trusted
roles.
Prerequisites¶
RLS policy diffing in Alembic requires calling
register_rls_alembic_events()once inenv.py.The application must call
SET LOCAL app.user_id = '...'(or equivalent) for each request so the session setting is available.
Usage:
class Users(Base):
__tablename__ = "users"
__table_args__ = {"schema": "public"}
__plugins__ = [UserRLSPlugin(bypass_roles=["authenticator"])]
user_id = Column(String, nullable=False)
name = Column(String, nullable=False)
- class OwnerRLSPlugin(column, setting, *, policy_name='owner_policy', bypass_roles=None, table_key='raw_table')[source]¶
Generic owner-column RLS: filter rows by a session setting.
Enables row-level security on the target table and creates a
PERMISSIVEpolicy that restricts access to rows wherecurrent_setting(setting, true)::text = column::text.- Parameters:
column (
str) – Column that identifies the row’s owner.setting (
str) – PostgreSQL session setting to compare against, e.g."app.user_id".policy_name (
str) – Name for the created policy (default"owner_policy").bypass_roles (
list[str] |None) – Roles that receive an unconditionalUSING (true)policy, bypassing the owner filter.table_key (
str) – Key inctxfor the table to protect (default"raw_table").
- class TenantRLSPlugin(tenant_column='tenant_id', *, bypass_roles=None, table_key='raw_table')[source]¶
Per-tenant row isolation via
app.tenant_id.Filters rows to those where the nominated tenant_column matches
current_setting('app.tenant_id', true).- Parameters:
- class TenantUserRLSPlugin(tenant_column='tenant_id', user_column='user_id', *, bypass_roles=None, table_key='raw_table')[source]¶
Tenant isolation combined with per-user visibility.
Rows are only visible when BOTH conditions hold:
current_setting('app.tenant_id', true)matches tenant_column, ANDcurrent_setting('app.user_id', true)matches user_column.
- Parameters:
- class UserRLSPlugin(user_column='user_id', *, bypass_roles=None, table_key='raw_table')[source]¶
Per-user row isolation via
app.user_id.Filters rows to those where the nominated user_column matches
current_setting('app.user_id', true).- Parameters:
Temporal validity range plugin for codegen_database dimensions.
TemporalPlugin adds valid_from/valid_to columns
to a dimension’s backing table and provides:
A
construct_temporal_as_of_query()helper that queries the table at a specific point in time.A GiST-based exclusion constraint (requiring the
btree_gistPostgreSQL extension) to prevent overlapping validity ranges for the same entity.
The plugin works with any factory type that has a user-controlled
backing table (CodegenDatabaseSimple, CodegenDatabaseAppendOnly,
CodegenDatabaseLedger). The user supplies valid_from and
valid_to columns in schema_items; the plugin validates them
and adds the exclusion constraint.
Usage:
orders = CodegenDatabaseSimple(
tablename="orders",
schemaname="app",
metadata=metadata,
schema_items=[
Column("order_id", Integer, nullable=False),
Column("status", String, nullable=False),
Column(
"valid_from",
DateTime(timezone=True),
nullable=False,
server_default=func.now(),
),
Column("valid_to", DateTime(timezone=True), nullable=True),
],
extra_plugins=[
TemporalPlugin(
subject_columns=["order_id"],
valid_from_column="valid_from",
valid_to_column="valid_to",
),
],
)
# Point-in-time query
query = construct_temporal_as_of_query(
orders,
as_of="2024-06-15T00:00:00Z",
subject_columns=["order_id"],
)
The exclusion constraint prevents storing two overlapping validity
windows for the same order_id. Requires btree_gist:
CREATE EXTENSION IF NOT EXISTS btree_gist;
- class TemporalPlugin(subject_columns, valid_from_column='valid_from', valid_to_column='valid_to', table_key='raw_table')[source]¶
Add temporal validity range support to a dimension.
Validates that valid_from_column and valid_to_column exist in the backing table, stores their names in
ctx["temporal_columns"], and appends a GiST exclusion constraint to the table so that no two rows with the same subject_columns values have overlappingtstzrange\ s.The exclusion constraint uses:
EXCLUDE USING GIST ( col1 WITH =, ..., tstzrange(valid_from, COALESCE(valid_to, 'infinity'), '[)') WITH && )
which requires the
btree_gistextension to be installed.- Parameters:
subject_columns (
list[str]) – Columns that identify a unique entity across multiple temporal versions (e.g.["order_id"]).valid_from_column (
str) – Name of the range-start column (default"valid_from").valid_to_column (
str) – Name of the range-end column (default"valid_to").table_key (
str) – Key inctxfor the backing table to add the constraint to (default"raw_table").
- Raises:
CodegenDatabaseValidationError – If valid_from_column or valid_to_column is missing, non-nullable for valid_from (it must have a value), or not a
DateTimecolumn.
- construct_temporal_as_of_query(source, as_of, subject_columns=None, *, table_key='raw_table')[source]¶
Build a point-in-time query for a temporal dimension.
Returns rows where:
valid_from <= :as_of AND (valid_to IS NULL OR valid_to > :as_of)
- Parameters:
source (
ContextSource) – A factory instance or declarative class that was built withTemporalPlugin.as_of (
str) – ISO 8601 timestamp string or PostgreSQL-parseable timestamp for the point in time to query.subject_columns (
list[str] |None) – Columns identifying unique entities. When provided, appliesDISTINCT ONto return only the most recent version per subject at as_of. WhenNone, returns all matching rows.table_key (
str) – Key inctxfor the backing table (default"raw_table").
- Return type:
- Returns:
A SQLAlchemy
Select.- Raises:
CodegenDatabaseValidationError – If
TemporalPluginhas not been applied to source.
Ledger snapshot plugin: maintain a running-total table.
LedgerSnapshotPlugin creates a {tablename}_snapshot
table whose rows always reflect SUM(value) per dimension group.
An AFTER INSERT FOR EACH STATEMENT trigger on the ledger raw
table applies each batch of inserts as incremental UPSERT
operations, so the snapshot stays in sync without a full re-scan.
This is the performance-optimised alternative to
construct_ledger_balance_query()
when the ledger is large and balance reads are frequent.
Usage:
inventory = CodegenDatabaseLedger(
tablename="inventory",
schemaname="private",
metadata=metadata,
schema_items=[
Column("warehouse", String, nullable=False),
Column("sku", String, nullable=False),
],
extra_plugins=[
LedgerSnapshotPlugin(dimensions=["warehouse", "sku"]),
],
)
# inventory.snapshot_table is a joinable proxy.
After construction the snapshot_table attribute on the factory
instance holds a joinable SQLAlchemy Table whose columns are the
declared dimension columns plus balance (same type as
value) and updated_at.
Note: the snapshot does not enforce a minimum balance. Use
LedgerBalanceCheckPlugin on the
ledger table for that.
- class LedgerSnapshotPlugin(dimensions, value_type='integer', *, precision=None, scale=None, table_key='raw_table')[source]¶
Maintain a running-total snapshot table for a ledger.
Creates
{tablename}_snapshotwith one row per unique combination of dimensions columns. AnAFTER INSERT FOR EACH STATEMENTtrigger incrementally applies each batch of ledger inserts viaINSERT ... ON CONFLICT DO UPDATE, keeping the snapshot current without a full aggregation scan.After the plugin runs,
ctx["snapshot_table"]is a joinable SQLAlchemyTablewhose columns are the declared dimensions plusbalanceandupdated_at.- Parameters:
dimensions (
list[str]) – Column names to group the balance by. Must be a non-empty list matching columns on the ledger raw table.value_type (
Literal['integer','numeric','decimal']) – Type for thebalancecolumn. One of"integer","numeric", or"decimal". Should match the ledger’svalue_type(default"integer").precision (
int|None) – Total number of digits when value_type is"numeric"/"decimal". Should match the ledger’sprecision.Noneleaves the column unconstrained.scale (
int|None) – Digits after the decimal point. Requires precision.table_key (
str) – Key inctxfor the ledger raw table (default"raw_table").
- Raises:
CodegenDatabaseValidationError – If dimensions is empty, value_type is unrecognised, or precision/scale are misconfigured.
Search vector plugin for codegen_database dimensions.
SearchVectorPlugin maintains a tsvector column on the
backing table, keeping it in sync with one or more source text columns
via a BEFORE INSERT OR UPDATE trigger.
PostgreSQL’s full-text search infrastructure (tsvector,
to_tsvector, @@, GIN indexes) is built into the database
engine — no extension is required for basic usage.
Usage:
products = CodegenDatabaseSimple(
"products",
"app",
metadata,
schema_items=[
Column("name", String, nullable=False),
Column("description", String),
Column("search_vector", TSVECTOR, nullable=True),
],
extra_plugins=[
SearchVectorPlugin(
source_columns=["name", "description"],
vector_column="search_vector",
),
],
)
The trigger function is registered under
{schema}.{schema}_{tablename}_search_vector_update() and fires
BEFORE INSERT OR UPDATE on the raw backing table.
You may optionally pass a GIN index via
CodegenDatabaseIndex on the vector column
(e.g. CodegenDatabaseIndex(columns=["search_vector"], using="gin")),
which is the standard way to make full-text searches fast.
If the text search configuration (ts_config) references a
dictionary installed via a PostgreSQL extension (e.g.
unaccent, pg_trgm), register that extension in metadata
via register_pg_extension().
- class SearchVectorPlugin(source_columns, vector_column='search_vector', ts_config='english', table_key='raw_table')[source]¶
Maintain a
tsvectorcolumn via a BEFORE trigger.Generates and registers a
BEFORE INSERT OR UPDATEtrigger function that recomputes:NEW.vector_column := to_tsvector( ts_config, COALESCE(col1, '') || ' ' || COALESCE(col2, '') || ... )
Source columns are concatenated with a space separator;
NULLvalues are coalesced to an empty string so they do not suppress the entire vector.- Parameters:
source_columns (
list[str]) – One or more column names whose text content feeds the search vector. At least one required.vector_column (
str) – Name of thetsvectorcolumn to maintain (default"search_vector"). Must exist inschema_itemswith a compatible type.ts_config (
str) – PostgreSQL text-search configuration name (default"english"). Use any configuration installed in the database. If the configuration depends on a PostgreSQL extension (e.g."unaccent"), register that extension withregister_pg_extension().table_key (
str) – Key inctxfor the backing table to attach the trigger to (default"raw_table").
- Raises:
CodegenDatabaseValidationError – If source_columns is empty, any source column is absent from the table, or vector_column is absent from the table.
Generic view plugin for dimension factories.
- class ViewPlugin(query_builder, proxy_builder, primary_key='primary', extra_requires=None)[source]¶
Register a view and store a proxy table in ctx.
A generic, composable view plugin. Each factory type provides a
query_builderthat returns the view SQL and aproxy_builderthat returns proxy columns for downstream plugins.- Parameters:
query_builder (
Callable[[FactoryContext],str]) – Callable(ctx) -> strreturning the compiled SQL for the view definition.proxy_builder (
Callable[[FactoryContext],list[Column]]) – Callable(ctx) -> list[Column]returning proxy columns for the view’s selectable proxy.primary_key (
str) – Key inctxto store the view proxy under (default"primary").extra_requires (
list[str] |None) – Additional ctx keys this plugin depends on. Declared to the topological sorter so that upstream plugins run first.
Generic INSTEAD OF trigger plugin.
- class DeleteTriggerOverride(template_name, template_vars=<factory>)[source]¶
Replacement template for a dimension’s INSTEAD OF DELETE body.
A plugin (e.g.
SoftDeletePlugin) injects this into the factory context underDELETE_TRIGGER_OVERRIDE_KEYto swap the default physical-delete template for an alternative (e.g. a soft-deleteUPDATE). Each factory renders its delete op throughrender_delete_op(), so no factory needs to special-case the override itself.- Parameters:
- class InsteadOfTriggerPlugin(ops_builder, naming_defaults, function_key, trigger_key, view_key='primary', permitted_operations=None, *, extra_requires=None)[source]¶
Register INSTEAD OF triggers from pre-rendered PL/pgSQL bodies.
A generic, composable trigger plugin. Each factory type provides an
ops_buildercallable that reads from the factory context and returns a list ofTriggerOpwith fully rendered PL/pgSQL.- Parameters:
ops_builder (
Callable[[FactoryContext],list[TriggerOp]]) – Callable that takes aFactoryContextand returns a list ofTriggerOp.naming_defaults (
dict[str,str]) – Default naming templates for function and trigger names.function_key (
str) – Key for function name resolution.trigger_key (
str) – Key for trigger name resolution.view_key (
str) – Key inctxfor the trigger target view. If absent fromctx, trigger registration is skipped.permitted_operations (
list[str] |None) – When set, only operations whose names appear in this list are registered.extra_requires (
list[str] |None) – Additional ctx keys to declare as dependencies for topological ordering.
- render_delete_op(ctx, templates_dir, base_vars)[source]¶
Render the DELETE
TriggerOp, honoring a context override.Renders
delete.plpgsql.makofrom templates_dir with base_vars unless a plugin injected aDeleteTriggerOverrideinctx, in which case the override’s template and extra vars take precedence. This keeps soft-delete (and any future delete variant) out of each factory’s ops builder.
Utilities¶
Migration glue (codegen_database.alembic.*)¶
Hooks your project’s env.py wires into Alembic. The
comparator/renderer/rewriter modules are internal but documented
for reference.
- alembic_hook(config=None)[source]¶
Register codegen_database’s alembic extensions.
Call before importing models.
Usage in
env.py:from codegen_database.alembic.register import ( alembic_hook, configure_metadata, process_revision_directives, ) alembic_hook() # ... import models / build metadata ... configure_metadata(target_metadata)
Then pass
process_revision_directivestocontext.configure(process_revision_directives=...).- Parameters:
config (
CodegenDatabaseConfig|None) – Optional config providing extensions whoseconfigure_alembic()hooks will be called.- Return type:
- configure_metadata(metadata, config=None)[source]¶
Register schemas and extension hooks on metadata.
- Parameters:
metadata (
MetaData) – The SQLAlchemyMetaDatato configure.config (
CodegenDatabaseConfig|None) – Optional config providing extensions. IfNone, falls back tometadata.info["codegen_database_config"].
- Raises:
CodegenDatabaseValidationError – A two-part FK reference names a dimension no imported model registered.
- Return type:
- render_item(type_, obj, autogen_context)[source]¶
Render custom column types for migrations.
The in-scope types (
codegen_database.typesplussqlalchemy_utilsencrypted types, e.g. behindfsh_lib.oauth’s token columns) areTypeDecoratorwrappers around plain SQL types, carrying Python-side state a migration can neither render nor use – an enum class, an encryption-key callable. Alembic’s default rendering emits their dotted class name with no import, producing a migration that doesn’t run. Migrations only need the database-level type, so this renders each type’s ownimpl(TextEnum->sa.Text(),EncryptedText->sa.Text(), …) instead of maintaining a parallel name -> DDL table.Pass this as
render_itemtocontext.configure(...)in your Alembicenv.py.Returns
Falsefor unrecognized objects so Alembic falls through to its default rendering.
- class EntityIdentifier(schema='public', name=None, phase=None)[source]¶
Identifies a database entity within a migration’s dependency graph.
nameisNonefor schema-level entities (i.e. the entity is the schema). For tables, views, and functions,nameholds the unqualified object name andschemaholds its containing schema.phasedistinguishes drop and create ops for the same entity when anUpdate*Ophas been expanded."drop"ops are ordered before"create"ops for the same entity.
- build_fk_graph_from_metadata(metadata)[source]¶
Build a table FK dependency map from SQLAlchemy metadata.
Alembic’s
DropTableOpcarries only the table name and schema — no column or foreign key information. Without an external FK graph the topological sort cannot order drop operations correctly, leading to constraint violations when a referenced table is dropped before its dependents.This function reads FK relationships from SQLAlchemy’s
MetaData(which has the full schema) so thatsort_migration_opscan order both create and drop operations safely.
- drop_spurious_declarative_updates(ops)[source]¶
Drop no-op function/view/trigger
Updateops (default-schema and renderer-whitespace churn). Real changes survive; dependent recreation a real change or column alter needs is re-added by_inject_dependent_update_ops.- Return type:
list[MigrateOperation|MigrateOp]
- expand_update_ops(migration_ops)[source]¶
Split
Update*OpintoDrop*Op+Create*Op.All update op types are split so the topological sort can interleave drops and creates correctly:
Views:
CREATE OR REPLACE VIEWfails when a dependent view has an incompatible column list. Splitting lets dependents be dropped before the dependency is dropped and recreated.Functions/procedures:
DROP FUNCTIONfails when dependent triggers or views still reference the function. Splitting lets the sort order drops before the function drop and creates after the function create._inject_dependent_update_opsensures that dependent objects are present in the op list before this expansion runs.Triggers:
CREATE OR REPLACE TRIGGERis not available in the PostgreSQL versions we target.
- Return type:
list[MigrateOperation|MigrateOp]
- prune_redundant_index_drops(ops)[source]¶
Drop redundant
DROP INDEXops for tables dropped in this migration.Postgres drops a table’s indexes when the table is dropped, so a separate
DROP INDEXfor a table that also has aDropTableOpin the same migration is redundant – and, when it lands after the table drop (e.g. wrapped in aModifyTableOpsthat pins to the create phase), fails outright with “index does not exist”. Strip such index drops, whether top-level or nested inside aModifyTableOps(drop the wrapper if it empties out).
- sort_migration_ops(migration_ops, *, fk_graph)[source]¶
Return migration_ops topologically sorted by entity dependencies.
Dependency edges are derived from the ops themselves:
A table depends on its schema.
A table depends on tables it references via foreign keys.
A replaceable entity (view, function, …) depends on its schema and on every schema-qualified table or view referenced in its SQL definition.
Only dependencies between ops in the current migration produce edges; references to already-existing objects are ignored.
Edge direction is determined per-op by phase: drop-phase ops reverse their edges (dependents dropped first), create-phase ops use normal direction (dependencies created first).
- Parameters:
- Return type:
list[MigrateOperation|MigrateOp]- Returns:
A new list containing the same ops in dependency order.
Automatic schema discovery for alembic autogenerate.
Scans a MetaData instance for schemas referenced by
tables and views, then registers them with
sqlalchemy-declarative-extensions so its built-in schema comparator
emits the appropriate CREATE SCHEMA / DROP SCHEMA ops.
- register_schemas(metadata)[source]¶
Populate
metadata.info["schemas"]from tables and views.Merges with any schemas already registered on the metadata. Safe to call multiple times — existing entries are preserved.
- Return type:
Custom alembic renderers that format SQL with pglast.
- register_renderers()[source]¶
Override the library’s renderers with pglast-formatted versions.
- Return type:
- render_item(type_, obj, autogen_context)[source]¶
Render custom column types for migrations.
The in-scope types (
codegen_database.typesplussqlalchemy_utilsencrypted types, e.g. behindfsh_lib.oauth’s token columns) areTypeDecoratorwrappers around plain SQL types, carrying Python-side state a migration can neither render nor use – an enum class, an encryption-key callable. Alembic’s default rendering emits their dotted class name with no import, producing a migration that doesn’t run. Migrations only need the database-level type, so this renders each type’s ownimpl(TextEnum->sa.Text(),EncryptedText->sa.Text(), …) instead of maintaining a parallel name -> DDL table.Pass this as
render_itemtocontext.configure(...)in your Alembicenv.py.Returns
Falsefor unrecognized objects so Alembic falls through to its default rendering.
Pre-built features (codegen_database.ext.*)¶
Pre-built features composed from codegen_database primitives.
Each submodule under codegen_database.ext bundles tables, views,
functions, plugins, and extensions that solve a specific problem
(ledgers, audit queries, state machines, materialized-view refresh,
cron, row-level security, etc.) using the declarative
primitives exposed at the top level of codegen_database.
Import the specific submodule you need — this package does not re-export symbols.
Opt-in features composed from the primitives above. Each submodule is independent — import only the ones you use.
Ledger¶
Ledger configuration objects.
LedgerEvent configuration and helpers.
A LedgerEvent declares a named operation on a ledger. The
user provides lambdas that produce SQLAlchemy selects; the plugin
compiles them into a single PostgreSQL function per event.
Two modes are supported:
Simple mode (
inputonly): the input select is inserted directly into the ledger view.Diff mode (
input+desired+existing): the desired state is diffed against the existing state and only the correcting deltas are inserted.
- class LedgerEvent(name, input, desired=None, existing=None, diff_keys=<factory>)[source]¶
Declare a named ledger operation.
Each event compiles into a single PostgreSQL function that inserts rows into the ledger view and returns the inserted rows via
RETURNING *.Simple mode — provide only
input. The input select’s columns are inserted directly.Diff mode — provide
input,desired,existing, anddiff_keys. The desired and existing selects are unioned and only non-zero deltas are inserted.- Parameters:
name (
str) – Unique event name within the ledger.input (
Callable[[ParamCollector],Select]) – Lambda(p) -> Selectthat builds the input CTE.pis aParamCollector.desired (
Callable[[FromClause],SelectBase] |None) – Lambda(pginput) -> SelectBasethat builds the desired-state CTE.pginputis a synthetic table reference to theinputCTE – read function-param values aspginput.c.<name>, nop_prefix. May return aunion_allor other compound select.existing (
Callable[[Table,FromClause,FromClause],Select] |None) – Lambda(table, desired, pginput) -> Selectthat builds the existing-state CTE.pginputis the same input-CTE reference passed todesired, so you can reach function params the same way –pginput.c.<name>– in the predicate or join. Useconstruct_ledger_balances_query()(filter by diff-key tuples indesired) orconstruct_ledger_scoped_balances_query()(filter by an owner predicate) for the common patterns.diff_keys (
list[str]) – Column names used for grouping in diff mode. Required whendesiredis set.
- class ParamCollector[source]¶
Collect SQL function parameters during lambda evaluation.
Usage inside an
inputlambda:lambda p: select( p("warehouse", String).label("warehouse"), p("sku", String).label("sku"), )
Each call to
p(name, sa_type)records the parameter and returns aliteral_column()reference (p_name) suitable for embedding in a select.- Parameters:
None.
- construct_ledger_balances_query(*keys)[source]¶
Return an
existingcallable for common balance lookup.Produces a select that negates the current balances for each diff-key group present in the desired CTE:
SELECT key1, key2, SUM(value) * -1 AS value FROM ledger_table WHERE (key1, key2) IN (SELECT key1, key2 FROM desired) GROUP BY key1, key2
Use this when every key the event might want to unwind is also present in
desired. If a status transition or cancellation can retire keys – so they disappear fromdesired– useconstruct_ledger_scoped_balances_query()instead, which slices by an owner predicate rather than by the diff-key tuple.- Parameters:
*keys (
str) – Dimension column names to group by.- Return type:
Callable[[Table,FromClause,FromClause],Select]- Returns:
A callable suitable for
LedgerEvent(existing=...).
- construct_ledger_scoped_balances_query(*keys, where)[source]¶
Return an
existingcallable scoped by an owner predicate.Unlike
construct_ledger_balances_query(), which filters existing rows to diff-key tuples that appear indesired, this filters by an arbitrary predicate over the ledger root table:SELECT key1, key2, SUM(value) * -1 AS value FROM ledger_table WHERE <where(root, pginput)> GROUP BY key1, key2
The scoped form is the right tool whenever an event reconciles every ledger row belonging to some owner entity (a purchase order, an invoice, a work order) and a state transition might retire a diff key – i.e. a row that was posted previously should no longer be posted after the event runs. The default
construct_ledger_balances_querywould miss such retired keys because they no longer appear indesired; the scoped form sees them because it slices by the owner, so the diff naturally unwinds them.- Parameters:
*keys (
str) – Dimension column names to group by (the diff keys).where (
Callable[[Table,FromClause],ColumnElement[bool]]) – Callable(ledger_root, pginput) -> predicatethat returns a boolean restricting rows to the owner scope.pginputis a table reference to the generatedinputCTE, so function-param values are reached aspginput.c.<name>– the same accessordesireduses.
- Return type:
Callable[[Table,FromClause,FromClause],Select]- Returns:
A callable suitable for
LedgerEvent(existing=...).
Example
When the owner id is denormalized on the ledger:
def owned_by_input_po(ledger, pginput): return ( ledger.c.purchase_order_id == pginput.c.purchase_order_id ) existing=construct_ledger_scoped_balances_query( "lot_id", "class_id", "account", "direction", where=owned_by_input_po, )
When the owner is reached via a join, use a subquery:
def owned_by_input_po(ledger, pginput): return ledger.c.lot_id.in_( select(PurchaseLot.id) .join(LineItem, LineItem.id == PurchaseLot.line_item_id) .where( LineItem.purchase_order_id == pginput.c.purchase_order_id, ), ) existing=construct_ledger_scoped_balances_query( "lot_id", "class_id", "account", "direction", where=owned_by_input_po, )
Query builders for ledger factories.
Provides pure-Python helpers that return SQLAlchemy selects
without registering anything on metadata. Callers are responsible
for passing the results to
CodegenDatabasePlainView.
- construct_ledger_balance_query(source, dimensions)[source]¶
Build a balance aggregation query for a ledger.
Generates:
SELECT dim_col1, ..., SUM(value) AS balance FROM <primary_table> GROUP BY dim_col1, ...
- Parameters:
source (
ContextSource) – ACodegenDatabaseLedgerinstance or aCodegenDatabaseBasesubclass usingCodegenDatabaseLedgeras its factory.dimensions (
list[str]) – Column names to group by. Must be a non-empty list.
- Return type:
- Returns:
A SQLAlchemy
Select.- Raises:
CodegenDatabaseValidationError – If dimensions is empty.
- construct_ledger_latest_query(source, dimensions)[source]¶
Build a latest-row query for a ledger using DISTINCT ON.
Generates:
SELECT * FROM <primary_table> DISTINCT ON (dim_col1, ...) ORDER BY dim_col1, ..., created_at DESC
- Parameters:
source (
ContextSource) – ACodegenDatabaseLedgerinstance or aCodegenDatabaseBasesubclass usingCodegenDatabaseLedgeras its factory.dimensions (
list[str]) – Column names to partition by. Must be a non-empty list.
- Return type:
- Returns:
A SQLAlchemy
Select.- Raises:
CodegenDatabaseValidationError – If dimensions is empty.
Rolling and period aggregation query builders for ledger factories.
Provides pure-Python helpers that return SQLAlchemy selects without
registering anything on metadata. Pass results to
CodegenDatabasePlainView or
CodegenDatabaseMaterializedView.
- construct_ledger_gap_filled_period_rollup_query(source, dimensions, *, start=None, end=None, period='day')[source]¶
Build a gap-filled period rollup query.
Generates the Cartesian product of every period in the range
[start, end]with every dimension combination observed in the ledger, then LEFT JOINs deltas back in so empty periods are emitted withdelta = 0:SELECT p.period, d.dim_col1, ..., COALESCE(SUM(l.value), 0) AS delta FROM generate_series(<start>, <end>, INTERVAL '1 <period>') p(period) CROSS JOIN ( SELECT DISTINCT dim_col1, ... FROM <primary_table> ) d LEFT JOIN <primary_table> l ON date_trunc('<period>', l.created_at) = p.period AND l.dim_col1 = d.dim_col1 AND ... GROUP BY p.period, d.dim_col1, ... ORDER BY p.period, d.dim_col1, ...
When start or end is
None, aMIN(created_at)/MAX(created_at)subquery is substituted so the range auto-covers the observed data.- Parameters:
source (
ContextSource) – Ledger source.dimensions (
list[str]) – Column names to group by. Must be a non-empty list.start (
str|ColumnElement|None) – Lower bound of the period axis, orNoneto auto-detect. Accepts a timestamp literal string or a SQLAlchemy expression.end (
str|ColumnElement|None) – Upper bound of the period axis, orNone.period (
Literal['microsecond','millisecond','second','minute','hour','day','week','month','quarter','year','decade','century','millennium']) –date_truncprecision string. Defaults to"day".
- Return type:
- Returns:
A SQLAlchemy
Select.- Raises:
CodegenDatabaseValidationError – If dimensions is empty or period is not a valid
date_truncprecision.
- construct_ledger_period_rollup_query(source, dimensions, period='day')[source]¶
Build a period rollup query using
date_trunc.Generates:
SELECT date_trunc('day', created_at) AS period, dim_col1, ..., SUM(value) AS balance FROM <primary_table> GROUP BY 1, dim_col1, ... ORDER BY 1, dim_col1, ...
- Parameters:
source (
ContextSource) – ACodegenDatabaseLedgerinstance or aCodegenDatabaseBasesubclass usingCodegenDatabaseLedgeras its factory.dimensions (
list[str]) – Column names to group by. Must be a non-empty list.period (
Literal['microsecond','millisecond','second','minute','hour','day','week','month','quarter','year','decade','century','millennium']) –date_truncprecision string, e.g."day","week","month","year". Defaults to"day".
- Return type:
- Returns:
A SQLAlchemy
Select.- Raises:
CodegenDatabaseValidationError – If dimensions is empty or period is not a valid
date_truncprecision.
- construct_ledger_period_running_balance_query(source, dimensions, period='day', split_by=None)[source]¶
Build a per-period running balance query.
Generates:
SELECT date_trunc('<period>', created_at) AS period, dim_col1, ..., COALESCE(SUM(SUM(value)) OVER ( PARTITION BY dim_col1, ... ORDER BY date_trunc('<period>', created_at) ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING ), 0) AS starting_balance, SUM(value) AS delta, SUM(SUM(value)) OVER ( PARTITION BY dim_col1, ... ORDER BY date_trunc('<period>', created_at) ) AS ending_balance FROM <primary_table> GROUP BY 1, dim_col1, ... ORDER BY 1, dim_col1, ...
- Columns:
starting_balance– cumulative balance just before this period (i.e. sum of all prior-period deltas).0for the first period in each partition.delta– net change within this period.ending_balance– cumulative balance at the close of this period (starting_balance + delta).
Passing split_by adds one
SUM(value) FILTER (WHERE col = v)column per listed value, interleaved betweendeltaandending_balance. Typical use:split_by=("direction", ["debit", "credit"])on a double-entry ledger.- Parameters:
source (
ContextSource) – ACodegenDatabaseLedgerinstance or aCodegenDatabaseBasesubclass usingCodegenDatabaseLedgeras its factory.dimensions (
list[str]) – Column names to partition by. Must be a non-empty list.period (
Literal['microsecond','millisecond','second','minute','hour','day','week','month','quarter','year','decade','century','millennium']) –date_truncprecision string. Defaults to"day".split_by (
tuple[str,list[str]] |None) – Optional(column_name, values)pair. Each value becomes a namedSUMcolumn filtered on equality.
- Return type:
- Returns:
A SQLAlchemy
Select.- Raises:
CodegenDatabaseValidationError – On empty dimensions, unknown period, empty or unknown split_by.
- construct_ledger_pivoted_period_query(source, dimensions, periods, period='day')[source]¶
Build a pivoted period query with one column per listed period.
Rows are dimension combinations; columns are the listed period start timestamps. Each cell holds the summed
valuefor that period x dimension combination:SELECT dim_col1, ..., COALESCE(SUM(value) FILTER ( WHERE date_trunc('<period>', created_at) = '<periods[0]>'::timestamptz ), 0) AS "<periods[0]>", ... FROM <primary_table> GROUP BY dim_col1, ... ORDER BY dim_col1, ...
Postgres requires the column set to be known at plan time, so periods must be passed in as an explicit list. This mirrors what the running-balance view provides but with periods on the column axis instead of rows, which is often the shape a spreadsheet or stacked bar chart wants.
- Parameters:
source (
ContextSource) – Ledger source.dimensions (
list[str]) – Column names to group by. Must be a non-empty list.periods (
list[str]) – Period start timestamps as ISO strings, e.g.["2024-01-01", "2024-01-02"]. Each becomes a named output column. Must be a non-empty list.period (
Literal['microsecond','millisecond','second','minute','hour','day','week','month','quarter','year','decade','century','millennium']) –date_truncprecision string. Defaults to"day". Values in periods are matched after both sides are truncated to this precision.
- Return type:
- Returns:
A SQLAlchemy
Select.- Raises:
CodegenDatabaseValidationError – If dimensions or periods is empty, or period is invalid.
- construct_ledger_rolling_window_query(source, dimensions, period='day', window_size=7)[source]¶
Build a rolling-window sum query over per-period deltas.
Generates:
SELECT date_trunc('<period>', created_at) AS period, dim_col1, ..., SUM(value) AS delta, SUM(SUM(value)) OVER ( PARTITION BY dim_col1, ... ORDER BY date_trunc('<period>', created_at) ROWS BETWEEN <window_size-1> PRECEDING AND CURRENT ROW ) AS rolling_sum FROM <primary_table> GROUP BY 1, dim_col1, ... ORDER BY 1, dim_col1, ...
The rolling sum is over rows, not calendar periods: missing periods are skipped rather than counted as zero. Use
construct_ledger_gap_filled_period_rollup_query()as a CTE first if you need calendar-aligned rolling windows.- Parameters:
source (
ContextSource) – Ledger source.dimensions (
list[str]) – Column names to partition by. Must be a non-empty list.period (
Literal['microsecond','millisecond','second','minute','hour','day','week','month','quarter','year','decade','century','millennium']) –date_truncprecision string. Defaults to"day".window_size (
int) – Number of rows in the trailing window, inclusive of the current row. Must be>= 1.
- Return type:
- Returns:
A SQLAlchemy
Select.- Raises:
CodegenDatabaseValidationError – If dimensions is empty, period is invalid, or window_size is less than 1.
- construct_ledger_running_balance_query(source, dimensions)[source]¶
Build a running balance query using a window function.
Generates:
SELECT dim_col1, ..., created_at, value, SUM(value) OVER ( PARTITION BY dim_col1, ... ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS running_balance FROM <primary_table> ORDER BY dim_col1, ..., created_at
- Parameters:
source (
ContextSource) – ACodegenDatabaseLedgerinstance or aCodegenDatabaseBasesubclass usingCodegenDatabaseLedgeras its factory.dimensions (
list[str]) – Column names to partition by. Must be a non-empty list.
- Return type:
- Returns:
A SQLAlchemy
Select.- Raises:
CodegenDatabaseValidationError – If dimensions is empty.
Function spec builder for ledger factories.
Provides ledger_event_function() which compiles a
LedgerEvent into a
CodegenDatabaseFunctionSpec ready to
unpack into CodegenDatabaseFunction.
- ledger_event_function(source, event)[source]¶
Build a function spec for a single ledger event.
Returns a
CodegenDatabaseFunctionSpec.The spec contains the SQL function body generated from the event lambdas and all metadata needed to register a PostgreSQL function via
CodegenDatabaseFunction.- Parameters:
source (
ContextSource) – ACodegenDatabaseLedgerinstance or aCodegenDatabaseBasesubclass usingCodegenDatabaseLedgeras its factory.event (
LedgerEvent) – TheLedgerEventto compile into a function.
- Return type:
- Returns:
A
CodegenDatabaseFunctionSpecready to pass toCodegenDatabaseFunction.- Raises:
CodegenDatabaseValidationError – If event configuration is invalid.
Chart-focused function builders for ledger factories.
These build CodegenDatabaseFunctionSpec
instances that can be unpacked into a declarative
CodegenDatabaseFunctionMixin
subclass (via __funcspec__) or registered imperatively.
The generated functions are LANGUAGE sql STABLE: Postgres inlines
them at plan time, so predicates added by callers (WHERE,
LIMIT) push through the parameters. PL/pgSQL is avoided on
purpose — dynamic grouping in PL/pgSQL means EXECUTE format(...)
which blocks the planner from inlining.
Function bodies are assembled with SQLAlchemy core and compiled via
compile_query() so identifiers, casts, and
window frames go through the normal dialect compiler. Function
parameters (p_period, p_start, p_end) surface as
literal_column references so they render as bare identifiers in
the generated SELECT.
Bucketing uses the codegen_database_date_bin helper
rather than date_trunc so callers can pass arbitrary
interval strides ('15 minutes', '3 months', '1 year',
etc.). The helper must be installed in the same schema as the chart
function (or in date_bin_schema, if overridden).
- class NormalSide(*values)[source]¶
Which side an account’s balance naturally grows on.
Double-entry accounting splits accounts into two camps:
DEBIT: assets and expenses — the balance grows with debit postings (e.g. cash receipts, inventory purchases).CREDIT: liabilities, equity, and revenue — the balance grows with credit postings (e.g. sales, capital injections).
construct_double_entry_chart_function()reads a column carrying these string values from the accounts dimension and uses them to normalizedelta = debits - credits(for debit-normal accounts) ordelta = credits - debits(for credit-normal accounts), sodeltais always positive when the balance moves in the account’s natural direction.Pair with
TextEnumto persist the string value:from codegen_database import NormalSide, TextEnum class Accounts(Base): name = Column(String, unique=True, nullable=False) normal_side = Column(TextEnum(NormalSide), nullable=False)
- construct_double_entry_chart_function(source, *, name, accounts_table, dimensions, account_column='account', direction_column='direction', accounts_key_column='name', normal_side_column='normal_side', period_default='1 day', date_bin_schema=None, date_bin_name='codegen_database_date_bin')[source]¶
Build a double-entry period x dimensions chart function spec.
This is a specialization of
construct_ledger_chart_function()for double-entry ledgers: it joins each raw row to an accounts dimension table, splits rawvalueintodebitsandcreditsby direction, and normalizes the per-bucket change (delta) by each account’snormal_side.The normalization rule is the standard accounting convention:
Debit-normal account (assets, expenses):
delta = debits - credits.Credit-normal account (liabilities, equity, revenue):
delta = credits - debits.
Running balances (
starting_balance,ending_balance) are cumulative sums of the normalizeddeltainside the[p_start, p_end)window, partitioned bydimensions.Signature:
<name>(p_period interval DEFAULT '<period_default>'::interval, p_start timestamptz DEFAULT NULL, p_end timestamptz DEFAULT NULL) RETURNS TABLE ( period tstzrange, <dim1> <type>, ..., starting_balance numeric, debits numeric, credits numeric, delta numeric, ending_balance numeric )
The
periodcolumn is the same[bucket_start, bucket_end)tstzrangeasconstruct_ledger_chart_function()— see that function for filtering examples.Body (CTE-based for readability; the CTE holds per-bucket aggregates, the outer SELECT adds running-balance windows):
WITH per_bucket AS ( SELECT <bucket_start> AS bucket_start, <dims>, a.<normal_side> AS _normal_side, COALESCE(SUM(l.value) FILTER ( WHERE l.<direction> = 'debit'), 0) AS debits, COALESCE(SUM(l.value) FILTER ( WHERE l.<direction> = 'credit'), 0) AS credits FROM <schema>.<raw_table> l INNER JOIN <a_schema>.<accounts> a ON a.<name> = l.<account> WHERE (p_start IS NULL OR l.created_at >= p_start) AND (p_end IS NULL OR l.created_at < p_end) GROUP BY bucket_start, <dims>, a.<normal_side> ) SELECT <period_range> AS period, <dims>, COALESCE(SUM(<delta>) OVER ( PARTITION BY <dims> ORDER BY bucket_start ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING ), 0)::numeric AS starting_balance, debits::numeric AS debits, credits::numeric AS credits, (<delta>)::numeric AS delta, (SUM(<delta>) OVER ( PARTITION BY <dims> ORDER BY bucket_start ))::numeric AS ending_balance FROM per_bucket ORDER BY bucket_start, <dims>
where
<delta>is:CASE WHEN _normal_side = 'debit' THEN debits - credits ELSE credits - debits END
Note
starting_balanceandending_balanceare cumulative within the filtered window, not absolute historical balances. Widenp_startto anchor against history.- Parameters:
source (
ContextSource) – Ledger source.name (
str) – Unqualified function name.accounts_table (
object) – A SQLAlchemyTableor declarative class for the accounts dimension. Must havenameandnormal_sidecolumns (or override via accounts_key_column / normal_side_column).dimensions (
list[str]) – Ledger columns to group by (typically includesaccount). Must be non-empty.account_column (
str) – Name of the account column on the raw ledger table. Defaults to"account".direction_column (
str) – Name of the direction column on the raw ledger table. Defaults to"direction".accounts_key_column (
str) – Column on accounts_table that matchesaccount_columnvalues. Defaults to"name".normal_side_column (
str) – Column on accounts_table holding'debit'or'credit'. Defaults to"normal_side".period_default (
str) – Default forp_periodas an interval literal, e.g."1 day","1 month".date_bin_schema (
str|None) – Schema wherecodegen_database_date_binlives. Defaults to the schema configured byChartExtension("codegen_database"unless overridden).date_bin_name (
str) – Name of the polyfill function. Defaults tocodegen_database_date_bin.
- Return type:
- Returns:
- Raises:
CodegenDatabaseValidationError – If dimensions is empty, any dimension / account_column / direction_column is unknown, or period_default is empty.
- construct_ledger_chart_function(source, *, name, dimensions, period_default='1 day', split_by=None, date_bin_schema=None, date_bin_name='codegen_database_date_bin')[source]¶
Build a period x dimensions chart function spec.
This is a range function: callers pass
p_start/p_endto bracket a half-open[p_start, p_end)window oncreated_at. Both default toNULLmeaning “unbounded on that side”. The function returns one row per(codegen_database_date_bin(p_period, created_at), dimensions...)bucket inside the range.Signature:
<name>(p_period interval DEFAULT '<period_default>'::interval, p_start timestamptz DEFAULT NULL, p_end timestamptz DEFAULT NULL) RETURNS TABLE ( period tstzrange, <dim1> <type>, ..., starting_balance numeric, delta numeric, [<split_value_1> numeric, ...,] ending_balance numeric )
The
periodcolumn is a half-open[bucket_start, bucket_end)tstzrangeso callers see the full extent of each bucket, not just its start.bucket_startis the polyfilledcodegen_database_date_bin(p_period, created_at)andbucket_endisbucket_start + p_period. To filter:-- All buckets contained in Feb 2024: WHERE period <@ tstzrange('2024-02-01', '2024-03-01', '[)') -- Exactly the Feb 2024 bucket (only useful when stride and -- boundary align): WHERE lower(period) = '2024-02-01'
Body (compiled from a SQLAlchemy select):
SELECT CASE WHEN <schema>.codegen_database_date_bin(p_period, created_at) IS NULL THEN NULL::tstzrange ELSE tstzrange( <schema>.codegen_database_date_bin(p_period, created_at), <schema>.codegen_database_date_bin(p_period, created_at) + p_period, '[)') END AS period, <dims>, COALESCE(SUM(SUM(value)) OVER ( PARTITION BY <dims> ORDER BY <schema>.codegen_database_date_bin( p_period, created_at) ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING ), 0)::numeric AS starting_balance, SUM(value)::numeric AS delta, [COALESCE(SUM(value) FILTER ( WHERE <split_col> = '<v>'), 0)::numeric AS <v>, ...,] SUM(SUM(value)) OVER ( PARTITION BY <dims> ORDER BY <schema>.codegen_database_date_bin( p_period, created_at) )::numeric AS ending_balance FROM <schema>.<raw_table> WHERE (p_start IS NULL OR created_at >= p_start) AND (p_end IS NULL OR created_at < p_end) GROUP BY <schema>.codegen_database_date_bin( p_period, created_at), <dims> ORDER BY <schema>.codegen_database_date_bin( p_period, created_at), <dims>
GROUP BY/ORDER BYuse the scalar bucket-start rather than the range expression so the planner operates ontimestamptz, nottstzrange, and windowPARTITION BYstays cheap.The
codegen_database_date_binhelper (seeconstruct_date_bin_function()) must exist indate_bin_schema. It polyfills Postgres’sdate_binfor month/quarter/year strides, which the native function rejects. Mixed-stride intervals (e.g.'1 month 3 days') returnNULLforperiod— theCASEin the body preserves that signal rather than emitting the unbounded range(,).Note
starting_balanceandending_balanceare cumulative flows within the filtered window, not absolute ledger balances. To anchor against history, widenp_startor compose withconstruct_ledger_balance_query().- Parameters:
source (
ContextSource) – Ledger source.name (
str) – Unqualified function name.dimensions (
list[str]) – Column names to group by. Must be non-empty.period_default (
str) – Default value forp_periodas a Postgres interval literal, e.g."1 day","15 minutes","3 months","1 year".split_by (
tuple[str,list[str]] |None) – Optional(column_name, values)pair. Each value becomes a namedSUMcolumn filtered on equality, e.g.split_by=("direction", ["debit", "credit"])on a double-entry ledger.date_bin_schema (
str|None) – Schema wherecodegen_database_date_binlives. Defaults to the schema configured byChartExtension("codegen_database"unless overridden).date_bin_name (
str) – Name of the polyfill function. Defaults tocodegen_database_date_bin.
- Return type:
- Returns:
- Raises:
CodegenDatabaseValidationError – If dimensions is empty, any dimension is unknown, period_default is empty, or split_by names a missing column / is empty.
- construct_ledger_pivoted_chart_function(source, *, name, dimensions, period_default='1 day', date_bin_schema=None, date_bin_name='codegen_database_date_bin')[source]¶
Build a JSONB-pivoted chart function spec.
Returns one row per
dimensionsgroup with a singlebucketsjsonbcolumn mapping UTC-normalized bucket-start keys tonumericdeltas. Keeping the shape fixed (unlikecrosstab, which forces callers to spell every pivot column in anAS t(...)clause at every call site) makes this function usable anywhereSELECT * FROMgoes: ORMs, CTEs, views.Signature:
<name>(p_period interval DEFAULT '<period_default>'::interval, p_start timestamptz DEFAULT NULL, p_end timestamptz DEFAULT NULL) RETURNS TABLE ( <dim1> <type>, ..., buckets jsonb )
Typical usage:
SELECT warehouse, sku, (buckets->>'2024-01-01 00:00:00')::numeric AS day1, (buckets->>'2024-01-02 00:00:00')::numeric AS day2 FROM <schema>.<name>( p_period => '1 day'::interval, p_start => '2024-01-01'::timestamptz, p_end => '2024-01-03'::timestamptz );
Bucket keys are the UTC-normalized text form of the bucket-start timestamp —
to_char(bucket_start AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS')— chosen overbucket_start::textso the keys are stable across session timezones. Missing buckets are absent from the JSONB (not present with value0), so useCOALESCE((buckets->>'...')::numeric, 0)when you need zeros.Body (compiled from a SQLAlchemy select):
SELECT <dims>, jsonb_object_agg( to_char(bucket_start AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS'), delta ) AS buckets FROM ( SELECT <dims>, <schema>.codegen_database_date_bin(p_period, created_at) AS bucket_start, SUM(value)::numeric AS delta FROM <fn_schema>.<raw_table> WHERE (p_start IS NULL OR created_at >= p_start) AND (p_end IS NULL OR created_at < p_end) GROUP BY <dims>, bucket_start ) AS b GROUP BY <dims> ORDER BY <dims>
Note
jsonb_object_aggraises on duplicate keys. The inner aggregate’sGROUP BY <dims>, bucket_startguarantees one row per key within each dimension group, so duplicates only appear ifcodegen_database_date_binreturnsNULL(mixed-stride intervals like'1 month 3 days') for multiple source rows in the same dimension group — those keys collapse tonulland Postgres rejects the aggregate. Avoid mixed-stride intervals;codegen_database_date_bindocuments the supported forms.- Parameters:
source (
ContextSource) – Ledger source.name (
str) – Unqualified function name.dimensions (
list[str]) – Column names used as row keys. Must be non-empty.period_default (
str) – Default forp_periodas an interval literal, e.g."1 day","1 month".date_bin_schema (
str|None) – Schema wherecodegen_database_date_binlives. Defaults to the schema configured byChartExtension.date_bin_name (
str) – Name of the polyfill function. Defaults tocodegen_database_date_bin.
- Return type:
- Returns:
- Raises:
CodegenDatabaseValidationError – If dimensions is empty, any dimension is unknown, or period_default is empty.
- construct_ledger_rollup_chart_function(source, *, name, dimensions, period_default='1 day', date_bin_schema=None, date_bin_name='codegen_database_date_bin')[source]¶
Build a period x ROLLUP(dimensions) chart function spec.
This is a range function: callers pass
p_start/p_endto bracket a half-open[p_start, p_end)window oncreated_at. Both default toNULLmeaning “unbounded on that side”.Signature:
<name>(p_period interval DEFAULT '<period_default>'::interval, p_start timestamptz DEFAULT NULL, p_end timestamptz DEFAULT NULL) RETURNS TABLE ( period tstzrange, <dim1> <type>, ..., delta numeric )
The
periodcolumn is the same[bucket_start, bucket_end)tstzrangeasconstruct_ledger_chart_function()— see that function for the rationale and for filtering examples. Unlike the regular chart function,starting_balanceandending_balanceare not emitted: running totals are undefined against grouping sets.The body uses
ROLLUPto produce hierarchical subtotals in a single query. Rolled-up (subtotal) rows carryNULLin the rolled-up dimension columns; callers distinguish them from detail rows with<dim> IS NULL/<dim> IS NOT NULL. This assumes the dimension itself is neverNULLin the underlying data — which is the norm for FK / enum dimensions; if your data has genuineNULLvalues in a dimension, subtotal rows will be indistinguishable from NULL-data rows.SELECT CASE WHEN <schema>.codegen_database_date_bin(p_period, created_at) IS NULL THEN NULL::tstzrange ELSE tstzrange( <schema>.codegen_database_date_bin(p_period, created_at), <schema>.codegen_database_date_bin(p_period, created_at) + p_period, '[)') END AS period, <dims>, SUM(value)::numeric AS delta FROM <schema>.<raw_table> WHERE (p_start IS NULL OR created_at >= p_start) AND (p_end IS NULL OR created_at < p_end) GROUP BY <schema>.codegen_database_date_bin(p_period, created_at), ROLLUP(<dims>) ORDER BY <schema>.codegen_database_date_bin(p_period, created_at), <dims> NULLS LAST
- Parameters:
source (
ContextSource) – Ledger source.name (
str) – Unqualified function name.dimensions (
list[str]) – Column names forming the rollup hierarchy (left to right = outer to inner). Must be non-empty.period_default (
str) – Default value forp_periodas a Postgres interval literal, e.g."1 day","3 months".date_bin_schema (
str|None) – Schema wherecodegen_database_date_binlives. Defaults to the schema configured byChartExtension("codegen_database"unless overridden).date_bin_name (
str) – Name of the polyfill function. Defaults tocodegen_database_date_bin.
- Return type:
- Returns:
- Raises:
CodegenDatabaseValidationError – If dimensions is empty, any dimension is unknown, or period_default is empty.
Chart extension for codegen_database ledgers.
ChartExtension wires the codegen_database_date_bin polyfill
into the codegen_database lifecycle. Register it on a
CodegenDatabaseConfig to make the ledger chart
function builders usable:
from codegen_database.config import CodegenDatabaseConfig
from codegen_database.ext.chart import ChartExtension
config = CodegenDatabaseConfig()
config.use(ChartExtension())
After registration, codegen_database_date_bin lives in the codegen_database
utility schema (CodegenDatabaseConfig.utility_schema,
"codegen_database" by default). The ledger chart function builders
find it via
metadata.info["codegen_database_chart_schema"] and call into it with the
correct schema qualification.
Without this extension, the chart function builders raise
CodegenDatabaseValidationError with a message
pointing the user at config.use(ChartExtension()).
- class ChartExtension(name='codegen_database-chart', schema=None)[source]¶
Register the
codegen_database_date_binpolyfill into metadata.When registered on a
CodegenDatabaseConfig:configure_metadatacreatescodegen_database_date_binin the resolved schema and stores it undermetadata.info["codegen_database_chart_schema"]so the ledger chart function builders can resolve the helper without a user-supplied override.that schema is also added to
metadata.info["schemas"]so Alembic autogenerate creates it.
Ledger chart function builders (
construct_ledger_chart_function()and friends) callassert_chart_extension_declared()internally and raiseCodegenDatabaseValidationErrorwhen this extension is absent.- Parameters:
name (
str) – Extension name. Defaults to"codegen_database-chart".schema (
str|None) – Schema for the polyfill function.None(the default) uses the config’sutility_schema, socodegen_database_date_binshares the one codegen_database utility schema with every other codegen_database-managed object. Set a string to override.
- assert_chart_extension_declared(metadata)[source]¶
Raise if
ChartExtensionis not registered on metadata.Called by the ledger chart function builders to give a clear error when the user forgot to register the extension on their
CodegenDatabaseConfig.Mirrors
assert_pg_extension_declared(): if the marker is absent but aCodegenDatabaseConfigis present onmetadata.info, the config’s extension hooks are run eagerly so the check can succeed.- Parameters:
metadata (
MetaData) – TheMetaDatato check.- Raises:
CodegenDatabaseValidationError – If the extension is not declared and cannot be resolved from the registered config.
- Return type:
codegen_database_date_bin polyfill: date_bin with month/year strides.
PostgreSQL’s built-in date_bin(stride interval, ts timestamptz,
origin timestamptz) refuses strides that contain units of month or
larger. The reason is implementation-driven — date_bin works
by subtracting ts and origin and flooring that interval by
stride, which requires uniform-length strides, and months are
28-31 days.
This module registers an SQL function codegen_database_date_bin that
bridges the gap by falling through to date_bin for sub-month
strides and switching to calendar arithmetic for whole-month strides
(including quarter and year). Mixed strides (e.g. '1 month 3
days') are rejected by returning NULL.
The helper is used by the ledger chart functions in
codegen_database.ext.ledger.chart_functions, which accept an interval
p_period parameter. It lives under codegen_database.ext.chart rather
than the ledger because ChartExtension is
what installs it during configure_metadata; the ledger merely
calls into it.
- CODEGEN_DATABASE_DATE_BIN_DEFAULT_ORIGIN = "'2000-01-03'::timestamptz"¶
Default origin used by the polyfill (a Monday at midnight local).
'2000-01-03'was chosen so that week strides align to Monday (ISO-style), day/hour strides align to midnight, and month/quarter/ year strides align to the 1st of the month (because the month path snaps viadate_trunc('month', origin)).
- CODEGEN_DATABASE_DATE_BIN_NAME = 'codegen_database_date_bin'¶
Canonical unqualified name of the polyfill function.
- construct_date_bin_function(*, name='codegen_database_date_bin')[source]¶
Build a
codegen_database_date_binfunction spec.Returns a
CodegenDatabaseFunctionSpec. Register the returned spec once per schema that needs it (typically wherever a ledger chart function lives). The ledger chart functions call<schema>.codegen_database_date_bin(p_period, created_at)with the chart function’s own schema by default.Usage:
class DateBin(CodegenDatabaseFunctionMixin, Base): __table_args__ = {"schema": "ops"} __funcspec__ = construct_date_bin_function()
Signature:
codegen_database_date_bin( stride interval, ts timestamptz, origin timestamptz DEFAULT '2000-01-03'::timestamptz ) RETURNS timestamptz
- Semantics:
stridecontains only sub-month units → delegate to Postgres’s built-indate_bin.strideis a whole number of months (so includes quarter and year) → calendar arithmetic, aligned to the 1st of the month.stridemixes month and sub-month units → returnsNULL(no uniform bucketing possible).
- Parameters:
name (
str) – Unqualified function name. Defaults to"codegen_database_date_bin". Chart functions will look for the helper under this name, so override only if you know what you are doing.- Return type:
- Returns:
A
CodegenDatabaseFunctionSpecready to pass toCodegenDatabaseFunctionor assign to__funcspec__on aCodegenDatabaseFunctionMixinsubclass.
Audit¶
Audit/history query helpers.
These helpers return SQLAlchemy Selects without
registering anything on metadata. Pass the result to
CodegenDatabasePlainView (or
CodegenDatabaseMaterializedView) to
expose it as a view, or execute it directly against a connection.
Two dimension layouts are supported, each with its own pair of helpers:
Append-only (SCD Type 2) –
construct_append_only_diff_query(),construct_append_only_changed_between_query(). Each row is a full version. ForCodegenDatabaseAppendOnlyfactories, passfactory.ctx["attributes"].EAV –
construct_eav_diff_query(),construct_eav_changed_between_query(). Each row is one attribute’s value at a point in time. ForCodegenDatabaseEAVfactories, passfactory.ctx["attribute"].
- class ActivityViewPlugin[source]¶
Generate a
{table_name}_activitychangeset view.Auto-included by both append-only and EAV factories. Detects which backing table is present (
attributesfor append-only,attributefor EAV) and builds the activity view accordingly.The view is keyed by the backing table’s
BigIntegersurrogateidso keyset pagination works – the entity UUID repeats across versions and can’t page.
- construct_append_only_activity_query(table, *, key_cols, tracked_cols=None, ts_col='created_at')[source]¶
Unified activity stream over an append-only (SCD Type 2) table.
Wraps the same
LAG()partitioned by key_cols thatconstruct_append_only_diff_query()uses, then unpivots the wide<col>_before/<col>_afterpairs into one row per changed field – so a transition that touched three columns yields three activity rows (one per field) rather than one wide row. Each row carriesactor = NULL(Path A) and achange_typeof"create"(first version of the key,before IS NULL) or"update".Generates (conceptually):
WITH lagged AS ( SELECT <key...>, <ts> AS changed_at, lag(<col1>) OVER w AS <col1>_before, <col1> AS <col1>_after, ... FROM <table> WINDOW w AS (PARTITION BY <key...> ORDER BY <ts>) ) SELECT <key...>, changed_at, NULL AS actor, '<col1>' AS field, <col1>_before::text AS before, <col1>_after::text AS after, CASE WHEN <col1>_before IS NULL THEN 'create' ELSE 'update' END AS change_type FROM lagged WHERE <col1>_before IS DISTINCT FROM <col1>_after UNION ALL SELECT ... '<col2>' ... WHERE <col2>_before IS DISTINCT FROM <col2>_after ... ORDER BY <key...>, changed_at, field
- Parameters:
table (
FromClause) – The append-only history table.key_cols (
list[str]) – Column names identifying the audited entity (theLAGpartition).tracked_cols (
list[str] |None) – Columns whose transitions to surface. Defaults to every non-key, non-timestamp, non-PK column.ts_col (
str) – Timestamp column used for ordering. Defaults to"created_at".
- Return type:
- Returns:
A SQLAlchemy
Selectwith columns(*key_cols, changed_at, actor, field, before, after, change_type).- Raises:
CodegenDatabaseValidationError – If key_cols is empty or a referenced column is missing.
- construct_append_only_changed_between_query(table, *, key_cols, start, end, tracked_cols=None, ts_col='created_at')[source]¶
Net-change query over a window on an append-only history table.
Companion to
construct_append_only_diff_query(). For each key, compares state-as-of start against state-as-of end (both inclusive). Intermediate versions inside the window are collapsed away – only the net delta is returned. One row per key that changed (including keys created or last-seen inside the window, which appear withNULLon the missing side). Seeconstruct_eav_changed_between_query()for the EAV equivalent.Generates (conceptually):
WITH before_ AS ( SELECT DISTINCT ON (<key...>) <key...>, <col...> FROM <table> WHERE <ts> <= :start ORDER BY <key...>, <ts> DESC ), after_ AS ( SELECT DISTINCT ON (<key...>) <key...>, <col...> FROM <table> WHERE <ts> <= :end ORDER BY <key...>, <ts> DESC ) SELECT COALESCE(a.<key>, b.<key>) AS <key>, ..., b.<col> AS <col>_before, a.<col> AS <col>_after, ... FROM after_ a FULL JOIN before_ b USING (<key...>) WHERE b.<col> IS DISTINCT FROM a.<col> OR ...
- Parameters:
table (
FromClause) – The append-only history table.key_cols (
list[str]) – Column names identifying the audited entity.start (
datetime|ColumnElement[Any]) – Lower bound of the window (inclusive). Adatetimeor SQL expression.end (
datetime|ColumnElement[Any]) – Upper bound of the window (inclusive).tracked_cols (
list[str] |None) – Columns whose net change to surface. Defaults to every non-key, non-timestamp, non-PK column.ts_col (
str) – Timestamp column. Defaults to"created_at".
- Return type:
- Returns:
A SQLAlchemy
Selectwith columns(*key_cols, <col>_before, <col>_after, ...).- Raises:
CodegenDatabaseValidationError – If key_cols is empty, a referenced column is missing, or tracked_cols resolves to empty.
- construct_append_only_diff_query(table, *, key_cols, tracked_cols=None, ts_col='created_at')[source]¶
Per-transition diff over an append-only (SCD Type 2) history table.
Works on any table where each row represents a full version of an entity – typically the
attributesbacking table of aCodegenDatabaseAppendOnlyfactory (factory.ctx["attributes"]), or a hand-rolled append-only log. Seeconstruct_eav_diff_query()for the EAV equivalent.Uses
LAG()partitioned by key_cols ordered by ts_col to pair each row with its predecessor. Emits one row per transition where at least one tracked column changed (IS DISTINCT FROM).The first row per key is included as a creation event: its
*_beforecolumns areNULLand*_afterhold the initial values.Generates (conceptually):
SELECT <key...>, <ts> AS changed_at, LAG(<col1>) OVER w AS <col1>_before, <col1> AS <col1>_after, LAG(<col2>) OVER w AS <col2>_before, <col2> AS <col2>_after, ... FROM <table> WINDOW w AS (PARTITION BY <key...> ORDER BY <ts>) WHERE <col1>_before IS DISTINCT FROM <col1>_after OR <col2>_before IS DISTINCT FROM <col2>_after OR ... ORDER BY <key...>, changed_at
- Parameters:
table (
FromClause) – The append-only history table.key_cols (
list[str]) – Column names identifying the audited entity (the partition forLAG). Must be non-empty.tracked_cols (
list[str] |None) – Columns whose transitions to surface. Defaults to every column that is not in key_cols, not the timestamp, and not part of the primary key.ts_col (
str) – Timestamp column used for ordering. Defaults to"created_at".
- Return type:
- Returns:
A SQLAlchemy
Selectwith columns(*key_cols, changed_at, <col>_before, <col>_after, ...).- Raises:
CodegenDatabaseValidationError – If key_cols is empty, a referenced column is missing, or tracked_cols resolves to empty.
- construct_eav_activity_query(table, *, entity_col='entity_id', attribute_col='attribute_name', value_cols=None, attributes=None, ts_col='created_at')[source]¶
Unified activity stream over an EAV attribute log.
The EAV diff is already long (one row per
(entity, attribute)transition), so this wrapsconstruct_eav_diff_query()and adds the two missing unified columns:actor = NULL(Path A) andchange_type("create"for the first observation of an attribute,"update"otherwise).- Parameters:
table (
FromClause) – The EAV attribute log.entity_col (
str) – Column naming the entity. Defaults to"entity_id".attribute_col (
str) – Column naming the attribute. Defaults to"attribute_name".value_cols (
list[str] |None) – The typed value columns. Defaults to every column ending in"_value".attributes (
list[str] |None) – Optional list of attribute names to restrict to.ts_col (
str) – Timestamp column. Defaults to"created_at".
- Return type:
- Returns:
A SQLAlchemy
Selectwith columns(<entity_col>, <attribute_col>, changed_at, actor, field, before, after, change_type).fieldmirrors the attribute name so the column layout matches the append-only helper.- Raises:
CodegenDatabaseValidationError – If a referenced column is missing.
- construct_eav_changed_between_query(table, *, start, end, entity_col='entity_id', attribute_col='attribute_name', value_cols=None, attributes=None, ts_col='created_at')[source]¶
Net-change query over a window on an EAV attribute log.
Companion to
construct_eav_diff_query(). For each(entity, attribute)pair, compares value-as-of start against value-as-of end (both inclusive) via twoDISTINCT ONsnapshots joined with aFULL JOIN. One row per pair whose value differs; pairs introduced inside the window appear withvalue_before = NULL.- Parameters:
table (
FromClause) – The EAV attribute log.start (
datetime|ColumnElement[Any]) – Lower bound of the window (inclusive).end (
datetime|ColumnElement[Any]) – Upper bound of the window (inclusive).entity_col (
str) – Column naming the entity.attribute_col (
str) – Column naming the attribute.value_cols (
list[str] |None) – Typed value columns; defaults to*_value.attributes (
list[str] |None) – Optional attribute-name filter.ts_col (
str) – Timestamp column.
- Return type:
- Returns:
A SQLAlchemy
Selectwith columns(<entity_col>, <attribute_col>, value_before, value_after).- Raises:
CodegenDatabaseValidationError – If a referenced column is missing or
value_colsresolves to empty.
- construct_eav_diff_query(table, *, entity_col='entity_id', attribute_col='attribute_name', value_cols=None, attributes=None, ts_col='created_at')[source]¶
Per-attribute diff over an EAV attribute log.
Works on any table shaped like the codegen_database EAV
attributetable: one row per (entity, attribute) value observation. For aCodegenDatabaseEAVfactory, passfactory.ctx["attribute"]. Seeconstruct_append_only_diff_query()for the append-only equivalent.Uses
LAG()partitioned by(entity, attribute)ordered by ts_col. The first observation per(entity, attribute)comes through withvalue_before = NULL(a “first seen” event).All value columns are coalesced into a single
text-typed expression. EAV rows carry exactly one non-null value, so the coalesce is loss-free modulo formatting.Generates (conceptually):
SELECT entity, attribute, <ts> AS changed_at, LAG(value) OVER w AS value_before, value AS value_after FROM ( SELECT <entity>, <attribute>, <ts>, COALESCE(<v1>::text, <v2>::text, ...) AS value FROM <table> [WHERE <attribute> IN :attributes] ) t WINDOW w AS (PARTITION BY entity, attribute ORDER BY <ts>) WHERE value_before IS DISTINCT FROM value_after ORDER BY entity, attribute, changed_at
- Parameters:
table (
FromClause) – The EAV attribute log.entity_col (
str) – Column naming the entity. Defaults to"entity_id".attribute_col (
str) – Column naming the attribute. Defaults to"attribute_name".value_cols (
list[str] |None) – The typed value columns. Defaults to every column ending in"_value".attributes (
list[str] |None) – Optional list of attribute names to restrict the diff to.Nonemeans all attributes.ts_col (
str) – Timestamp column. Defaults to"created_at".
- Return type:
- Returns:
A SQLAlchemy
Selectwith columns(<entity_col>, <attribute_col>, changed_at, value_before, value_after).- Raises:
CodegenDatabaseValidationError – If a referenced column is missing or
value_colsresolves to empty.
Query builders for history/audit tables.
Pure-Python helpers that return SQLAlchemy selects. Two dimension layouts are supported:
Append-only (SCD Type 2): each version is a full row, keyed by the dimension’s business columns. Use
construct_append_only_diff_query()andconstruct_append_only_changed_between_query(). For aCodegenDatabaseAppendOnlyfactory, passfactory.ctx["attributes"].EAV (Entity-Attribute-Value): each row is a single attribute’s value at a point in time. Use
construct_eav_diff_query()andconstruct_eav_changed_between_query(). For aCodegenDatabaseEAVfactory, passfactory.ctx["attribute"].
All helpers emit typed <col>_before / <col>_after (or
value_before / value_after for EAV) pairs. No JSONB.
- construct_append_only_changed_between_query(table, *, key_cols, start, end, tracked_cols=None, ts_col='created_at')[source]¶
Net-change query over a window on an append-only history table.
Companion to
construct_append_only_diff_query(). For each key, compares state-as-of start against state-as-of end (both inclusive). Intermediate versions inside the window are collapsed away – only the net delta is returned. One row per key that changed (including keys created or last-seen inside the window, which appear withNULLon the missing side). Seeconstruct_eav_changed_between_query()for the EAV equivalent.Generates (conceptually):
WITH before_ AS ( SELECT DISTINCT ON (<key...>) <key...>, <col...> FROM <table> WHERE <ts> <= :start ORDER BY <key...>, <ts> DESC ), after_ AS ( SELECT DISTINCT ON (<key...>) <key...>, <col...> FROM <table> WHERE <ts> <= :end ORDER BY <key...>, <ts> DESC ) SELECT COALESCE(a.<key>, b.<key>) AS <key>, ..., b.<col> AS <col>_before, a.<col> AS <col>_after, ... FROM after_ a FULL JOIN before_ b USING (<key...>) WHERE b.<col> IS DISTINCT FROM a.<col> OR ...
- Parameters:
table (
FromClause) – The append-only history table.key_cols (
list[str]) – Column names identifying the audited entity.start (
datetime|ColumnElement[Any]) – Lower bound of the window (inclusive). Adatetimeor SQL expression.end (
datetime|ColumnElement[Any]) – Upper bound of the window (inclusive).tracked_cols (
list[str] |None) – Columns whose net change to surface. Defaults to every non-key, non-timestamp, non-PK column.ts_col (
str) – Timestamp column. Defaults to"created_at".
- Return type:
- Returns:
A SQLAlchemy
Selectwith columns(*key_cols, <col>_before, <col>_after, ...).- Raises:
CodegenDatabaseValidationError – If key_cols is empty, a referenced column is missing, or tracked_cols resolves to empty.
- construct_append_only_diff_query(table, *, key_cols, tracked_cols=None, ts_col='created_at')[source]¶
Per-transition diff over an append-only (SCD Type 2) history table.
Works on any table where each row represents a full version of an entity – typically the
attributesbacking table of aCodegenDatabaseAppendOnlyfactory (factory.ctx["attributes"]), or a hand-rolled append-only log. Seeconstruct_eav_diff_query()for the EAV equivalent.Uses
LAG()partitioned by key_cols ordered by ts_col to pair each row with its predecessor. Emits one row per transition where at least one tracked column changed (IS DISTINCT FROM).The first row per key is included as a creation event: its
*_beforecolumns areNULLand*_afterhold the initial values.Generates (conceptually):
SELECT <key...>, <ts> AS changed_at, LAG(<col1>) OVER w AS <col1>_before, <col1> AS <col1>_after, LAG(<col2>) OVER w AS <col2>_before, <col2> AS <col2>_after, ... FROM <table> WINDOW w AS (PARTITION BY <key...> ORDER BY <ts>) WHERE <col1>_before IS DISTINCT FROM <col1>_after OR <col2>_before IS DISTINCT FROM <col2>_after OR ... ORDER BY <key...>, changed_at
- Parameters:
table (
FromClause) – The append-only history table.key_cols (
list[str]) – Column names identifying the audited entity (the partition forLAG). Must be non-empty.tracked_cols (
list[str] |None) – Columns whose transitions to surface. Defaults to every column that is not in key_cols, not the timestamp, and not part of the primary key.ts_col (
str) – Timestamp column used for ordering. Defaults to"created_at".
- Return type:
- Returns:
A SQLAlchemy
Selectwith columns(*key_cols, changed_at, <col>_before, <col>_after, ...).- Raises:
CodegenDatabaseValidationError – If key_cols is empty, a referenced column is missing, or tracked_cols resolves to empty.
- construct_eav_changed_between_query(table, *, start, end, entity_col='entity_id', attribute_col='attribute_name', value_cols=None, attributes=None, ts_col='created_at')[source]¶
Net-change query over a window on an EAV attribute log.
Companion to
construct_eav_diff_query(). For each(entity, attribute)pair, compares value-as-of start against value-as-of end (both inclusive) via twoDISTINCT ONsnapshots joined with aFULL JOIN. One row per pair whose value differs; pairs introduced inside the window appear withvalue_before = NULL.- Parameters:
table (
FromClause) – The EAV attribute log.start (
datetime|ColumnElement[Any]) – Lower bound of the window (inclusive).end (
datetime|ColumnElement[Any]) – Upper bound of the window (inclusive).entity_col (
str) – Column naming the entity.attribute_col (
str) – Column naming the attribute.value_cols (
list[str] |None) – Typed value columns; defaults to*_value.attributes (
list[str] |None) – Optional attribute-name filter.ts_col (
str) – Timestamp column.
- Return type:
- Returns:
A SQLAlchemy
Selectwith columns(<entity_col>, <attribute_col>, value_before, value_after).- Raises:
CodegenDatabaseValidationError – If a referenced column is missing or
value_colsresolves to empty.
- construct_eav_diff_query(table, *, entity_col='entity_id', attribute_col='attribute_name', value_cols=None, attributes=None, ts_col='created_at')[source]¶
Per-attribute diff over an EAV attribute log.
Works on any table shaped like the codegen_database EAV
attributetable: one row per (entity, attribute) value observation. For aCodegenDatabaseEAVfactory, passfactory.ctx["attribute"]. Seeconstruct_append_only_diff_query()for the append-only equivalent.Uses
LAG()partitioned by(entity, attribute)ordered by ts_col. The first observation per(entity, attribute)comes through withvalue_before = NULL(a “first seen” event).All value columns are coalesced into a single
text-typed expression. EAV rows carry exactly one non-null value, so the coalesce is loss-free modulo formatting.Generates (conceptually):
SELECT entity, attribute, <ts> AS changed_at, LAG(value) OVER w AS value_before, value AS value_after FROM ( SELECT <entity>, <attribute>, <ts>, COALESCE(<v1>::text, <v2>::text, ...) AS value FROM <table> [WHERE <attribute> IN :attributes] ) t WINDOW w AS (PARTITION BY entity, attribute ORDER BY <ts>) WHERE value_before IS DISTINCT FROM value_after ORDER BY entity, attribute, changed_at
- Parameters:
table (
FromClause) – The EAV attribute log.entity_col (
str) – Column naming the entity. Defaults to"entity_id".attribute_col (
str) – Column naming the attribute. Defaults to"attribute_name".value_cols (
list[str] |None) – The typed value columns. Defaults to every column ending in"_value".attributes (
list[str] |None) – Optional list of attribute names to restrict the diff to.Nonemeans all attributes.ts_col (
str) – Timestamp column. Defaults to"created_at".
- Return type:
- Returns:
A SQLAlchemy
Selectwith columns(<entity_col>, <attribute_col>, changed_at, value_before, value_after).- Raises:
CodegenDatabaseValidationError – If a referenced column is missing or
value_colsresolves to empty.
Unified activity/audit query helpers.
Companion to codegen_database.ext.audit.queries. Where the
construct_*_diff_query helpers emit the raw per-table transition
shape (wide <col>_before / <col>_after pairs for append-only,
long value_before / value_after for EAV), these helpers fold
both layouts into one uniform activity row:
(key..., changed_at, actor, field, before, after, change_type)
changed_at– when the transition happened.actor– who made it.NULLfor historical rows: today’s append-only / EAV backing tables carry onlycreated_at, no actor column (Path A – non-invasive). Forward-only capture plugs in here once a resource records an actor.field– the column (append-only) or attribute name (EAV) that changed.before/after– the prior and new values, cast totextfor a uniform wire shape (the FE renders them as strings).change_type–"create"for the first observation of a key / attribute (before IS NULL),"update"otherwise.
Both helpers return a SQLAlchemy Select without
registering anything on metadata. Execute directly against a
connection.
- construct_append_only_activity_query(table, *, key_cols, tracked_cols=None, ts_col='created_at')[source]¶
Unified activity stream over an append-only (SCD Type 2) table.
Wraps the same
LAG()partitioned by key_cols thatconstruct_append_only_diff_query()uses, then unpivots the wide<col>_before/<col>_afterpairs into one row per changed field – so a transition that touched three columns yields three activity rows (one per field) rather than one wide row. Each row carriesactor = NULL(Path A) and achange_typeof"create"(first version of the key,before IS NULL) or"update".Generates (conceptually):
WITH lagged AS ( SELECT <key...>, <ts> AS changed_at, lag(<col1>) OVER w AS <col1>_before, <col1> AS <col1>_after, ... FROM <table> WINDOW w AS (PARTITION BY <key...> ORDER BY <ts>) ) SELECT <key...>, changed_at, NULL AS actor, '<col1>' AS field, <col1>_before::text AS before, <col1>_after::text AS after, CASE WHEN <col1>_before IS NULL THEN 'create' ELSE 'update' END AS change_type FROM lagged WHERE <col1>_before IS DISTINCT FROM <col1>_after UNION ALL SELECT ... '<col2>' ... WHERE <col2>_before IS DISTINCT FROM <col2>_after ... ORDER BY <key...>, changed_at, field
- Parameters:
table (
FromClause) – The append-only history table.key_cols (
list[str]) – Column names identifying the audited entity (theLAGpartition).tracked_cols (
list[str] |None) – Columns whose transitions to surface. Defaults to every non-key, non-timestamp, non-PK column.ts_col (
str) – Timestamp column used for ordering. Defaults to"created_at".
- Return type:
- Returns:
A SQLAlchemy
Selectwith columns(*key_cols, changed_at, actor, field, before, after, change_type).- Raises:
CodegenDatabaseValidationError – If key_cols is empty or a referenced column is missing.
- construct_eav_activity_query(table, *, entity_col='entity_id', attribute_col='attribute_name', value_cols=None, attributes=None, ts_col='created_at')[source]¶
Unified activity stream over an EAV attribute log.
The EAV diff is already long (one row per
(entity, attribute)transition), so this wrapsconstruct_eav_diff_query()and adds the two missing unified columns:actor = NULL(Path A) andchange_type("create"for the first observation of an attribute,"update"otherwise).- Parameters:
table (
FromClause) – The EAV attribute log.entity_col (
str) – Column naming the entity. Defaults to"entity_id".attribute_col (
str) – Column naming the attribute. Defaults to"attribute_name".value_cols (
list[str] |None) – The typed value columns. Defaults to every column ending in"_value".attributes (
list[str] |None) – Optional list of attribute names to restrict to.ts_col (
str) – Timestamp column. Defaults to"created_at".
- Return type:
- Returns:
A SQLAlchemy
Selectwith columns(<entity_col>, <attribute_col>, changed_at, actor, field, before, after, change_type).fieldmirrors the attribute name so the column layout matches the append-only helper.- Raises:
CodegenDatabaseValidationError – If a referenced column is missing.
Comments¶
Comment query helpers.
Pure-Python helpers that return SQLAlchemy selects. One helper is exposed:
construct_comments_thread_query()– the flat, parent-first row stream for one(resource, row), ready to feedfsh_lib.comments.build_comment_thread().
Pair with fsh_lib.comments.CommentMixin for the storage
columns.
- construct_comments_thread_query(table, *, resource_type, resource_id, resource_type_col='resource_type', resource_id_col='resource_id', parent_col='parent_comment_id', created_at_col='created_at')[source]¶
Flat, parent-first comment stream for one
(resource, row).Filters the comments table to resource_type / resource_id and orders so every parent precedes its children: top-level comments (
parent_comment_id IS NULL) first bycreated_at, then each parent’s replies bycreated_at. Feed the result tofsh_lib.comments.build_comment_thread()to nest it.The ordering groups top-level comments (
parent_comment_id IS NULL) ahead of replies, and within each group sorts bycreated_at.fsh_lib.comments.build_comment_thread()nests the flat stream into a tree from an id-keyed dict, so the only ordering that matters for the tree is the order of a parent’s replies – which this sort keeps chronological. This avoids a recursive CTE for the common one-level-deep thread shape.Generates (conceptually):
SELECT * FROM <table> WHERE resource_type = :resource_type AND resource_id = :resource_id ORDER BY parent_comment_id NULLS FIRST, created_at
- Parameters:
table (
Table) – The comments table.resource_type (
str) – Slug of the parent resource to filter on.resource_id (
str) – Stringified id of the parent row to filter on.resource_type_col (
str) – Column holding the resource slug.resource_id_col (
str) – Column holding the parent row’s id.parent_col (
str) – Self-referential parent-comment column.created_at_col (
str) – Timestamp column for child ordering.
- Return type:
- Returns:
A SQLAlchemy
Selectover every column of table, filtered and ordered for threading.- Raises:
CodegenDatabaseValidationError – If a referenced column is missing.
Threaded comment query helpers.
These helpers return SQLAlchemy Selects without
registering anything on metadata. Execute the result directly against
a connection. Pair with
fsh_lib.comments.CommentMixin (the storage columns) and
fsh_lib.comments.build_comment_thread() (the pure-Python
nester) to serve a threaded comment list for any opted-in resource.
construct_comments_thread_query() returns the flat, parent-first
row stream fsh_lib.comments.build_comment_thread() nests into a
tree. The ordering is structural (every parent precedes its
children) so the nester never has to look ahead.
- construct_comments_thread_query(table, *, resource_type, resource_id, resource_type_col='resource_type', resource_id_col='resource_id', parent_col='parent_comment_id', created_at_col='created_at')[source]¶
Flat, parent-first comment stream for one
(resource, row).Filters the comments table to resource_type / resource_id and orders so every parent precedes its children: top-level comments (
parent_comment_id IS NULL) first bycreated_at, then each parent’s replies bycreated_at. Feed the result tofsh_lib.comments.build_comment_thread()to nest it.The ordering groups top-level comments (
parent_comment_id IS NULL) ahead of replies, and within each group sorts bycreated_at.fsh_lib.comments.build_comment_thread()nests the flat stream into a tree from an id-keyed dict, so the only ordering that matters for the tree is the order of a parent’s replies – which this sort keeps chronological. This avoids a recursive CTE for the common one-level-deep thread shape.Generates (conceptually):
SELECT * FROM <table> WHERE resource_type = :resource_type AND resource_id = :resource_id ORDER BY parent_comment_id NULLS FIRST, created_at
- Parameters:
table (
Table) – The comments table.resource_type (
str) – Slug of the parent resource to filter on.resource_id (
str) – Stringified id of the parent row to filter on.resource_type_col (
str) – Column holding the resource slug.resource_id_col (
str) – Column holding the parent row’s id.parent_col (
str) – Self-referential parent-comment column.created_at_col (
str) – Timestamp column for child ordering.
- Return type:
- Returns:
A SQLAlchemy
Selectover every column of table, filtered and ordered for threading.- Raises:
CodegenDatabaseValidationError – If a referenced column is missing.
State machines¶
Ledger-backed state machine factory.
- class CodegenDatabaseStateMachine(name, schemaname, metadata, subject_column, states, valid_transitions, extra_columns=None, *, actor_column=None, actor_fk=None, allow_initial=True)[source]¶
Ledger-backed state machine with transition validation.
Creates an append-only transitions table, a full history view, and a current-state view. A
BEFORE INSERTtrigger validates that each new transition is:Declared in valid_transitions.
Consistent with the subject’s actual current state.
The valid transitions are compiled into the trigger function body at migration time, so changing them requires a migration.
- Parameters:
name (
str) – Base name for all generated objects.schemaname (
str) – PostgreSQL schema for all objects.metadata (
MetaData) – SQLAlchemyMetaDatato register on.subject_column (
Column) –Columnthat identifies the entity being tracked. UseCodegenDatabaseForeignKeyto reference the subject’s table. Must benullable=False.valid_transitions (
list[tuple[str|None,str]]) – List of(from_state, to_state)pairs. UseNoneas from_state for the initial transition.extra_columns (
list[Column] |None) – Additional columns on the raw table (e.g.changed_by,notes).allow_initial (
bool) – IfTrue(default), a row withfrom_state = NULLis allowed as the first transition for a subject. Set toFalseto require an explicit initial state defined in valid_transitions.
- Raises:
CodegenDatabaseValidationError – If states or valid_transitions is empty, or if a transition references an undeclared state.
- can_transition_to(from_state, to_state)[source]¶
Return whether
from_state -> to_stateis a legal transition.
- async transition_many(db, *, subjects, to_state, **extras)[source]¶
Bulk-transition every subject in subjects to to_state.
Issues a single
INSERT INTO raw (subject, from_state, to_state, ...) SELECT ... FROM current_viewafter locking the latest raw row per affected subject – mirroring whattransition_to()does for a single subject so concurrent single-row and bulk callers serialize against each other.The set of legal current states is auto-derived from the SM’s declared transitions: only subjects whose current state has a registered
(from, to_state)edge are advanced. Subjects in any other state are silently skipped. This is the same set the BEFORE INSERT trigger would accept, applied as aWHEREfilter so the trigger never has to reject rows.The BEFORE INSERT trigger validates each emitted row, so a misconfigured
to_state(one that’s not a valid target from any state) results in zero rows inserted – not an error.AsyncSession only. Unlike
transition_to()andcall(), this method issues two statements (theFOR UPDATElock and theINSERT ... SELECT) and awaits both internally – so it cannot dispatch transparently through a syncSession. If you need a sync variant, run the two statements manually with the helpers exposed onself.tableandself.current_view.- Parameters:
db (
Any) – AnAsyncSession.subjects (
Select|Iterable[Any]) – A SQLAlchemySelectthat yields subject ids, or any iterable of ids (UUIDs, ints, etc.). Iterables are bound as aVALUESlist; aSelectis correlated as a subquery so callers can compose against other tables (select(LineItem.id).where(...)).to_state (
Any) – Target state.enum.Enumvalues are coerced via.value.**extras (
Any) – Values for the SM’s extra columns (actor_id,note, …). Each value is applied to every row.Enumvalues are coerced.
- Return type:
- Returns:
The
Resultfrom theINSERT ... RETURNING– iterate.scalars()for the subject ids that actually transitioned.
- transition_to(db, **kwargs)[source]¶
Invoke the SM’s generated
transition_toSQL function.Builds
select(func.<schema>.<schema>_<name>_transition_to( ...))and dispatches viadb.execute(...). The SQL function locks the latest raw row for the subject, reads the actual current state, and inserts a new row withfrom_state = <current>– the BEFORE INSERT trigger then validates the(from_state, to_state)pair.Kwargs are keyed by the subject column’s name (e.g.
line_item_idfor a state machine whose subject is a line item),to_state, plus any extra-column names declared on the SM.enum.Enumvalues are coerced to.valueso callers can pass typed enums directly.- Parameters:
db (
Any) – Any object with anexecute(stmt)method –Session,AsyncSession, orConnection. codegen_database does no awaiting itself; ifexecutereturns an awaitable,awaitthe return value at the call site.**kwargs (
Any) –<subject_col_name>,to_state, and any extra-column kwargs declared on the SM.
- Return type:
- Returns:
Whatever
db.execute(stmt)returns – aResultfor sync sessions, anAwaitable[Result]for async ones.- Raises:
KeyError – If the subject or
to_statekwarg is missing.
- class StateMachineCurrentStatePlugin(*, sm, column_type, column_name='status')[source]¶
Maintain a denormalized, read-only current-state column.
- class Deal(Base):
- __plugins__ = [
UUIDV7PKPlugin(), StateMachineCurrentStatePlugin(
sm=deal_sm, column_type=DealStatus
),
]
status: Mapped[DealStatus]
- Parameters:
sm (
CodegenDatabaseStateMachine) – TheCodegenDatabaseStateMachinewhose current state is mirrored onto the subject.column_type (
type[Enum]) – Theenum.Enumthe SM’s states belong to.column_name (
str) – Name of the current-state column on the subject (default"status").
- construct_state_machine_current_query(sm)[source]¶
Return a select against the current-state view.
- Parameters:
sm (
CodegenDatabaseStateMachine) – ACodegenDatabaseStateMachineinstance.- Return type:
- Returns:
A SQLAlchemy
Selectover{name}_current.
- construct_state_machine_history_query(sm, subject_id=None)[source]¶
Return all transitions, optionally filtered to one subject.
- Parameters:
sm (
CodegenDatabaseStateMachine) – ACodegenDatabaseStateMachineinstance.subject_id (
object) – When provided, adds aWHERE subject_col = subject_idfilter.
- Return type:
- Returns:
A SQLAlchemy
Select.
Incremental refresh¶
Generic incremental refresh plugin.
IncrementalRefreshPlugin registers a checkpoint-based
{schema}_{tablename}_refresh(p_since TIMESTAMPTZ DEFAULT NULL)
function in PostgreSQL. Callers invoke this function from pg_cron
(or manually after bulk loads) to roll forward an aggregate table.
The refresh function resolves the starting cutoff as:
_since = COALESCE(p_since, fallback_sql, '-infinity'::TIMESTAMPTZ)
where fallback_sql is a SQL expression (supplied by the caller) that reads the last-processed watermark, e.g.:
SELECT MIN(updated_at) - INTERVAL '1 second' FROM my_snapshot
The user-supplied body is PL/pgSQL that runs after _since is
resolved. It typically aggregates rows from the source table where
watermark_col >= _since and upserts results into an aggregate table.
Usage:
orders = CodegenDatabaseSimple(
"orders",
"app",
metadata,
schema_items=[
Column("customer_id", Integer, nullable=False),
Column("amount", Numeric, nullable=False),
],
extra_plugins=[
IncrementalRefreshPlugin(
body="""
INSERT INTO app.orders_daily_agg (customer_id, day, total)
SELECT customer_id,
date_trunc('day', created_at),
SUM(amount)
FROM app.orders_raw
WHERE created_at >= _since
GROUP BY 1, 2
ON CONFLICT (customer_id, day)
DO UPDATE SET total = orders_daily_agg.total + EXCLUDED.total;
""",
fallback_sql=(
"SELECT MIN(day::timestamptz)"
" - INTERVAL '1 second'"
" FROM app.orders_daily_agg"
),
schedule="*/5 * * * *",
),
],
)
# The above auto-registers a CronJob named "app.app_orders_refresh"
# that runs every 5 minutes. Alembic autogenerate emits:
# op.execute("SELECT cron.schedule(...)")
# Requires pg_cron to be installed. Register it alongside the
# factory:
# register_pg_extension(metadata, PGExtension("pg_cron", cascade=True))
- class IncrementalRefreshPlugin(body, fallback_sql, *, schedule=None, table_key='raw_table')[source]¶
Register a checkpoint-based incremental refresh function.
Generates and registers a PostgreSQL function with signature:
{schema}.{schema}_{tablename}_refresh( p_since TIMESTAMPTZ DEFAULT NULL ) RETURNS void
The function resolves
_sinceas the caller-supplied p_since, or falls back to the expression in fallback_sql. The user-supplied body is embedded after_sinceis resolved and can reference_sincedirectly.- Parameters:
body (
str) – PL/pgSQL fragment (noDECLAREorBEGIN/ENDwrapper). Must be valid PL/pgSQL and may reference_since TIMESTAMPTZ.fallback_sql (
str) – SQL expression that returns aTIMESTAMPTZused as the checkpoint when p_since isNULL. ANULLresult falls back to'-infinity'. Typically readsMIN(updated_at) - INTERVAL '1 second'from the aggregate table.table_key (
str) – Key inctxfor the source table (used only to satisfy the@requiresdependency). Default"raw_table".
Query builders for incremental refresh patterns.
construct_unincorporated_since_query() returns a SELECT for rows in the
source table that have not yet been rolled into an aggregate, based on
a watermark timestamp.
Usage:
# Find orders not yet incorporated into the daily aggregate.
q = construct_unincorporated_since_query(
orders,
watermark_col="created_at",
since="2024-06-01T00:00:00Z",
)
# Without a fixed timestamp: use a subquery as the checkpoint.
q = construct_unincorporated_since_query(
orders,
watermark_col="created_at",
since_subquery="SELECT MIN(day::timestamptz) FROM app.orders_daily_agg",
)
- construct_unincorporated_since_query(source, *, watermark_col='created_at', since=None, since_subquery=None, table_key='raw_table')[source]¶
Return rows in source not yet incorporated into an aggregate.
Generates:
SELECT * FROM <source_table> WHERE <watermark_col> >= <since>
where
<since>is either a literal timestamp or the result of a subquery.- Parameters:
source (
ContextSource) – A factory instance or declarative class with a populated context.watermark_col (
str) – Column in the source table to compare against the cutoff (default"created_at").since (
str|None) – ISO 8601 timestamp string used as the lower bound. Exactly one of since or since_subquery must be given.since_subquery (
str|None) – SQL expression (a scalar subquery) that returns aTIMESTAMPTZcutoff. Exactly one of since or since_subquery must be given.table_key (
str) – Key inctxfor the source table (default"raw_table").
- Return type:
- Returns:
A SQLAlchemy
Select.- Raises:
CodegenDatabaseValidationError – If neither or both of since and since_subquery are provided.
Cron¶
pg_cron extension for codegen_database.
PGCronExtension wires pg_cron support into the codegen_database
lifecycle. Register it on a
CodegenDatabaseConfig to
enable declarative cron job scheduling via
CronJob and
IncrementalRefreshPlugin.
Usage:
from codegen_database.config import CodegenDatabaseConfig
from codegen_database.ext.pg_cron import PGCronExtension
config = CodegenDatabaseConfig()
config.use(PGCronExtension())
After registration, any
CronJob stored in metadata
(e.g. by
IncrementalRefreshPlugin
with a schedule argument) will be diffed against the live cron.job
table during alembic revision --autogenerate. Alembic emits
op.execute("SELECT cron.schedule(...)") for new or changed jobs.
The extension also auto-declares the pg_cron PostgreSQL extension in
metadata so that the Alembic comparator can emit
CREATE EXTENSION IF NOT EXISTS pg_cron CASCADE when needed.
Convenience re-exports:
from codegen_database.ext.pg_cron import (
PGCronExtension,
CronJob,
register_cron_job,
PGExtension,
register_pg_extension,
)
- class CronJob(name, schedule, command, database=None)[source]¶
Describe a pg_cron job to register.
- Parameters:
- class PGCronExtension(name='pg_cron')[source]¶
Wire pg_cron support into the codegen_database lifecycle.
When registered on a
CodegenDatabaseConfig:configure_metadatadeclarespg_cronas a required PostgreSQL extension so Alembic can create it when absent.configure_alembicregisters the cron job comparator and renderer soalembic revision --autogenerateemitsSELECT cron.schedule(...)for new or changed jobs, and the extension comparator soCREATE EXTENSION IF NOT EXISTS pg_cron CASCADEis emitted when pg_cron is missing.
Plugins that use pg_cron (e.g.
IncrementalRefreshPluginwith ascheduleargument) callassert_pg_extension_declared()to verify this extension is registered, giving a clear error message when it is absent.- Parameters:
name (
str) – Extension name. Defaults to"pg_cron".
- class PGExtension(name, schema=None, cascade=False)[source]¶
Describe a PostgreSQL extension to install.
- Parameters:
- register_pg_extension(metadata, ext)[source]¶
Store ext in metadata for Alembic autogenerate.
- Parameters:
metadata (
MetaData) – SQLAlchemyMetaDatato register on.ext (
PGExtension) – The extension to register.
- Return type:
pg_cron job dataclasses and metadata registration.
- class CronJob(name, schedule, command, database=None)[source]¶
Describe a pg_cron job to register.
- Parameters:
- class CronJobs(jobs=<factory>)[source]¶
Container for all cron jobs registered on a MetaData.
Stored under
metadata.info["cron_jobs"]byregister_cron_job().
Alembic comparator ops and diff logic for pg_cron jobs.
- class ScheduleCronJobOp(job)[source]¶
Alembic operation: create or update a pg_cron job.
- reverse()[source]¶
Return a no-op downgrade: jobs are never dropped automatically.
cron.scheduleis an upsert so re-running it on downgrade is always safe. Users who want to remove a job on downgrade should add an explicitUnscheduleCronJobOpcall.- Return type:
- class UnscheduleCronJobOp(name)[source]¶
Alembic operation: remove a pg_cron job.
This op is available for manual migration authoring. The autogenerate comparator never emits it automatically to avoid accidentally removing externally-managed jobs.
- compare_cron_jobs(connection, desired)[source]¶
Diff desired cron jobs against the current database state.
Only emits
ScheduleCronJobOp(create or update) — jobs are never dropped automatically. UseUnscheduleCronJobOpin a hand-written migration to remove a job.- Parameters:
connection (
Connection) – Active database connection.desired (
CronJobs) – TheCronJobsholder read from metadata.
- Return type:
- Returns:
List of
ScheduleCronJobOpoperations to bring declared jobs into sync with the database.
PostGIS¶
PostGIS extension for codegen_database.
PostGISExtension wires PostGIS-suite support into the
codegen_database lifecycle. Register it on a
CodegenDatabaseConfig to enable the
PostGIS column types (currently
STDADDR, the stdaddr
composite produced by the address_standardizer extension).
Usage:
from codegen_database.config import CodegenDatabaseConfig
from codegen_database.ext.postgis import PostGISExtension
config = CodegenDatabaseConfig()
config.use(PostGISExtension())
PostGISExtension is the grouping for the whole PostGIS suite; which
PostgreSQL extensions it actually installs are controlled by boolean
flags. By default it declares only address_standardizer (which
provides stdaddr), since that installs independently of the heavy
postgis core. Enable the core when you need geometry:
config.use(PostGISExtension(postgis=True))
Enable the TIGER geocoder when you need to turn a free-form address
string into ranked candidate addresses with coordinates (the engine
behind fsh_lib.geocode.find_address_candidates()):
config.use(PostGISExtension(tiger_geocoder=True))
postgis_tiger_geocoder is declared CASCADE because it depends on
postgis and fuzzystrmatch; CASCADE installs those
automatically. (It does not pull in address_standardizer – that
is a sibling extension for STDADDR, installed by the default
address_standardizer flag above; the geocoder does not require it.)
The extension only ships the geocode machinery – it does not
load the TIGER census data the geocoder matches against. That data load
(Loader_Generate_Nation_Script / Loader_Generate_Census_Script
per state, US-only) is a separate operational step; geocode returns
no rows until it has run. See tiger-loader/ for the loader image
that performs it.
After registration the extension auto-declares the selected PostgreSQL
extensions in metadata, so the Alembic comparator emits
CREATE EXTENSION IF NOT EXISTS ... for any that are missing from the
database. Columns then use the type directly:
from sqlalchemy import Column
from codegen_database.ext.postgis import STDADDR, StdAddr
Column("address", STDADDR(), nullable=True)
# store StdAddr(house_num="123", name="MAIN", suftype="ST")
Convenience re-exports:
from codegen_database.ext.postgis import (
PostGISExtension,
STDADDR,
StdAddr,
PGExtension,
register_pg_extension,
)
- class PostGISExtension(name='postgis', postgis=False, address_standardizer=True, geography=False, tiger_geocoder=False)[source]¶
Wire PostGIS-suite support into the codegen_database lifecycle.
The extension groups the PostGIS suite; the boolean flags select which PostgreSQL extensions it installs.
- Parameters:
name (
str) – Extension name. Defaults to"postgis".address_standardizer (
bool) – Important forSTDADDRgeography (
bool) – Important forCOORDINATEpostgis (
bool) – Important for…… everything elsetiger_geocoder (
bool) – Important for free-string geocoding (geocode);CASCADEpulls in its dependencies.
Row-level security¶
Alembic autogenerate integration for RLS policies.
Call register_rls_alembic_events() in env.py alongside
alembic_hook() to enable
RLS policy diffing in Alembic autogenerate:
from codegen_database.alembic.register import alembic_hook
from codegen_database.ext.rls.alembic import register_rls_alembic_events
alembic_hook()
register_rls_alembic_events()
The comparator queries pg_policies and emits op.execute(...)
statements for any policies that need to be created or dropped.