feat: Refactor database initialization for SQLite compatibility
This commit is contained in:
@@ -76,159 +76,312 @@ ENUM_DEFINITIONS = {
|
||||
}
|
||||
|
||||
# Minimal DDL for tables we seed / that bootstrap relies on
|
||||
TABLE_DDLS = [
|
||||
# roles
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
display_name VARCHAR(128) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT uq_roles_name UNIQUE (name)
|
||||
);
|
||||
""",
|
||||
# users
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
username VARCHAR(128) NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
is_superuser BOOLEAN NOT NULL DEFAULT false,
|
||||
last_login_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT uq_users_email UNIQUE (email),
|
||||
CONSTRAINT uq_users_username UNIQUE (username)
|
||||
);
|
||||
""",
|
||||
# user_roles
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
user_id INTEGER NOT NULL,
|
||||
role_id INTEGER NOT NULL,
|
||||
granted_at TIMESTAMPTZ DEFAULT now(),
|
||||
granted_by INTEGER,
|
||||
PRIMARY KEY (user_id, role_id),
|
||||
CONSTRAINT uq_user_roles_user_role UNIQUE (user_id, role_id)
|
||||
);
|
||||
""",
|
||||
# pricing_settings
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS pricing_settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
slug VARCHAR(64) NOT NULL,
|
||||
description TEXT,
|
||||
default_currency VARCHAR(3),
|
||||
default_payable_pct NUMERIC(5,2) DEFAULT 100.00 NOT NULL,
|
||||
moisture_threshold_pct NUMERIC(5,2) DEFAULT 8.00 NOT NULL,
|
||||
moisture_penalty_per_pct NUMERIC(14,4) DEFAULT 0.0000 NOT NULL,
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT uq_pricing_settings_slug UNIQUE (slug),
|
||||
CONSTRAINT uq_pricing_settings_name UNIQUE (name)
|
||||
);
|
||||
""",
|
||||
# pricing_metal_settings
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS pricing_metal_settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
pricing_settings_id INTEGER NOT NULL REFERENCES pricing_settings(id) ON DELETE CASCADE,
|
||||
metal_code VARCHAR(32) NOT NULL,
|
||||
payable_pct NUMERIC(5,2),
|
||||
moisture_threshold_pct NUMERIC(5,2),
|
||||
moisture_penalty_per_pct NUMERIC(14,4),
|
||||
data JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT uq_pricing_metal_settings_code UNIQUE (pricing_settings_id, metal_code)
|
||||
);
|
||||
""",
|
||||
# pricing_impurity_settings
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS pricing_impurity_settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
pricing_settings_id INTEGER NOT NULL REFERENCES pricing_settings(id) ON DELETE CASCADE,
|
||||
impurity_code VARCHAR(32) NOT NULL,
|
||||
threshold_ppm NUMERIC(14,4) DEFAULT 0.0000 NOT NULL,
|
||||
penalty_per_ppm NUMERIC(14,4) DEFAULT 0.0000 NOT NULL,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT uq_pricing_impurity_settings_code UNIQUE (pricing_settings_id, impurity_code)
|
||||
);
|
||||
""",
|
||||
# core domain tables: projects, scenarios, financial_inputs, simulation_parameters
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
location VARCHAR(255),
|
||||
operation_type miningoperationtype NOT NULL,
|
||||
description TEXT,
|
||||
pricing_settings_id INTEGER REFERENCES pricing_settings(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT uq_projects_name UNIQUE (name)
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS scenarios (
|
||||
id SERIAL PRIMARY KEY,
|
||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
status scenariostatus NOT NULL,
|
||||
start_date DATE,
|
||||
end_date DATE,
|
||||
discount_rate NUMERIC(5,2),
|
||||
currency VARCHAR(3),
|
||||
primary_resource resourcetype,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT uq_scenarios_project_name UNIQUE (project_id, name)
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS financial_inputs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
scenario_id INTEGER NOT NULL REFERENCES scenarios(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
category financialcategory NOT NULL,
|
||||
cost_bucket costbucket,
|
||||
amount NUMERIC(18,2) NOT NULL,
|
||||
currency VARCHAR(3),
|
||||
effective_date DATE,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT uq_financial_inputs_scenario_name UNIQUE (scenario_id, name)
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS simulation_parameters (
|
||||
id SERIAL PRIMARY KEY,
|
||||
scenario_id INTEGER NOT NULL REFERENCES scenarios(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
distribution distributiontype NOT NULL,
|
||||
variable stochasticvariable,
|
||||
resource_type resourcetype,
|
||||
mean_value NUMERIC(18,4),
|
||||
standard_deviation NUMERIC(18,4),
|
||||
minimum_value NUMERIC(18,4),
|
||||
maximum_value NUMERIC(18,4),
|
||||
unit VARCHAR(32),
|
||||
configuration JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
""",
|
||||
]
|
||||
|
||||
|
||||
def _get_table_ddls(is_sqlite: bool) -> List[str]:
|
||||
if is_sqlite:
|
||||
return [
|
||||
# roles
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
display_name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
""",
|
||||
# users
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
is_superuser INTEGER NOT NULL DEFAULT 0,
|
||||
last_login_at DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
""",
|
||||
# user_roles
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
user_id INTEGER NOT NULL,
|
||||
role_id INTEGER NOT NULL,
|
||||
granted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
granted_by INTEGER,
|
||||
PRIMARY KEY (user_id, role_id)
|
||||
);
|
||||
""",
|
||||
# pricing_settings
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS pricing_settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
default_currency TEXT,
|
||||
default_payable_pct REAL DEFAULT 100.00 NOT NULL,
|
||||
moisture_threshold_pct REAL DEFAULT 8.00 NOT NULL,
|
||||
moisture_penalty_per_pct REAL DEFAULT 0.0000 NOT NULL,
|
||||
metadata TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
""",
|
||||
# pricing_metal_settings
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS pricing_metal_settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
pricing_settings_id INTEGER NOT NULL REFERENCES pricing_settings(id) ON DELETE CASCADE,
|
||||
metal_code TEXT NOT NULL,
|
||||
payable_pct REAL,
|
||||
moisture_threshold_pct REAL,
|
||||
moisture_penalty_per_pct REAL,
|
||||
data TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (pricing_settings_id, metal_code)
|
||||
);
|
||||
""",
|
||||
# pricing_impurity_settings
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS pricing_impurity_settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
pricing_settings_id INTEGER NOT NULL REFERENCES pricing_settings(id) ON DELETE CASCADE,
|
||||
impurity_code TEXT NOT NULL,
|
||||
threshold_ppm REAL DEFAULT 0.0000 NOT NULL,
|
||||
penalty_per_ppm REAL DEFAULT 0.0000 NOT NULL,
|
||||
notes TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (pricing_settings_id, impurity_code)
|
||||
);
|
||||
""",
|
||||
# core domain tables: projects, scenarios, financial_inputs, simulation_parameters
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
location TEXT,
|
||||
operation_type TEXT NOT NULL CHECK (operation_type IN ('open_pit', 'underground', 'in_situ_leach', 'placer', 'quarry', 'mountaintop_removal', 'other')),
|
||||
description TEXT,
|
||||
pricing_settings_id INTEGER REFERENCES pricing_settings(id) ON DELETE SET NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS scenarios (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
status TEXT NOT NULL CHECK (status IN ('draft', 'active', 'archived')),
|
||||
start_date DATE,
|
||||
end_date DATE,
|
||||
discount_rate REAL,
|
||||
currency TEXT,
|
||||
primary_resource TEXT CHECK (primary_resource IN ('diesel', 'electricity', 'water', 'explosives', 'reagents', 'labor', 'equipment_hours', 'tailings_capacity') OR primary_resource IS NULL),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (project_id, name)
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS financial_inputs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
scenario_id INTEGER NOT NULL REFERENCES scenarios(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
category TEXT NOT NULL CHECK (category IN ('capex', 'opex', 'revenue', 'contingency', 'other')),
|
||||
cost_bucket TEXT CHECK (cost_bucket IN ('capital_initial', 'capital_sustaining', 'operating_fixed', 'operating_variable', 'maintenance', 'reclamation', 'royalties', 'general_admin') OR cost_bucket IS NULL),
|
||||
amount REAL NOT NULL,
|
||||
currency TEXT,
|
||||
effective_date DATE,
|
||||
notes TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (scenario_id, name)
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS simulation_parameters (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
scenario_id INTEGER NOT NULL REFERENCES scenarios(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
distribution TEXT NOT NULL CHECK (distribution IN ('normal', 'triangular', 'uniform', 'lognormal', 'custom')),
|
||||
variable TEXT CHECK (variable IN ('ore_grade', 'recovery_rate', 'metal_price', 'operating_cost', 'capital_cost', 'discount_rate', 'throughput') OR variable IS NULL),
|
||||
resource_type TEXT CHECK (resource_type IN ('diesel', 'electricity', 'water', 'explosives', 'reagents', 'labor', 'equipment_hours', 'tailings_capacity') OR resource_type IS NULL),
|
||||
mean_value REAL,
|
||||
standard_deviation REAL,
|
||||
minimum_value REAL,
|
||||
maximum_value REAL,
|
||||
unit TEXT,
|
||||
configuration TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
""",
|
||||
]
|
||||
else:
|
||||
# PostgreSQL DDLs
|
||||
return [
|
||||
# roles
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
display_name VARCHAR(128) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT uq_roles_name UNIQUE (name)
|
||||
);
|
||||
""",
|
||||
# users
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
username VARCHAR(128) NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
is_superuser BOOLEAN NOT NULL DEFAULT false,
|
||||
last_login_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT uq_users_email UNIQUE (email),
|
||||
CONSTRAINT uq_users_username UNIQUE (username)
|
||||
);
|
||||
""",
|
||||
# user_roles
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
user_id INTEGER NOT NULL,
|
||||
role_id INTEGER NOT NULL,
|
||||
granted_at TIMESTAMPTZ DEFAULT now(),
|
||||
granted_by INTEGER,
|
||||
PRIMARY KEY (user_id, role_id),
|
||||
CONSTRAINT uq_user_roles_user_role UNIQUE (user_id, role_id)
|
||||
);
|
||||
""",
|
||||
# pricing_settings
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS pricing_settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
slug VARCHAR(64) NOT NULL,
|
||||
description TEXT,
|
||||
default_currency VARCHAR(3),
|
||||
default_payable_pct NUMERIC(5,2) DEFAULT 100.00 NOT NULL,
|
||||
moisture_threshold_pct NUMERIC(5,2) DEFAULT 8.00 NOT NULL,
|
||||
moisture_penalty_per_pct NUMERIC(14,4) DEFAULT 0.0000 NOT NULL,
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT uq_pricing_settings_slug UNIQUE (slug),
|
||||
CONSTRAINT uq_pricing_settings_name UNIQUE (name)
|
||||
);
|
||||
""",
|
||||
# pricing_metal_settings
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS pricing_metal_settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
pricing_settings_id INTEGER NOT NULL REFERENCES pricing_settings(id) ON DELETE CASCADE,
|
||||
metal_code VARCHAR(32) NOT NULL,
|
||||
payable_pct NUMERIC(5,2),
|
||||
moisture_threshold_pct NUMERIC(5,2),
|
||||
moisture_penalty_per_pct NUMERIC(14,4),
|
||||
data JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT uq_pricing_metal_settings_code UNIQUE (pricing_settings_id, metal_code)
|
||||
);
|
||||
""",
|
||||
# pricing_impurity_settings
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS pricing_impurity_settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
pricing_settings_id INTEGER NOT NULL REFERENCES pricing_settings(id) ON DELETE CASCADE,
|
||||
impurity_code VARCHAR(32) NOT NULL,
|
||||
threshold_ppm NUMERIC(14,4) DEFAULT 0.0000 NOT NULL,
|
||||
penalty_per_ppm NUMERIC(14,4) DEFAULT 0.0000 NOT NULL,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT uq_pricing_impurity_settings_code UNIQUE (pricing_settings_id, impurity_code)
|
||||
);
|
||||
""",
|
||||
# core domain tables: projects, scenarios, financial_inputs, simulation_parameters
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
location VARCHAR(255),
|
||||
operation_type miningoperationtype NOT NULL,
|
||||
description TEXT,
|
||||
pricing_settings_id INTEGER REFERENCES pricing_settings(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT uq_projects_name UNIQUE (name)
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS scenarios (
|
||||
id SERIAL PRIMARY KEY,
|
||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
status scenariostatus NOT NULL,
|
||||
start_date DATE,
|
||||
end_date DATE,
|
||||
discount_rate NUMERIC(5,2),
|
||||
currency VARCHAR(3),
|
||||
primary_resource resourcetype,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT uq_scenarios_project_name UNIQUE (project_id, name)
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS financial_inputs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
scenario_id INTEGER NOT NULL REFERENCES scenarios(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
category financialcategory NOT NULL,
|
||||
cost_bucket costbucket,
|
||||
amount NUMERIC(18,2) NOT NULL,
|
||||
currency VARCHAR(3),
|
||||
effective_date DATE,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT uq_financial_inputs_scenario_name UNIQUE (scenario_id, name)
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS simulation_parameters (
|
||||
id SERIAL PRIMARY KEY,
|
||||
scenario_id INTEGER NOT NULL REFERENCES scenarios(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
distribution distributiontype NOT NULL,
|
||||
variable stochasticvariable,
|
||||
resource_type resourcetype,
|
||||
mean_value NUMERIC(18,4),
|
||||
standard_deviation NUMERIC(18,4),
|
||||
minimum_value NUMERIC(18,4),
|
||||
maximum_value NUMERIC(18,4),
|
||||
unit VARCHAR(32),
|
||||
configuration JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
""",
|
||||
]
|
||||
|
||||
|
||||
# Seeds
|
||||
DEFAULT_ROLES = [
|
||||
@@ -404,6 +557,10 @@ def _get_database_url() -> str:
|
||||
return DATABASE_URL
|
||||
|
||||
|
||||
def _is_sqlite(database_url: str) -> bool:
|
||||
return database_url.startswith("sqlite://")
|
||||
|
||||
|
||||
def _create_engine(database_url: Optional[str] = None) -> Engine:
|
||||
database_url = database_url or _get_database_url()
|
||||
engine = create_engine(database_url, future=True)
|
||||
@@ -422,7 +579,11 @@ def _create_enum_if_missing_sql(type_name: str, values: List[str]) -> str:
|
||||
return sql
|
||||
|
||||
|
||||
def ensure_enums(engine: Engine) -> None:
|
||||
def ensure_enums(engine: Engine, is_sqlite: bool) -> None:
|
||||
if is_sqlite:
|
||||
# SQLite doesn't have enums, constraints are in table DDL
|
||||
logger.debug("Skipping enum creation for SQLite")
|
||||
return
|
||||
with engine.begin() as conn:
|
||||
for name, vals in ENUM_DEFINITIONS.items():
|
||||
sql = _create_enum_if_missing_sql(name, vals)
|
||||
@@ -445,7 +606,11 @@ def _fetch_enum_values(conn, type_name: str) -> Set[str]:
|
||||
return {row.enumlabel for row in rows}
|
||||
|
||||
|
||||
def normalize_enum_values(engine: Engine) -> None:
|
||||
def normalize_enum_values(engine: Engine, is_sqlite: bool) -> None:
|
||||
if is_sqlite:
|
||||
# No enums to normalize in SQLite
|
||||
logger.debug("Skipping enum normalization for SQLite")
|
||||
return
|
||||
with engine.begin() as conn:
|
||||
for type_name, expected_values in ENUM_DEFINITIONS.items():
|
||||
try:
|
||||
@@ -485,9 +650,10 @@ def normalize_enum_values(engine: Engine) -> None:
|
||||
existing_values.add(normalized)
|
||||
|
||||
|
||||
def ensure_tables(engine: Engine) -> None:
|
||||
def ensure_tables(engine: Engine, is_sqlite: bool) -> None:
|
||||
table_ddls = _get_table_ddls(is_sqlite)
|
||||
with engine.begin() as conn:
|
||||
for ddl in TABLE_DDLS:
|
||||
for ddl in table_ddls:
|
||||
logger.debug("Executing DDL:\n%s", ddl)
|
||||
conn.execute(text(ddl))
|
||||
|
||||
@@ -524,14 +690,18 @@ CONSTRAINT_DDLS = [
|
||||
]
|
||||
|
||||
|
||||
def ensure_constraints(engine: Engine) -> None:
|
||||
def ensure_constraints(engine: Engine, is_sqlite: bool) -> None:
|
||||
if is_sqlite:
|
||||
# Constraints are already in table DDL for SQLite
|
||||
logger.debug("Skipping constraint creation for SQLite")
|
||||
return
|
||||
with engine.begin() as conn:
|
||||
for ddl in CONSTRAINT_DDLS:
|
||||
logger.debug("Ensuring constraint via:\n%s", ddl)
|
||||
conn.execute(text(ddl))
|
||||
|
||||
|
||||
def seed_roles(engine: Engine) -> None:
|
||||
def seed_roles(engine: Engine, is_sqlite: bool) -> None:
|
||||
with engine.begin() as conn:
|
||||
for r in DEFAULT_ROLES:
|
||||
seed = RoleSeed(**r)
|
||||
@@ -545,7 +715,7 @@ def seed_roles(engine: Engine) -> None:
|
||||
)
|
||||
|
||||
|
||||
def seed_admin_user(engine: Engine) -> None:
|
||||
def seed_admin_user(engine: Engine, is_sqlite: bool) -> None:
|
||||
with engine.begin() as conn:
|
||||
# Use environment-configured admin settings when present so initializer
|
||||
# aligns with the application's bootstrap configuration.
|
||||
@@ -592,7 +762,7 @@ def seed_admin_user(engine: Engine) -> None:
|
||||
)
|
||||
|
||||
|
||||
def ensure_default_pricing(engine: Engine) -> None:
|
||||
def ensure_default_pricing(engine: Engine, is_sqlite: bool) -> None:
|
||||
with engine.begin() as conn:
|
||||
p = PricingSeed(**DEFAULT_PRICING)
|
||||
# Try insert on slug conflict
|
||||
@@ -622,7 +792,7 @@ def _project_id_by_name(conn, project_name: str) -> Optional[int]:
|
||||
return row.id if row else None
|
||||
|
||||
|
||||
def ensure_default_projects(engine: Engine) -> None:
|
||||
def ensure_default_projects(engine: Engine, is_sqlite: bool) -> None:
|
||||
with engine.begin() as conn:
|
||||
for project in DEFAULT_PROJECTS:
|
||||
conn.execute(
|
||||
@@ -640,7 +810,7 @@ def ensure_default_projects(engine: Engine) -> None:
|
||||
)
|
||||
|
||||
|
||||
def ensure_default_scenarios(engine: Engine) -> None:
|
||||
def ensure_default_scenarios(engine: Engine, is_sqlite: bool) -> None:
|
||||
with engine.begin() as conn:
|
||||
for scenario in DEFAULT_SCENARIOS:
|
||||
project_id = _project_id_by_name(conn, scenario.project_name)
|
||||
@@ -654,34 +824,48 @@ def ensure_default_scenarios(engine: Engine) -> None:
|
||||
|
||||
payload = scenario.model_dump(exclude={"project_name"})
|
||||
payload.update({"project_id": project_id})
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO scenarios (
|
||||
project_id, name, description, status, discount_rate,
|
||||
currency, primary_resource
|
||||
)
|
||||
VALUES (
|
||||
:project_id, :name, :description, CAST(:status AS scenariostatus),
|
||||
:discount_rate, :currency,
|
||||
CASE WHEN :primary_resource IS NULL
|
||||
THEN NULL
|
||||
ELSE CAST(:primary_resource AS resourcetype)
|
||||
END
|
||||
)
|
||||
ON CONFLICT (project_id, name) DO UPDATE SET
|
||||
description = EXCLUDED.description,
|
||||
status = EXCLUDED.status,
|
||||
discount_rate = EXCLUDED.discount_rate,
|
||||
currency = EXCLUDED.currency,
|
||||
primary_resource = EXCLUDED.primary_resource
|
||||
"""
|
||||
),
|
||||
payload,
|
||||
)
|
||||
if is_sqlite:
|
||||
sql = """
|
||||
INSERT INTO scenarios (
|
||||
project_id, name, description, status, discount_rate,
|
||||
currency, primary_resource
|
||||
)
|
||||
VALUES (
|
||||
:project_id, :name, :description, :status,
|
||||
:discount_rate, :currency, :primary_resource
|
||||
)
|
||||
ON CONFLICT (project_id, name) DO UPDATE SET
|
||||
description = EXCLUDED.description,
|
||||
status = EXCLUDED.status,
|
||||
discount_rate = EXCLUDED.discount_rate,
|
||||
currency = EXCLUDED.currency,
|
||||
primary_resource = EXCLUDED.primary_resource
|
||||
"""
|
||||
else:
|
||||
sql = """
|
||||
INSERT INTO scenarios (
|
||||
project_id, name, description, status, discount_rate,
|
||||
currency, primary_resource
|
||||
)
|
||||
VALUES (
|
||||
:project_id, :name, :description, CAST(:status AS scenariostatus),
|
||||
:discount_rate, :currency,
|
||||
CASE WHEN :primary_resource IS NULL
|
||||
THEN NULL
|
||||
ELSE CAST(:primary_resource AS resourcetype)
|
||||
END
|
||||
)
|
||||
ON CONFLICT (project_id, name) DO UPDATE SET
|
||||
description = EXCLUDED.description,
|
||||
status = EXCLUDED.status,
|
||||
discount_rate = EXCLUDED.discount_rate,
|
||||
currency = EXCLUDED.currency,
|
||||
primary_resource = EXCLUDED.primary_resource
|
||||
"""
|
||||
conn.execute(text(sql), payload)
|
||||
|
||||
|
||||
def ensure_default_financial_inputs(engine: Engine) -> None:
|
||||
def ensure_default_financial_inputs(engine: Engine, is_sqlite: bool) -> None:
|
||||
with engine.begin() as conn:
|
||||
for item in DEFAULT_FINANCIAL_INPUTS:
|
||||
project_id = _project_id_by_name(conn, item.project_name)
|
||||
@@ -711,32 +895,48 @@ def ensure_default_financial_inputs(engine: Engine) -> None:
|
||||
payload = item.model_dump(
|
||||
exclude={"project_name", "scenario_name"},
|
||||
)
|
||||
if is_sqlite:
|
||||
# Convert Decimal to float for SQLite
|
||||
payload["amount"] = float(payload["amount"])
|
||||
payload.update({"scenario_id": scenario_row.id})
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO financial_inputs (
|
||||
scenario_id, name, category, cost_bucket, amount, currency, notes
|
||||
)
|
||||
VALUES (
|
||||
:scenario_id, :name, CAST(:category AS financialcategory),
|
||||
CASE WHEN :cost_bucket IS NULL THEN NULL
|
||||
ELSE CAST(:cost_bucket AS costbucket)
|
||||
END,
|
||||
:amount,
|
||||
:currency,
|
||||
:notes
|
||||
)
|
||||
ON CONFLICT (scenario_id, name) DO UPDATE SET
|
||||
category = EXCLUDED.category,
|
||||
cost_bucket = EXCLUDED.cost_bucket,
|
||||
amount = EXCLUDED.amount,
|
||||
currency = EXCLUDED.currency,
|
||||
notes = EXCLUDED.notes
|
||||
"""
|
||||
),
|
||||
payload,
|
||||
)
|
||||
if is_sqlite:
|
||||
sql = """
|
||||
INSERT INTO financial_inputs (
|
||||
scenario_id, name, category, cost_bucket, amount, currency, notes
|
||||
)
|
||||
VALUES (
|
||||
:scenario_id, :name, :category, :cost_bucket,
|
||||
:amount, :currency, :notes
|
||||
)
|
||||
ON CONFLICT (scenario_id, name) DO UPDATE SET
|
||||
category = EXCLUDED.category,
|
||||
cost_bucket = EXCLUDED.cost_bucket,
|
||||
amount = EXCLUDED.amount,
|
||||
currency = EXCLUDED.currency,
|
||||
notes = EXCLUDED.notes
|
||||
"""
|
||||
else:
|
||||
sql = """
|
||||
INSERT INTO financial_inputs (
|
||||
scenario_id, name, category, cost_bucket, amount, currency, notes
|
||||
)
|
||||
VALUES (
|
||||
:scenario_id, :name, CAST(:category AS financialcategory),
|
||||
CASE WHEN :cost_bucket IS NULL THEN NULL
|
||||
ELSE CAST(:cost_bucket AS costbucket)
|
||||
END,
|
||||
:amount,
|
||||
:currency,
|
||||
:notes
|
||||
)
|
||||
ON CONFLICT (scenario_id, name) DO UPDATE SET
|
||||
category = EXCLUDED.category,
|
||||
cost_bucket = EXCLUDED.cost_bucket,
|
||||
amount = EXCLUDED.amount,
|
||||
currency = EXCLUDED.currency,
|
||||
notes = EXCLUDED.notes
|
||||
"""
|
||||
conn.execute(text(sql), payload)
|
||||
|
||||
|
||||
def init_db(database_url: Optional[str] = None) -> None:
|
||||
@@ -749,18 +949,20 @@ def init_db(database_url: Optional[str] = None) -> None:
|
||||
- Ensure default pricing settings record exists.
|
||||
- Seed sample projects, scenarios, and financial inputs.
|
||||
"""
|
||||
database_url = database_url or _get_database_url()
|
||||
is_sqlite = _is_sqlite(database_url)
|
||||
engine = _create_engine(database_url)
|
||||
logger.info("Starting DB initialization using engine=%s", engine)
|
||||
ensure_enums(engine)
|
||||
normalize_enum_values(engine)
|
||||
ensure_tables(engine)
|
||||
ensure_constraints(engine)
|
||||
seed_roles(engine)
|
||||
seed_admin_user(engine)
|
||||
ensure_default_pricing(engine)
|
||||
ensure_default_projects(engine)
|
||||
ensure_default_scenarios(engine)
|
||||
ensure_default_financial_inputs(engine)
|
||||
ensure_enums(engine, is_sqlite)
|
||||
normalize_enum_values(engine, is_sqlite)
|
||||
ensure_tables(engine, is_sqlite)
|
||||
ensure_constraints(engine, is_sqlite)
|
||||
seed_roles(engine, is_sqlite)
|
||||
seed_admin_user(engine, is_sqlite)
|
||||
ensure_default_pricing(engine, is_sqlite)
|
||||
ensure_default_projects(engine, is_sqlite)
|
||||
ensure_default_scenarios(engine, is_sqlite)
|
||||
ensure_default_financial_inputs(engine, is_sqlite)
|
||||
logger.info("DB initialization complete")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user