losalamos.tools.database#
Database Manager#
A standalone Python tool for creating and managing environmental science databases in GeoPackage (SQLite) format. Designed around two primitive but scalable schemas for spatial and time series data.
Everything runs through a single file — database.py — driven by JSON specs.
pip install geopandas fiona pandas
Usage#
python database.py --command initialize --parameters path/to/specs.json
python database.py --command append --parameters path/to/specs.json
python database.py --command update --parameters path/to/specs.json
Add --verbose for debug-level logging.
The specs file orchestrates all parameters. Multiple commands can coexist in the
same file; only the one named by --command is executed. Each command holds a
list of procedures, allowing batches.
Schemas#
Two primitive schemas cover most environmental science use cases.
In-situ measurements
The canonical case: temperature, precipitation, river level — anything measured continuously at fixed locations. Works for point geometries (stations, gauges) and polygon geometries (census tracts, regular grids, catchments).
sites is the core geospatial layer. Wide table.
field |
type |
notes |
|---|---|---|
|
int PK |
auto-generated |
|
text |
sourced from provider or created; must be unique; indexed |
|
text |
short label; sized for composite names |
|
text |
inline citation or URL |
|
text |
e.g. climate station, rain gauge |
|
text |
comments on unusual sites, etc |
|
geometry |
usually point or polygon |
code is the source of identity. Duplicates not allowed on update.
measurements is the core data. Long table. Non-spatial.
field |
type |
notes |
|---|---|---|
|
int PK |
auto-generated |
|
int FK |
→ sites |
|
int FK |
→ attributes |
|
text |
|
|
int |
data level flag → tier table; DEFAULT 0 |
|
int |
quality flag → quality table; DEFAULT 0 |
|
real |
Each site_id–attribute_id–datetime–tier–quality combination is unique and indexed.
Default assimilation skips existing records (append); overwrite is explicit (update).
The time key in specs controls datetime resolution. If absent or null, defaults to
true (full datetime). Set to false for date-only storage.
attributes — semantic metadata for each measured variable.
field |
type |
notes |
|---|---|---|
|
text |
unique identifier |
|
text |
short label |
|
text |
full name |
|
text |
LaTeX symbol |
|
text |
LaTeX units |
|
text |
|
|
text |
math domain: |
|
real |
valid range |
|
text |
system classification: flow, level, ratio, etc |
|
text |
thematic field: e.g. Hydrology > Hillslope > Isotopes |
tier and quality — small lookup tables for documenting flags.
field |
type |
|---|---|
|
int (unique) |
|
text |
|
text |
|
text |
|
text |
storage — optional compression metadata per attribute.
field |
type |
notes |
|---|---|---|
|
int FK |
→ attributes |
|
text |
e.g. |
|
real |
default 1.0 |
|
real |
default 0.0 |
actual_value = (stored_value × scale) + offset
If the table is empty or an attribute has no entry, scale=1 and offset=0 are
assumed. This applies to both retrieval and assimilation.
Off-site measurements
For environmental quality assessments: samples collected in the field, analysed later in the lab. Sites produce samples (a bottle, a soil core) at a given datetime. Samples produce measurements at a (possibly different) analysis datetime.
Multiple measurements can come from one sample — e.g. three replicate analyses entering
as tier=0 (raw), their average entering as tier=1 (consisted).
The quality flag handles detection limits (value receives the limit, quality maps to “less than”) and super-large values (value as power of 10, quality encodes the exponent).
samples — field collection catalog. Wide table.
field |
type |
notes |
|---|---|---|
|
int PK |
|
|
text |
|
|
int FK |
→ sites |
|
text |
sampling datetime |
|
text |
field comments |
Identity is code–site_id–datetime. Multiple samples at the same site and date are
allowed if they have different codes.
measurements — same structure as in-situ, but site_id becomes sample_id
(FK → samples).
Commands#
initialize
Creates a new GeoPackage database and loads seed data. Fails if the database already exists.
python database.py --command initialize --parameters specs.json
"initialize": [
{
"database": "path/to/output.gpkg",
"category": "in-situ",
"time": true,
"sep": ";",
"tables": {
"sites": {"file": "path/to/sites.gpkg", "layer": "sites"},
"tier": "path/to/tier.csv",
"quality": {"file": "path/to/quality.csv", "sep": ",", "header_lines": 2},
"attributes": {"file": "path/to/attributes.csv", "sep": " "},
"storage": null,
"measurements": "path/to/seed_data.csv"
},
"extra_sql": ["path/to/extra_fields.sql"]
}
]
category:"in-situ"or"off-site"time:true(default) →YYYY-MM-DD HH:MM:SS;false→YYYY-MM-DDstorage: null→ table is created empty; scale=1, offset=0 assumed everywheremeasurements(andsamplesfor off-site) at init time are optionalextra_sql: optional list of.sqlfiles for extra columns or extra tables
append
Adds new rows to any table. Records that already exist (matching the identity index) are silently skipped. Batching is allowed.
python database.py --command append --parameters specs.json
"append": [
{
"database": "path/to/output.gpkg",
"table": "measurements",
"data_file": "path/to/new_data.csv",
"sep": ";",
"header_lines": 4
}
]
update
Same as append, but existing records that collide with incoming data are overwritten.
Records with no collision are left untouched. New records are inserted normally.
python database.py --command update --parameters specs.json
"update": [
{
"database": "path/to/output.gpkg",
"table": "attributes",
"data_file": "path/to/revised_attributes.csv"
}
]
CSV Loading#
The default field separator is ;. Two optional keys control how each file is read.
key |
type |
description |
|---|---|---|
|
string |
field separator: |
|
int |
number of preamble lines to skip before the header row |
Options can be set at two levels, with the per-table value taking priority:
Per-table — inside the table spec dict in
tablesProcedure-level — at the top of the procedure dict, as a fallback for all CSV tables in that step
A plain string path (no dict) always inherits from the procedure level. If neither
level specifies a key, the built-in default applies (sep=";", header_lines=0).
Missing columns. Incoming data does not need to supply every column in the target table. The assimilation engine handles absent columns as follows:
Identity columns with a schema default (e.g.
tier,quality— bothDEFAULT 0in the measurements table): padded with the schema default value.Non-identity optional columns (e.g.
abstract,symbol,units): padded withNULL.Identity columns with no default (e.g.
site_id,attribute_id,datetime): missing → hard error.
Extra columns in the incoming file that have no matching column in the target table are dropped with a warning.
Foreign Key Resolution#
Incoming CSV data carries human-readable codes, not internal integer IDs. The assimilation engine resolves codes to IDs automatically before inserting.
For measurements (in-situ) the CSV uses a site or site_id column resolved
against sites.code, and an attribute or attribute_id column resolved against
attributes.code. The engine tries several column name candidates in order:
site_id→site→sites_code→site_code→code
Unresolvable codes raise an error listing the offending values.
Identity#
Each table has a defined set of columns that jointly identify a unique record.
table |
identity columns |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Extensibility#
Extra columns — write an .sql file with ALTER TABLE ... ADD COLUMN ...
statements and list it under extra_sql in the initialize spec. Useful additions
include sites.area_km2 for polygon-based grids or catchment areas.
Extra tables — same mechanism. A CREATE TABLE in the .sql file adds a new
table at initialization. Useful for site_attributes populated from spatial joins.
Both are per-database: different databases initialized from different specs can have different extra columns and tables without touching the core schema.
Notes#
The sites table is created and owned by the GeoPackage driver (via geopandas/Fiona),
ensuring the file is readable by QGIS, GDAL, and any GeoPackage-compliant library.
All other tables are plain SQLite tables inside the same file.
SQLite stores TEXT and DATETIME identically at the storage level. Datetime
columns are declared TEXT and formatted as ISO 8601 (YYYY-MM-DD HH:MM:SS),
which guarantees correct lexicographic ordering and compatibility with SQLite’s
built-in date/time functions (strftime, date, between, etc.).
The tool uses PRAGMA foreign_keys = ON and PRAGMA journal_mode = WAL.
Functions
|
Align df to the target table's column set. |
|
|
|
|
|
|
|
Heuristic: off-site if 'samples' table exists, else in-situ. |
|
Infer datetime resolution from an existing measurements row. |
|
|
|
|
|
Write minimal GeoPackage metadata tables into a fresh database. |
|
Return {code: pk_id} for any table with a code column. |
|
Open (or create) a GeoPackage file. |
|
Execute an external SQL file (for extra_sql extensions). |
|
Execute a block of SQL statements against con. |
|
Return the primary key column name — 'fid' for Fiona tables, 'id' otherwise. |
|
|
|
|
|
Return column metadata keyed by column name. |
|
Write a GeoDataFrame as a GeoPackage spatial layer via geopandas/Fiona. |
|
Insert df into table with duplicate handling. |
|
Load a delimited text file into a DataFrame. |
|
|
|
|
|
|
|
Replace code-based columns with integer FK ids. |
|
- losalamos.tools.database.gpkg_connect(path: Path) Connection[source]#
Open (or create) a GeoPackage file. Bootstraps metadata if new.
- losalamos.tools.database.gpkg_bootstrap(con: Connection) None[source]#
Write minimal GeoPackage metadata tables into a fresh database.
- losalamos.tools.database.gpkg_exec_sql(con: Connection, sql: str) None[source]#
Execute a block of SQL statements against con. Writes to a temp file in the module folder, executes, deletes. The file stays on disk if execution crashes — useful for debugging.
- losalamos.tools.database.gpkg_exec_file(con: Connection, sql_path: Path) None[source]#
Execute an external SQL file (for extra_sql extensions).
- losalamos.tools.database.gpkg_write_spatial(gdf: geopandas.GeoDataFrame, gpkg_path: Path, layer: str, crs: str = 'EPSG:4326') None[source]#
Write a GeoDataFrame as a GeoPackage spatial layer via geopandas/Fiona.
- losalamos.tools.database.gpkg_table_info(con: Connection, table: str) dict[str, dict][source]#
Return column metadata keyed by column name. Each value is a dict with keys: type, notnull, dflt_value.
- losalamos.tools.database.gpkg_pk_col(con: Connection, table: str) str[source]#
Return the primary key column name — ‘fid’ for Fiona tables, ‘id’ otherwise.
- losalamos.tools.database.gpkg_code_id_map(con: Connection, table: str, code_col: str = 'code') dict[str, int][source]#
Return {code: pk_id} for any table with a code column.
- losalamos.tools.database.detect_category(con: Connection) str[source]#
Heuristic: off-site if ‘samples’ table exists, else in-situ.
- losalamos.tools.database.detect_has_time(con: Connection) bool[source]#
Infer datetime resolution from an existing measurements row.
- losalamos.tools.database.load_csv(path: str | Path, sep: str = ';', skiprows: int = 0) pandas.DataFrame[source]#
Load a delimited text file into a DataFrame.
Parameters#
sep : field separator (default “;”) skiprows : number of preamble lines to skip before the header row
- losalamos.tools.database.load_spatial(path: str | Path, layer: str | None = None) geopandas.GeoDataFrame[source]#
- losalamos.tools.database.resolve_fk(df: pandas.DataFrame, con: Connection, fk_rules: dict[str, dict]) pandas.DataFrame[source]#
Replace code-based columns with integer FK ids.
Incoming CSV may use the FK column name (e.g. ‘site_id’) containing codes, OR alternative names like ‘sites_code’ or ‘site_code’. All candidates are tried.
- losalamos.tools.database.normalise_datetime(df: pandas.DataFrame, has_time: bool) pandas.DataFrame[source]#
- losalamos.tools.database.align_columns(df: pandas.DataFrame, db_cols: list[str], identity_cols: list[str], col_info: dict[str, dict] | None = None) pandas.DataFrame[source]#
Align df to the target table’s column set.
Columns in df that don’t exist in the table are dropped (with a warning).
Missing identity columns are a hard error UNLESS the schema declares a DEFAULT value for that column — in which case they are padded with it. This covers tier/quality (DEFAULT 0) arriving without those columns.
Missing non-identity columns are padded with their schema DEFAULT if one exists, otherwise NULL. A debug message notes each padded column.
The auto-generated pk columns (id, fid) are always excluded from the result.
col_info is the output of gpkg_table_info(). If None, missing columns are always padded with NULL (no default awareness).
- losalamos.tools.database.insert_rows(con: Connection, table: str, df: pandas.DataFrame, identity_cols: list[str], mode: Literal['append', 'update'] = 'append') dict[str, int][source]#
Insert df into table with duplicate handling.
append : INSERT OR IGNORE — duplicates skipped update : INSERT OR IGNORE + targeted UPDATE for collisions (never DELETEs rows, so FK children are safe)