Source code for codegen_database.ext.chart.date_bin
"""``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
:mod:`codegen_database.ext.ledger.chart_functions`, which accept an ``interval``
``p_period`` parameter. It lives under :mod:`codegen_database.ext.chart` rather
than the ledger because :class:`~codegen_database.ext.chart.ChartExtension` is
what installs it during ``configure_metadata``; the ledger merely
calls into it.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from sqlalchemy_declarative_extensions.dialects.postgresql import (
FunctionParam,
FunctionSecurity,
FunctionVolatility,
)
if TYPE_CHECKING:
from codegen_database.functions import CodegenDatabaseFunctionSpec
CODEGEN_DATABASE_DATE_BIN_NAME = "codegen_database_date_bin"
"""Canonical unqualified name of the polyfill function."""
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 via ``date_trunc('month', origin)``).
"""
_DEFINITION = """\
SELECT
CASE
-- Sub-month stride: native date_bin handles it.
WHEN stride_months = 0
THEN date_bin(stride, ts, origin)
-- Whole-month (incl. quarter, year) stride. Align on the
-- 1st-of-month of origin; preserve origin's month-of-year
-- offset so custom fiscal alignments work (pass origin as
-- e.g. '2000-04-01' to get quarters starting in April).
WHEN stride_sub = INTERVAL '0'
THEN date_trunc('month', origin) + make_interval(
months => FLOOR(
((EXTRACT(year FROM ts)
- EXTRACT(year FROM origin)) * 12
+ (EXTRACT(month FROM ts)
- EXTRACT(month FROM origin)))::numeric
/ stride_months
)::int * stride_months
)
-- Mixed stride (e.g. '1 month 3 days') has no uniform
-- bucketing — surface the error as NULL rather than a
-- silently-wrong result.
ELSE NULL::timestamptz
END
FROM (VALUES (
(EXTRACT(year FROM stride) * 12
+ EXTRACT(month FROM stride))::int,
stride - make_interval(
years => EXTRACT(year FROM stride)::int,
months => EXTRACT(month FROM stride)::int
)
)) AS s(stride_months, stride_sub)
"""
[docs]
def construct_date_bin_function(
*,
name: str = CODEGEN_DATABASE_DATE_BIN_NAME,
) -> CodegenDatabaseFunctionSpec:
"""Build a ``codegen_database_date_bin`` function spec.
Returns a :class:`~codegen_database.functions.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:
* ``stride`` contains only sub-month units → delegate to
Postgres's built-in ``date_bin``.
* ``stride`` is a whole number of months (so includes quarter
and year) → calendar arithmetic, aligned to the 1st of the
month.
* ``stride`` mixes month and sub-month units → returns
``NULL`` (no uniform bucketing possible).
Args:
name: 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.
Returns:
A :class:`~codegen_database.functions.CodegenDatabaseFunctionSpec`
ready to pass to
:class:`~codegen_database.functions.CodegenDatabaseFunction`
or assign to ``__funcspec__`` on a
:class:`~codegen_database.declarative.CodegenDatabaseFunctionMixin`
subclass.
"""
# Returned as a plain dict literal -- equivalent to constructing
# the ``CodegenDatabaseFunctionSpec`` TypedDict, but avoids importing it
# at runtime here. ``CodegenDatabaseFunctionSpec`` lives in
# :mod:`codegen_database.functions`, which is import-safe from this module,
# but the literal keeps this file dependency-free for the
# ``date_bin`` polyfill which is otherwise just a static SQL body.
return {
"name": name,
"definition": _DEFINITION,
"language": "sql",
"parameters": [
FunctionParam.input("stride", "interval"),
FunctionParam.input("ts", "timestamptz"),
FunctionParam.input(
"origin",
"timestamptz",
default=CODEGEN_DATABASE_DATE_BIN_DEFAULT_ORIGIN,
),
],
"returns": "timestamptz",
"security": FunctionSecurity.invoker,
"volatility": FunctionVolatility.STABLE,
}