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.