Database schema and migration
FlexTool's input data lives in Spine databases (SQLite plus a thin
Python layer from spinedb_api). The schema — entity classes, parameter
definitions, value lists, alternatives — is template-driven: every
JSON file under flextool/schemas/
(consolidated from the older top-level version/ and textual_templates/
directories) represents one snapshot of the schema. Migrations chain
those snapshots forward, so a user's database can always be brought up
to the latest version without losing data.
This page is a pointer for developers who need to read or modify the
schema. For the user-facing meaning of individual parameters, see
reference.md. For where this DB sits in the
overall data flow, see architecture.md.
The current schema
The canonical, fully-populated snapshot is:
schemas/spinedb_schema.json
It lists every entity class, parameter definition, default value, value
list, alternative, and parameter-group assignment that FlexTool knows
about at the current version. New empty input databases are built from
this file. The GUI's parameter-group picker (see
excel_interface.md for the same groupings
used in the Excel layer) reads its categorisation from here.
The current target version is the integer constant FLEXTOOL_DB_VERSION
in flextool/update_flextool/__init__.py.
The same constant is read at write time by
flextool/process_inputs/write_to_input_db.py, so the schema version a
fresh write announces matches the migration target.
Output-side databases use a parallel set of templates:
flextool_template_results_master.json— output DB schemaoutput_settings_template.json,output_info_template.json— output config seedscomparison_settings_template.json— scenario-comparison config seed
Migrations
flextool/update_flextool/db_migration.py walks a user's database
forward through versioned steps. Each step either:
- imports a small
flextool_template_v<N>.jsonsnapshot (e.g.flextool_template_rolling_window.json,flextool_template_lifetime_method.json), or - runs a hand-written Python step that adds/removes/renames specific entity classes, parameter definitions, value lists, or alternatives.
The chain is forward-only — there are no down-migrations. Each
step bumps the model.version parameter stored in the DB so the
migration is resumable from where it left off.
Idempotent commits
Migration steps wrap their commit_session calls in _commit_step,
which swallows spinedb_api.exception.NothingToCommit. A step that
was already hand-applied by an earlier partial run is treated as a
no-op rather than a fatal abort, so the version bump still
persists and later steps run normally.
Skim migrate_database in db_migration.py to see the full chain and
the per-version diffs.
Recent schema versions
A few recent steps worth knowing about (the chain in db_migration.py
is the authoritative inventory):
- v52 — multi-solver selection parameters. Adds the
solve_advancedparameter group on thesolveentity (solver,solver_io_api,solver_options,solver_time_limit,solver_mip_gap,solver_threads,solver_log_level) so each solve can dispatch to a different LP / MIP solver. (Several of these were later folded or removed at v56 — see below.) See Solver selection for the user-facing story. - v54 —
storage_binding_methodarray → scalar. The previously array-valued parameter is collapsed to a single scalar via a priority-based reduction. Array values were previously summed silently innodeBalance_eq; the v54 step rejects that pattern at ingestion and the single scalar enforces the one-method-per-node invariant. - v55 —
storage_binding_methodrename + value_list extension. Renames the three legacy members (bind_within_timeset→bind_within_timeblock,bind_using_blended_weights→bind_within_solve_blended_weights,bind_within_model→bind_within_solve) and adds two new variants (bind_within_period_blended_weights,bind_forward_only_blended_weights) plus the orthogonal aggregation methodbind_intraperiod_blocks. The migration also refreshes the parameter description text. Seereference.mdfor the user-facing enumeration and silent-degrade behaviour. - v56 — solver-knob consolidation. Retypes
solve.solver_argumentsto a 1d-map (solver option name → value) and folds the v52-era per-solve solver knobs into it or onto CLI flags:solver_optionsfolds intosolver_arguments;highs_method/highs_parallel/highs_presolvefold into thesolver/parallel/presolvekeys ofsolver_arguments;solver_threads→--highs-threads,solver_log_level→--solver-log-level,solver_time_limit→--solver-time-limit,solver_io_api→--matrix-file-format, anduse_row_scaling→ the--scalingCLI flag (envFLEXTOOL_SCALING). The survivingsolve_advancedDB parameters aresolver,solver_arguments,solver_mip_gap, andsolver_precommand. See Solver selection.
A separate hand-coded step (independent of the numbered chain)
migrates legacy databases that carried has_storage = yes without
has_balance = yes, retagging the node as node_type = storage so
the v4 single-node_type convention takes hold.
CLI: flextool-migrate-database
flextool-migrate-database path/to/input_data.sqlite
Implemented in flextool/cli/cmd_migrate_database.py.
Runs the migration chain idempotently against the given SQLite file.
Re-running on an already-current DB is safe — every step is a no-op.
initialize_database
flextool/update_flextool/initialize_database.py creates a fresh empty
database from a JSON template (defaults to
schemas/spinedb_schema.json). Used by:
- the GUI's Add empty input DB action,
update_flextoolwheninput_data.sqliteis missing,- session-scoped pytest fixtures that need a clean DB.
update_flextool / self_update.py
The user-facing updater is implemented in
flextool/update_flextool/self_update.py.
On each run it:
git pullthe FlexTool source treepip install --upgradeto refresh Python dependencies- Run
migrate_databaseagainst every.sqliteundertemplates/and against the user'sinput_data.sqliteif present - Initialise the input DB from the master template if it doesn't exist
This is what the GUI's Update button invokes.
Adding a schema change
A typical workflow when introducing a new parameter or entity class:
- Pick the next free version number
N(one above the currentFLEXTOOL_DB_VERSION). - Either add a small
schemas/pre_v26/flextool_template_v<N>.jsonsnapshot that imports cleanly viaspinedb_api.import_data, or write a hand-codedadd_parameters_manual/remove_parameters_manual/add_value_list_manual/add_relationships_manualstep. Recent steps indb_migration.pyare good templates. - Append the new step to the
while next_version <= new_version:chain inmigrate_database. - Bump
FLEXTOOL_DB_VERSIONinflextool/update_flextool/__init__.py. - Mirror the change into
schemas/spinedb_schema.jsonso freshly initialised DBs land at the new schema directly. Thesync_master_json_template.pyhelper (CI runs it with--verify) checks that master is consistent with the migration chain. - Add a test if the migration is non-trivial — see
testing.mdfor where to put it.
Tip
Steps that touch existing rows should be tolerant of partial state:
use update_item / add_update_item so re-applying the step on a
DB that has already been migrated is a no-op.
Related pages
architecture.md— overall data flow from input DB through the engine to outputsreference.md— user-facing parameter docs that follow the schema groupingsexcel_interface.md— where the parameter-group picker also surfaces