Source code for codegen_database.ext.chart

"""Chart extension for codegen_database ledgers.

:class:`ChartExtension` wires the :func:`codegen_database_date_bin
<codegen_database.ext.chart.date_bin.construct_date_bin_function>` polyfill
into the codegen_database lifecycle.  Register it on a
:class:`~codegen_database.config.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
:class:`~codegen_database.errors.CodegenDatabaseValidationError` with a message
pointing the user at ``config.use(ChartExtension())``.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING

from sqlalchemy_declarative_extensions import (
    Schemas,
    register_function,
)
from sqlalchemy_declarative_extensions.dialects.postgresql import (
    Function,
    FunctionSecurity,
    FunctionVolatility,
)

from codegen_database.config import resolve_utility_schema
from codegen_database.ext.chart._contract import (
    CHART_EXTENSION_NAME,
    CHART_SCHEMA_KEY,
    assert_chart_extension_declared,
)
from codegen_database.ext.chart.date_bin import (
    construct_date_bin_function,
)
from codegen_database.extension import CodegenDatabaseExtension

if TYPE_CHECKING:
    from sqlalchemy import MetaData

__all__ = [
    "CHART_EXTENSION_NAME",
    "CHART_SCHEMA_KEY",
    "ChartExtension",
    "assert_chart_extension_declared",
]


[docs] @dataclass class ChartExtension(CodegenDatabaseExtension): """Register the ``codegen_database_date_bin`` polyfill into metadata. When registered on a :class:`~codegen_database.config.CodegenDatabaseConfig`: - ``configure_metadata`` creates :func:`codegen_database_date_bin <codegen_database.ext.chart.date_bin.construct_date_bin_function>` in the resolved schema and stores it under ``metadata.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 (:func:`~codegen_database.ext.ledger.chart_functions.construct_ledger_chart_function` and friends) call :func:`assert_chart_extension_declared` internally and raise :class:`~codegen_database.errors.CodegenDatabaseValidationError` when this extension is absent. Args: name: Extension name. Defaults to ``"codegen_database-chart"``. schema: Schema for the polyfill function. ``None`` (the default) uses the config's ``utility_schema``, so ``codegen_database_date_bin`` shares the one codegen_database utility schema with every other codegen_database-managed object. Set a string to override. """ name: str = CHART_EXTENSION_NAME schema: str | None = None
[docs] def configure_metadata(self, metadata: MetaData) -> None: """Register the polyfill, schema, and marker on *metadata*.""" schema = ( self.schema if self.schema is not None else resolve_utility_schema(metadata) ) existing_schemas: Schemas | None = metadata.info.get("schemas") if existing_schemas is None: metadata.info["schemas"] = Schemas(ignore_unspecified=True).are( schema ) else: # ``Schemas.are(...)`` replaces the tuple rather than merging. # Accumulate so earlier-registered schemas (from tables/views) # survive this call. names = {s.name for s in existing_schemas.schemas} names.add(schema) metadata.info["schemas"] = existing_schemas.are(*sorted(names)) metadata.info[CHART_SCHEMA_KEY] = schema spec = construct_date_bin_function() existing_fns = metadata.info.get("functions") if existing_fns is not None: for fn in existing_fns: if fn.name == spec["name"] and fn.schema == schema: return register_function( metadata, Function( spec["name"], spec["definition"], returns=spec.get("returns", "void"), language=spec.get("language", "sql"), schema=schema, parameters=spec.get("parameters", []), security=spec.get("security", FunctionSecurity.invoker), volatility=spec.get("volatility", FunctionVolatility.VOLATILE), ), )