feat: Refactor database initialization for SQLite compatibility
This commit is contained in:
@@ -76,7 +76,159 @@ ENUM_DEFINITIONS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Minimal DDL for tables we seed / that bootstrap relies on
|
# Minimal DDL for tables we seed / that bootstrap relies on
|
||||||
TABLE_DDLS = [
|
|
||||||
|
|
||||||
|
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
|
# roles
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS roles (
|
CREATE TABLE IF NOT EXISTS roles (
|
||||||
@@ -230,6 +382,7 @@ TABLE_DDLS = [
|
|||||||
""",
|
""",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Seeds
|
# Seeds
|
||||||
DEFAULT_ROLES = [
|
DEFAULT_ROLES = [
|
||||||
{"id": 1, "name": "admin", "display_name": "Administrator",
|
{"id": 1, "name": "admin", "display_name": "Administrator",
|
||||||
@@ -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,9 +824,25 @@ 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 (
|
||||||
|
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 (
|
INSERT INTO scenarios (
|
||||||
project_id, name, description, status, discount_rate,
|
project_id, name, description, status, discount_rate,
|
||||||
currency, primary_resource
|
currency, primary_resource
|
||||||
@@ -676,12 +862,10 @@ def ensure_default_scenarios(engine: Engine) -> None:
|
|||||||
currency = EXCLUDED.currency,
|
currency = EXCLUDED.currency,
|
||||||
primary_resource = EXCLUDED.primary_resource
|
primary_resource = EXCLUDED.primary_resource
|
||||||
"""
|
"""
|
||||||
),
|
conn.execute(text(sql), payload)
|
||||||
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,10 +895,28 @@ 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 (
|
||||||
|
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 (
|
INSERT INTO financial_inputs (
|
||||||
scenario_id, name, category, cost_bucket, amount, currency, notes
|
scenario_id, name, category, cost_bucket, amount, currency, notes
|
||||||
)
|
)
|
||||||
@@ -734,9 +936,7 @@ def ensure_default_financial_inputs(engine: Engine) -> None:
|
|||||||
currency = EXCLUDED.currency,
|
currency = EXCLUDED.currency,
|
||||||
notes = EXCLUDED.notes
|
notes = EXCLUDED.notes
|
||||||
"""
|
"""
|
||||||
),
|
conn.execute(text(sql), payload)
|
||||||
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