Extensions

Extensions group metadata, Alembic, and CLI integration behind one named object. They do not modify ORM model construction.

from dataclasses import dataclass
from sqlalchemy import MetaData
from codegen_database import CodegenDatabaseExtension

@dataclass
class ExampleExtension(CodegenDatabaseExtension):
    name: str = "example"

    def configure_metadata(self, metadata: MetaData) -> None:
        metadata.info["example_enabled"] = True

Available hooks are configure_metadata(metadata), configure_alembic(), register_cli(app), and validate(registered_names). Dependencies are declared with a depends_on class variable. Applications that use extension CLI commands call codegen_database.cli.configure_cli(config) during CLI setup.

configure_metadata applies each registered extension to a given MetaData at most once: a second configure_metadata run on the same metadata is a no-op for extensions that already applied. Extensions must not assume they get a second chance to mutate a metadata.

Manual extensions retain declaration order and take precedence over discovered extensions with the same name. Discovered entry points are sorted by name. An entry point must load an extension class whose constructor accepts name=<entry-point name>:

[project.entry-points."codegen_database.ext"]
example = "my_package.extension:ExampleExtension"

Built-in extensions

from codegen_database import CodegenDatabaseConfig
from codegen_database.ext.chart import ChartExtension
from codegen_database.ext.ltree import LTreeExtension
from codegen_database.ext.pg_cron import PGCronExtension
from codegen_database.ext.postgis import PostGISExtension

config = CodegenDatabaseConfig(auto_discover=False).use(
    ChartExtension(),
    LTreeExtension(),
    PGCronExtension(),
    PostGISExtension(postgis=True),
)

Chart support registers the date-bin function in utility_schema. LTreeExtension registers the ltree extension in utility_schema. PGCronExtension registers pg_cron and its Alembic comparator. PostGIS flags register PostgreSQL extensions explicitly.

ltree columns

LTreeExtension registers the ltree extension in utility_schema so models can declare LtreeType columns (sqlalchemy_utils). It does not maintain path values itself; callers own their path maintenance – for example the entity registry in fsh-lib computes materialized paths from its ledger in a recursive query, with LTreeExtension guaranteeing the extension exists:

from sqlalchemy_utils import LtreeType

class TreeNode(Base):
    __tablename__ = "tree_node"

    id: Mapped[uuid.UUID] = mapped_column(primary_key=True)
    parent_id: Mapped[uuid.UUID | None] = mapped_column(
        ForeignKey("tree_node.id")
    )
    path: Mapped[Ltree | None] = mapped_column(LtreeType)

LTreeExtension also resolves which schema the extension lives in (resolve_ltree_schema); pass schema=... to pin it explicitly.

text_range columns

TextRangeExtension registers the textrange PostgreSQL range type (with its automatically-created textmultirange companion) and the record_key(text) normalization function, so applications can store key-encoded multiranges and answer containment lookups with @> backed by a GiST index. Requires PostgreSQL 14+ (multirange types).

record_key maps record identifiers like ABC-1 onto a natural, collation-independent sort key: alphabetic runs become 0 + run, numeric runs become 1 + nine zero-padded digits, and separators are preserved. ABC-1 normalizes to 0ABC-1000000001 and ABC-10 to 0ABC-1000000010, so record_key('ABC-1') < record_key('ABC-10') is true. The function is IMMUTABLE; SDE cannot declare STRICT, so the body replicates strictness by returning NULL for NULL input.

The record_keys column maps to TextMultiRangeType, whose Python value is a plain str: the canonical multirange literal that psycopg materializes the column as. Stored values are record_key outputs, so apps usually build them in SQL:

from codegen_database import CodegenDatabaseConfig
from codegen_database.ext.text_range import (
    TextMultiRangeType,
    TextRangeExtension,
)

config = CodegenDatabaseConfig(auto_discover=False).use(
    TextRangeExtension()
)

class Entity(Base):
    __tablename__ = "entities"
    __table_args__ = (
        Index(
            "ix_entities_record_keys",
            "record_keys",
            postgresql_using="gist",
        ),
    )

    id: Mapped[uuid.UUID] = mapped_column(primary_key=True)
    record_keys: Mapped[str | None] = mapped_column(TextMultiRangeType)

Containment lookups use the range constructor against record_key outputs, which keeps the comparison order-independent of the database collation. record_key resolves to utility_schema (like chart’s date_bin); call it qualified, or put utility_schema on the search path:

SELECT id
FROM entities
WHERE record_keys @> textrange(
    codegen_database.record_key('ABC-42'),
    codegen_database.record_key('ABC-42')
);

When the types live outside the default schema, pass schema=... to the extension (and to the column type, so column DDL qualifies the type name); the function moves to that schema too. Identifiers (schema, type names) are emitted unquoted, so PostgreSQL folds them to lowercase; use lowercase identifiers.