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