Category overhaul (#20)
Reviewed-on: #20 Co-authored-by: Mathias <mathias2784@web.de>
This commit was merged in pull request #20.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
"""merge category validity and budget migration branches
|
||||
|
||||
Revision ID: c18ae67f4b2a
|
||||
Revises: d9c6533f8a1b, fe23064de1c5
|
||||
Create Date: 2026-10-27
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "c18ae67f4b2a"
|
||||
down_revision: str | Sequence[str] | None = ("d9c6533f8a1b", "fe23064de1c5")
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -0,0 +1,102 @@
|
||||
"""add temporal validity to categories
|
||||
|
||||
Revision ID: d9c6533f8a1b
|
||||
Revises: 78ec55b9e1d1
|
||||
Create Date: 2026-10-25 00:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "d9c6533f8a1b"
|
||||
down_revision: str | Sequence[str] | None = "78ec55b9e1d1"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
with op.batch_alter_table("category") as batch_op:
|
||||
batch_op.drop_constraint("uq_category_name_parent", type_="unique")
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"valid_from",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=True,
|
||||
info={
|
||||
"description": "UTC start of validity; NULL means valid since the indefinite past."
|
||||
},
|
||||
)
|
||||
)
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"valid_to",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=True,
|
||||
info={
|
||||
"description": "UTC end of validity; NULL means the version is currently valid."
|
||||
},
|
||||
)
|
||||
)
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"last_action",
|
||||
sa.String(),
|
||||
nullable=False,
|
||||
server_default="created",
|
||||
info={
|
||||
"description": "Most recent lifecycle action recorded for this category version."
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# Existing rows predate temporal tracking, so their start is unbounded.
|
||||
op.execute("UPDATE category SET last_action = 'migrated'")
|
||||
|
||||
# Separate root and non-root indexes so NULL parent IDs are unique too.
|
||||
op.create_index(
|
||||
"uq_category_active_name_parent",
|
||||
"category",
|
||||
["name", "parent_id"],
|
||||
unique=True,
|
||||
sqlite_where=sa.text("valid_to IS NULL AND parent_id IS NOT NULL"),
|
||||
postgresql_where=sa.text("valid_to IS NULL AND parent_id IS NOT NULL"),
|
||||
)
|
||||
op.create_index(
|
||||
"uq_category_active_root_name",
|
||||
"category",
|
||||
["name"],
|
||||
unique=True,
|
||||
sqlite_where=sa.text("valid_to IS NULL AND parent_id IS NULL"),
|
||||
postgresql_where=sa.text("valid_to IS NULL AND parent_id IS NULL"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
has_temporal_history = bind.execute(
|
||||
sa.text(
|
||||
"SELECT 1 FROM category "
|
||||
"WHERE valid_from IS NOT NULL OR valid_to IS NOT NULL LIMIT 1"
|
||||
)
|
||||
).first()
|
||||
if has_temporal_history:
|
||||
raise RuntimeError(
|
||||
"Cannot downgrade category validity while temporal history exists; "
|
||||
"the downgrade would discard it."
|
||||
)
|
||||
|
||||
op.drop_index("uq_category_active_root_name", table_name="category")
|
||||
op.drop_index("uq_category_active_name_parent", table_name="category")
|
||||
|
||||
with op.batch_alter_table("category") as batch_op:
|
||||
batch_op.drop_column("last_action")
|
||||
batch_op.drop_column("valid_to")
|
||||
batch_op.drop_column("valid_from")
|
||||
batch_op.create_unique_constraint(
|
||||
"uq_category_name_parent", ["name", "parent_id"]
|
||||
)
|
||||
@@ -0,0 +1,52 @@
|
||||
"""create category_budget table
|
||||
|
||||
Revision ID: fe23064de1c5
|
||||
Revises: 78ec55b9e1d1
|
||||
Create Date: 2026-10-16 10:43:24.952699
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "fe23064de1c5"
|
||||
down_revision: str | Sequence[str] | None = "78ec55b9e1d1"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"category_budget",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("category_id", sa.Integer(), nullable=False),
|
||||
sa.Column("amount_eur", sa.Numeric(precision=12, scale=2), nullable=False),
|
||||
sa.Column("valid_from", sa.Date(), nullable=True),
|
||||
sa.Column("valid_to", sa.Date(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["category_id"],
|
||||
["category.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_category_budget_category_id",
|
||||
"category_budget",
|
||||
["category_id"],
|
||||
unique=False,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index("ix_category_budget_category_id", table_name="category_budget")
|
||||
op.drop_table("category_budget")
|
||||
# ### end Alembic commands ###
|
||||
Reference in New Issue
Block a user