Changelog¶
All notable changes to this project are documented in this file.
The format follows Keep a Changelog, and this project uses Semantic Versioning.
Every pull request must bump the version in pyproject.toml and add a
matching entry below; this is enforced by .github/workflows/pr-checks.yml.
[Unreleased]¶
[0.7.0] - 2026-05-16¶
Many-to-many project↔sample schema. Applied to production ccr_metadata
via schema/003_cross_project_samples.sql.
Added¶
schema/003_cross_project_samples.sql: newproject_samplesjunction table — the sole source of truth for project↔sample membership (a sample can belong to several projects). Backfills study/input/control links and canonicalizes existingSQR/SQRP.samples.link_to_project(): idempotent project↔sample link helper.samples.canonical_plate_id(): single SQR/SQRP canonicalization chokepoint (strip whitespace,NA/N/A/empty →"", padding preserved); applied on every write and reused by the importer, which now also validates plate ids and reports normalizations.subjects.get_or_create()raises on a reusedsubject_codewith a conflictingsex/origin(loud cross-study collision guard).
Changed¶
- Project membership lives entirely in
project_samples;subjectsno longer carriesproject_idandsubject_codeis now globally unique.subject → visit → sampleis pure lineage with no project affiliation. - Controls (mockIP/anchor/NC) are linked to every study project that shares their plate (SQR+SQRP) at import/migration time instead of living in dedicated control projects.
queries.*project-scoped helpers,workflows.register_subject_with_visit, and the project importer rewritten to the junction model;project_summary.n_samplesnow counts every linked sample (controls included).scripts/add_controls.pyredesigned to emit controls into the plate-sharing study bundles rather than dedicated control projects.- Docs (
schema.md,index.md,quickstart.md,reference/index.md) updated to the junction model;quickstart.mdregenerated from a live run against the migrated database.
Removed¶
- Dedicated
mockIP/anchor/NCprojects (deleted by migration 003; theinputumbrella project is retained). scripts/fix_controls_projects.py(obsolete under the new model).
Security¶
users/revoke_readwrite.sql: revoked INSERT/UPDATE/DELETE from the two analyst accounts (SELECT-only like other non-admin users) — ad-hoc writes would bypass theproject_samplesinvariant.
[0.5.2] - 2026-05-15¶
Fixed¶
README.md: corrected all documentation site links fromnoxdbtonoxDBto match the renamed GitHub Pages URL.README.md: corrected codecov badge and link to point to the renamed repository (noxDB).
[0.5.1] - 2026-05-15¶
Changed¶
docs/index.md: replaced CLI card with cards for Preparing data, Testing, and Contributing; added Gabriel Innocenti's email to the contact section.README.md: restored WIP status, codecov, and version badges that were removed in a previous documentation pass.
[0.5.0] - 2026-05-15¶
Added¶
docs/quickstart.mdsection 14: fullfetchmodule example with live output — project structure (projects.get,project_summary), file manifest (files_for_project), andfetch.export_projectproducing a local folder withmetadata.csvandREADME.txt. Also shows theinclude_files=True/layout/file_typespath for on-LiSC use.docs/data-preparation.md: new data-preparation section covering how raw exports are transformed before import.
Changed¶
docs/schema.mdfully rewritten: every table now has a complete column-by-column reference (type, nullability, constraints). Controls design documented in a dedicated section — mockIP, anchor, NC, and input each live in their own project (ids 61, 64, 67, 58 respectively) and are linked back to study projects via theSQR/SQRPplate coordinates. Nullablesex/ageand theNCENUM addition from migration002are called out inline. Previously wrong column names (meta_key→key_name,value_float→value_numeric) corrected.docs/cli.mdremoved; CLI is no longer part of the package.mkdocs.ymlupdated to reflect the new documentation structure.- DB renamed to
noxDB.
[0.4.5] - 2026-05-15¶
Added¶
scripts/add_controls.py: recovers anchor, mock, NC, and input control samples fromOverview_SQRs.csv(absent from per-project metadata CSVs) and appends them to the four master CSVs in the import directory. Input samples are placed in a standalone"input"project. Safe to re-run — already-presentsample_nameentries are silently skipped.
Fixed¶
subjects.create/subjects.get_or_create: thesex or Nonenormalisation now also converts the literal strings'NA'/'N/A'toNULL, preventing a DB-sideCHECKconstraint violation whenprepare_migration.pywrites those placeholders.- Import validation (
runner._validate_schema,runner._commit): sex and age values of'NA'/'N/A'(written byprepare_migration.pyfor real samples with missing demographics) are now treated as nullish, consistent with the DB schema allowingNULLfor both columns.
[0.4.4] - 2026-05-15¶
Added¶
schema/002_controls_support.sql: migration that makessubjects.sexandvisits.agenullable (non-null values still constrained to'M'/'F'and>= 0respectively) and adds'NC'to thesamples.sample_typeENUM.scripts/prepare_migration.py:_detect_sample_type()inferssample_typefrom substrings inSampleName(Anchor→anchor,Mock→mockIP,NC→NC,input→input). Control samples no longer generate spurious sex/age warnings and are written with blank sex/age (stored as NULL) rather than a placeholder0.users/revoke_readwrite.sql: one-time script to downgradelovro.trgovec-greifandmelanie.prinzensteinertoSELECT-only.
Changed¶
subjects.create/subjects.get_or_create:sexparameter is nowstr | None; empty string is normalised toNULLon insert.visits.create/visits.get_or_create:ageparameter is nowint | None.- Import validation (
runner._validate_schema): sex and age checks are skipped when the value is empty/null rather than raising an error. users/users.sql:lovro.trgovec-greifandmelanie.prinzensteinerreduced toSELECT-only, consistent with all other non-admin users.
[0.4.3] - 2026-05-14¶
Added¶
scripts/prepare_migration.pyandscripts/bulk_import.py: two CLI scripts for bulk-loading legacy data.prepare_migration.pytransforms a raw export directory into the canonical import folder layout expected byimport_project_from_dir;bulk_import.pywraps it to drive multiple projects in one run.
Changed¶
countsfiles may now be stored in theworktier as well asarchive. The hardarchive ↔ workflip rejection infiles._resolve_tieris removed; thescratch/externalescape hatches are unchanged.files.register/files.get_or_registergain askip_disk_checkparameter (forwarded from_inspect_file): whenTrue, path-prefix and on-disk stat/MD5 checks are skipped andfile_size_bytesis stored asNULL. Useful when registering files that are not yet mounted on the current host.
Fixed¶
paramikopinned to<4.0in theanalysisoptional-dependency group.paramiko4+ removedDSSKey;sshtunnel0.4.0 still references it, causing anAttributeErroroninit_pool()when the SSH tunnel path is taken.
Maintenance¶
migration_import/,migrations/, andnotebooks/added to.gitignore.
[0.4.2] - 2026-05-11¶
Changed¶
README.mdrewritten as a plain, copy-pasteable onboarding page: short summary of the repo's purpose, step-by-step install (system deps →git clone→ venv →pip install -e ".[analysis]"→ smoke test), minimal~/.my.cnfexample, links to the docs site for everything else. The access-tier section is dropped (lives on the docs site / is managed off-repo); the Contact section now lists Gabriel Innocenti alongside Mateusz Kołek.
[0.4.1] - 2026-05-11¶
Changed¶
- Converted all public docstrings in
noxdb(connection,projects,subjects,visits,samples,metadata,files,queries,workflows,fetch) and the_importsubpackage to Google style with explicitArgs:/Returns:/Raises:sections. No code behavior changes. The API-reference pages on the docs site now render structured parameter tables. docs.ymlrunsmkdocs buildwithout--strict. Griffe warns on the deliberately-untypedcurparameter (cursor can be either amariadbcursor or a_LoggingCursorwrapper); addingcur: Anyeverywhere would be churn without semantic gain.
[0.4.0] - 2026-05-11¶
Added¶
- Documentation site scaffold built with MkDocs + Material theme +
mkdocstrings (
mkdocs.yml,docs/{index,install,quickstart,schema, cli,contributing,changelog}.md, and per-module pages underdocs/reference/). Auto-generates API reference from package docstrings;NEWS.mdandCONTRIBUTING.mdare surfaced viamkdocs-include-markdown-plugin. CONTRIBUTING.mddocumenting dev setup, branching, the version +NEWS.mdrequirement enforced bypr-checks.yml, docstring style, and how to preview the docs locally..github/workflows/docs.yml— builds the site withmkdocs build --stricton every PR and deploys to GitHub Pages on every push tomain. Concurrency group keeps deploys serialized.docsoptional-dependency group inpyproject.toml(mkdocs-material,mkdocstrings[python],mkdocs-include-markdown-plugin).pip install -e ".[docs]"thenmkdocs serveis the local preview path.
[0.3.0] - 2026-05-11¶
Added¶
- Master import: load a whole project folder (
project.yaml,subjects.csv,visits.csv,samples.csv,files/manifest.csv) into the database in one atomic transaction. noxdb._import.import_project_from_dir(root, *, dry_run, force, compute_md5, skip_disk_check, log_dir)is the library entry point; it raisesProjectImportErrorwith an exhaustive list of collected errors when validation fails.scripts/import_project.pyis the matching CLI, exits 0 / 2 / 3 for success / validation failure / unexpected error and writes a JSON report to stdout plus<log_dir>/<ts>_<project>.log.- CSV columns prefixed with
meta_are treated as typed metadata keys; values are coerced int → float → bool → str. - Re-running on the same folder requires
--force(orforce=True). With--forcethe importer is idempotent: re-uses every existing row viaget_or_create/set_*and reportsinsertedvsexisting/unchangedper table. - Cross-project collisions on
sample_name/file_pathare blocked even with--force(those UNIQUEs are global by design). pyyaml>=6.0added as a hard dependency (required to readproject.yaml).
[0.2.3] - 2026-05-11¶
Added¶
fetchmodule that materializes a project from the database to a local folder:export_metadata_table— write the project tidy table to CSV and/or XLSX (requirespandas, plusopenpyxlforxlsx).download_files_for_project— copy every registered file for a project into a target directory. Uses paramiko SFTP when anssh_hostis configured (via~/.my.cnf [noxdb-ssh],NOXDB_SSH_*env vars, or kwargs), and falls back toshutil.copyfilewhen running on LiSC with the storage mounted locally. Supportsby_sample,by_type, andflatoutput layouts; resumes by skipping destinations that already exist.export_project— one-shot snapshot:metadata.csv(and optionallymetadata.xlsx),files/<layout>/..., and aREADME.txtsummary.paramiko>=3.0added to theanalysisoptional-dependency group.
[0.2.2] - 2026-05-11¶
Fixed¶
get_connection()now forces server-sideSET autocommit=0on every pool checkout instead of relying on the Python-sideconn.autocommitsetter. The setter elides theSETcommand when its cached flag already matches, so after a pool reset the server could be left in autocommit=1 while Python thought it was 0. Inserts then auto-committed androllback()was a silent no-op. Triggered intermittently for later-session tests; surfaced by a regression test against thesubjectstable.
Added¶
workflowsmodule bundling atomic, idempotent high-level operations on top of the per-table CRUD helpers:register_subject_with_visit—subjects.get_or_create+visits.get_or_create+ optionalvisit_metadataupsert, all in one transaction.register_sample_with_files—samples.get_or_create+ optionalsample_metadataupsert + per-filefiles.get_or_register, all in one transaction. A disk-validation error on any file rolls back the sample and its metadata too.- Both workflows take an optional
cur=None; when None they open their owntransaction(), otherwise they piggyback on the caller's cursor viacontextlib.nullcontext, so they compose cleanly inside a larger atomic block.
[0.2.1] - 2026-05-11¶
Added¶
- Composite read-only
queriesmodule innoxdb: samples_for_project— joinprojects → subjects → visits → sampleswith optional filters onfile_type,sample_type, andhas_files.samples_with_metadata/project_tidy_table— EAV pivot fromsample_metadata(and optionallyvisit_metadata) into a wide-formpandas.DataFrame. Visit-level keys colliding with sample-level keys are renamed with avisit_prefix. Metadata key names are filtered against^[A-Za-z_][A-Za-z0-9_]*$for safe DataFrame columns.files_for_project—sample_filesjoined with parent identifiers, filtered byfile_typeand/orstorage_tier.project_summary— counts of subjects/visits/samples/files plus per-file_typebreakdown (returns adict, no pandas dependency).find_db_files_missing_on_disk— DB rows whosefile_pathis gone from disk; suitable for a cron sweep.find_disk_files_missing_in_db— regular files underNOXDB_ARCHIVE_ROOT/NOXDB_WORK_ROOT(or caller-provided roots) that are not registered insample_files.integrity_check— per-project report covering samples without files, archive files without MD5, and files outside their tier root.analysisoptional-dependency group inpyproject.tomlforpandas+openpyxl. Tests depend onpandasand skip DataFrame assertions when it is missing.
[0.2.0] - 2026-05-09¶
Added¶
- Per-table CRUD modules in
noxdb: projects—create,get,get_by_name,get_or_create,list_all,update,delete,exists(id/name XOR),count.subjects— same shape keyed on the composite(project_id, subject_code), pluslist_for_project/count_for_project.visits— keyed on(subject_id, timepoint);get_or_createrejects NULL timepoints because the UNIQUE doesn't deduplicate them.samples—sample_nameis globally UNIQUE; module mirrors theprojectsshape withlist_for_visit/count_for_visit.metadata— shared EAV wrapper forvisit_metadataandsample_metadatawith explicitset_visit / get_visit / list_for_visit / delete_visit(and_samplemirrors). Idempotent viaINSERT … ON DUPLICATE KEY UPDATE;set_*returns"inserted" | "updated" | "unchanged". Bool stored in BOOLEAN is coerced back to Pythonboolon read.files— filesystem-validating registration forsample_files: absolute path + regular-file check, fastq/bam extension validation, file-type → tier derivation (archive vs work),os.path.realpathprefix check that blocks symlink escapes and sibling-prefix paths, optional chunked MD5 (compute_md5) or caller-supplied checksum,register,get_or_register(idempotent onfile_pathand UNIQUE-violation race-safe),restat, plus the standard CRUD helpers. Roots configurable viaNOXDB_ARCHIVE_ROOT/NOXDB_WORK_ROOT(defaults/lisc/archive,/lisc/work).update(storage_tier=…)enforces the samefile_type → tierinvariant asregister()— flippingarchive↔workis rejected;scratch/externaloverrides are still allowed.
Changed¶
- CI now runs
pytest --covagainstsrc/noxdb(branch coverage), prints a missing-lines report in the workflow log, and uploadscoverage.xmlas a build artifact.pytest-covis a new optionaltestdependency; coverage settings live under[tool.coverage.*]inpyproject.toml. _LoggingCursorandexecute()passparamsthrough unchanged when the caller provides an empty container; onlyparams is Noneis substituted with()._log_if_writenow strips leading SQL comments and an optionalWITH …CTE before classifying the statement, so audited writes are no longer missed when the query starts with comments or a CTE.get_connection()cleanup wrapsrollback()andclose()in their own try/except so the original DB exception is preserved; rollback / close failures are logged instead of masking it.
Fixed¶
seed/load_fake_data.load()now closes its cursor in atry/finally, preventing cursor leaks back into the pool..github/workflows/pr-checks.ymlversion-extraction commands no longer abort the step ongrepmisses; the explicit-zguard now reports the error as intended.
[0.1.1] - 2026-05-08¶
Added¶
- Initial schema in
schema/001_initial.sql: project → subject → visit → sample hierarchy with EAV metadata tables (visit_metadata,sample_metadata) andsample_filesfor tracking file pointers. - User and role definitions in
users/users.sql(admin / read-write / read-only tiers, restricted tolisc.%hosts). noxdbPython package with:- Connection pool (
init_pool,close_pool,get_connection). transaction()context manager with audit-logging cursor wrapper.execute()helper returning rows aslist[dict].- Audit log of write statements at
~/.noxdb/audit.log(override viaNOXDB_AUDIT_LOG). - Credentials read from
~/.my.cnf[noxdb]section by default; overridable per-call viainit_pool(...)keyword arguments and via theNOXDB_DATABASEenv var. - Fake-data seed script (
seed/load_fake_data.py) covering all four EAV value types and a longitudinal subject example. - CI workflow running pytest against MariaDB 10.11.