diff --git a/alembic/versions/20251109_01_initial_schema.py b/alembic/versions/20251109_01_initial_schema.py new file mode 100644 index 0000000..020a56b --- /dev/null +++ b/alembic/versions/20251109_01_initial_schema.py @@ -0,0 +1,195 @@ +"""Initial domain schema""" + +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "20251109_01" +down_revision = None +branch_labels = None +depends_on = None + + +mining_operation_type = sa.Enum( + "open_pit", + "underground", + "in_situ_leach", + "placer", + "quarry", + "mountaintop_removal", + "other", + name="miningoperationtype", +) + +scenario_status = sa.Enum( + "draft", + "active", + "archived", + name="scenariostatus", +) + +financial_category = sa.Enum( + "capex", + "opex", + "revenue", + "contingency", + "other", + name="financialcategory", +) + +cost_bucket = sa.Enum( + "capital_initial", + "capital_sustaining", + "operating_fixed", + "operating_variable", + "maintenance", + "reclamation", + "royalties", + "general_admin", + name="costbucket", +) + +distribution_type = sa.Enum( + "normal", + "triangular", + "uniform", + "lognormal", + "custom", + name="distributiontype", +) + +stochastic_variable = sa.Enum( + "ore_grade", + "recovery_rate", + "metal_price", + "operating_cost", + "capital_cost", + "discount_rate", + "throughput", + name="stochasticvariable", +) + +resource_type = sa.Enum( + "diesel", + "electricity", + "water", + "explosives", + "reagents", + "labor", + "equipment_hours", + "tailings_capacity", + name="resourcetype", +) + + +def upgrade() -> None: + bind = op.get_bind() + mining_operation_type.create(bind, checkfirst=True) + scenario_status.create(bind, checkfirst=True) + financial_category.create(bind, checkfirst=True) + cost_bucket.create(bind, checkfirst=True) + distribution_type.create(bind, checkfirst=True) + stochastic_variable.create(bind, checkfirst=True) + resource_type.create(bind, checkfirst=True) + + op.create_table( + "projects", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("location", sa.String(length=255), nullable=True), + sa.Column("operation_type", mining_operation_type, nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("name"), + ) + op.create_index(op.f("ix_projects_id"), "projects", ["id"], unique=False) + + op.create_table( + "scenarios", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("project_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("status", scenario_status, nullable=False), + sa.Column("start_date", sa.Date(), nullable=True), + sa.Column("end_date", sa.Date(), nullable=True), + sa.Column("discount_rate", sa.Numeric(precision=5, scale=2), nullable=True), + sa.Column("currency", sa.String(length=3), nullable=True), + sa.Column("primary_resource", resource_type, nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_scenarios_id"), "scenarios", ["id"], unique=False) + op.create_index(op.f("ix_scenarios_project_id"), "scenarios", ["project_id"], unique=False) + + op.create_table( + "financial_inputs", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("scenario_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("category", financial_category, nullable=False), + sa.Column("cost_bucket", cost_bucket, nullable=True), + sa.Column("amount", sa.Numeric(precision=18, scale=2), nullable=False), + sa.Column("currency", sa.String(length=3), nullable=True), + sa.Column("effective_date", sa.Date(), nullable=True), + sa.Column("notes", sa.Text(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.ForeignKeyConstraint(["scenario_id"], ["scenarios.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_financial_inputs_id"), "financial_inputs", ["id"], unique=False) + op.create_index(op.f("ix_financial_inputs_scenario_id"), "financial_inputs", ["scenario_id"], unique=False) + + op.create_table( + "simulation_parameters", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("scenario_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("distribution", distribution_type, nullable=False), + sa.Column("variable", stochastic_variable, nullable=True), + sa.Column("resource_type", resource_type, nullable=True), + sa.Column("mean_value", sa.Numeric(precision=18, scale=4), nullable=True), + sa.Column("standard_deviation", sa.Numeric(precision=18, scale=4), nullable=True), + sa.Column("minimum_value", sa.Numeric(precision=18, scale=4), nullable=True), + sa.Column("maximum_value", sa.Numeric(precision=18, scale=4), nullable=True), + sa.Column("unit", sa.String(length=32), nullable=True), + sa.Column("metadata", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.ForeignKeyConstraint(["scenario_id"], ["scenarios.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_simulation_parameters_id"), "simulation_parameters", ["id"], unique=False) + op.create_index(op.f("ix_simulation_parameters_scenario_id"), "simulation_parameters", ["scenario_id"], unique=False) + + +def downgrade() -> None: + op.drop_index(op.f("ix_simulation_parameters_scenario_id"), table_name="simulation_parameters") + op.drop_index(op.f("ix_simulation_parameters_id"), table_name="simulation_parameters") + op.drop_table("simulation_parameters") + + op.drop_index(op.f("ix_financial_inputs_scenario_id"), table_name="financial_inputs") + op.drop_index(op.f("ix_financial_inputs_id"), table_name="financial_inputs") + op.drop_table("financial_inputs") + + op.drop_index(op.f("ix_scenarios_project_id"), table_name="scenarios") + op.drop_index(op.f("ix_scenarios_id"), table_name="scenarios") + op.drop_table("scenarios") + + op.drop_index(op.f("ix_projects_id"), table_name="projects") + op.drop_table("projects") + + resource_type.drop(op.get_bind(), checkfirst=True) + stochastic_variable.drop(op.get_bind(), checkfirst=True) + distribution_type.drop(op.get_bind(), checkfirst=True) + cost_bucket.drop(op.get_bind(), checkfirst=True) + financial_category.drop(op.get_bind(), checkfirst=True) + scenario_status.drop(op.get_bind(), checkfirst=True) + mining_operation_type.drop(op.get_bind(), checkfirst=True) diff --git a/changelog.md b/changelog.md index 4cfec0c..1f5de9f 100644 --- a/changelog.md +++ b/changelog.md @@ -3,3 +3,4 @@ ## 2025-11-09 - Captured current implementation status, requirements coverage, missing features, and prioritized roadmap in `calminer-docs/implementation_status.md` to guide future development. +- Added core SQLAlchemy domain models, shared metadata descriptors, and Alembic migration setup (with initial schema snapshot) to establish the persistence layer foundation. diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..dffe6b7 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +-r requirements.txt +alembic