Repository navigation
Dump the files collection to a flattened relational DuckDB database — Closes #123 - #124
Open
conradbzura wants to merge 4 commits into
Open
conradbzura wants to merge 4 commits into
conradbzura wants to merge 4 commits into
Conversation
conradbzura
marked this pull request as ready for review
October 27, 2026 14:46
The dump is an offline batch tool most deployments never run, so its only dependency stays out of the API image: pip install cfdb[duckdb]. The dev extra carries it so the test suite can exercise the loader, and package-data ships the dump schema DDL inside the wheel, which src-layout auto-discovery would otherwise omit.
Add cfdb dump duckdb, which streams the materialized files collection into a single DuckDB database whose schema of record is the checked-in src/cfdb/dump/schema.sql: C2M2-mirroring entity and junction tables on natural (id_namespace, local_id) keys, promoted cross-consortium common columns, per-consortium extension tables carrying only the unpromoted residue, three convenience views, and provenance tables (dump_info, dump_stat, dump_conflict) that make every dump self-describing. The loader validates each document through FileMetadataModel, exactly as the GraphQL resolvers hydrate the same documents, so the model's read validators apply identically on both paths. Entity rows repeated across documents are first-write-wins with divergence counted and sampled into dump_conflict; junction rows take the union over all copies so a relationship carried by only one copy is never dropped. A key resolving to two submissions aborts the dump, since the global natural keys rest on id_namespaces being DCC-scoped. High-cardinality rows stream through constraint-free TEMP staging tables so memory is bounded by the entity registries rather than the file count, and foreign keys are declared up front because DuckDB has no ALTER TABLE ADD CONSTRAINT, with finalize inserting parents before children.
Exercise the public surface end to end against a real in-memory DuckDB: schema bootstrap, flattening and junction recovery, first-wins entity dedup with divergence sampling, junction union semantics, both DumpError abort conditions, coercion and provenance accounting, stub project upgrades, JSON and list column fidelity, all three views, and the full CLI option surface including partial-output cleanup. Two Hypothesis properties pin the cardinality invariants under arbitrary file/collection sharing and byte-exact text roundtrips through model validation and parameterized insert.
conradbzura
force-pushed
the
123-dump-files-to-duckdb
branch
from
October 27, 2026 18:15
fa8c6b9 to
06cbb4c
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add
cfdb dump duckdb, which writes the materializedfilescollection — a denormalized document projection shaped for serving one file's metadata — into a single relational DuckDB database shaped for interrogating the corpus. The dump recovers the C2M2 entity and junction tables the materializer joined away, keyed by natural(id_namespace, local_id)composites throughout, and resolves the cross-consortium enrichment question in two tiers: a field with a common semantic slot occupies a promoted column (genome_assembly,output_type,experiment_type,analyte_class, …), and the residue occupies 1:1 per-consortium extension tables that omit verbatim-promoted fields while retaining any field whose promotion is derived rather than verbatim (replicate_infosurvives because the replicate columns are regex-extracted from it). The schema of record is the checked-insrc/cfdb/dump/schema.sql, versioned by itsschema_versionheader — an artifact in the same sense asschema.graphql.Three loader policies carry the correctness argument. Entity rows repeated across documents resolve first-write-wins, with every divergent later copy counted and a bounded sample written to
dump_conflict— divergence is reported, never silently reconciled. Junction rows take the union over all copies, a deliberate asymmetry: a divergent entity column is observable indump_conflict, whereas a relationship dropped by first-wins would vanish without trace. A key resolving to two submissions aborts the dump, because the schema's global natural keys rest on id_namespaces being DCC-scoped, and the loader asserts that premise rather than assuming it — on the stub paths as much as the ordinary ones — and duplicate file keys abort on the same grounds. The policy's boundary is uniform: model-valid dirt that cannot satisfy the schema, such as a CV term without an id or a name, is skipped and counted at the decision point rather than surfacing as a constraint crash at finalize, and any residual finalize-time constraint violation is translated into aDumpErrornaming the table. Document validation reusesFileMetadataModel— the same hydration path the GraphQL resolvers run — so the dump and the API read the corpus through one set of read validators.The design decisions follow the resolutions recorded against the issue's open questions: natural composite keys rather than surrogates,
submissionas a non-key column, globally deduped ontology tables, and an on-demand CLI with Parquet export. Scheduled publication of dump artifacts remains out of scope.Closes #123
Proposed changes
Schema (
src/cfdb/dump/schema.sql)Define 29 tables in four tiers — provenance (
dump_info,dump_stat,dump_conflict), reference (dcc,project, five ontology vocabularies), C2M2 entities and junctions under their upstream names, and per-consortium extensions — plus three views:v_file(term names and extension columns inlined, one row per file),v_file_biosample(the file × collection × biosample × subject fan-out grain, named so nobody sumssize_in_bytesover it unawares), andv_subject_demographics(sex/race/age coalesced across the C2M2 columns and HuBMAP enrichment, each value paired with a*_sourcecolumn so provenance survives the coalesce). Gateage_unitto the HuBMAP branch of the age coalesce — the unit belongs toage_valuealone, and a C2M2-sourced age is years by definition — carried asschema_version2. Declare every foreign key in the DDL, since DuckDB has noALTER TABLE … ADD CONSTRAINT.Loader (
src/cfdb/dump/loader.py)Stream documents through a two-tier accumulator: low-cardinality entities live in Python registries for column-level divergence comparison, while files, their 1:1 extras, and every junction stream through constraint-free
TEMPstaging tables, bounding memory by entity cardinality rather than file count. Finalize in foreign-key order — dcc → project → ontology → entities → file → junctions and extensions — with junctions deduplicated bySELECT DISTINCT. Coerce the known string-typed numerics from the C2M2 TSV path and ISO timestamps, recording each failure as a named counter rather than failing the document; skip documents that fail model validation, likewise counted. Route every CV-term foreign key through one guard that skips and counts a term lacking an id or a name, nulling the referencing column in the same decision. Assert the dcc id↔submission mapping 1:1 in both directions, run the two-submission assertion ahead of the stub short-circuits, and translate any residual finalize-timeConstraintExceptioninto aDumpErrornaming the table, so every keying-assumption violation is one legible diagnostic. Register stub project rows for foreign keys whose joined object never materialized, upgradeable by a later document carrying the real row, with bothproject_stubs_createdandproject_stubs_survivingreported. Anchor thedump_infocommit probe to the package directory with a trackedness check, so only a genuine cfdb checkout ever yields a sha and an analyst's own repository never does. Serialize HuBMAPmetadatawith sorted keys so key order never registers as divergence, parseSCHEMA_VERSIONfrom the DDL header so the constant cannot drift from the schema, and fail that parse with a message naming the required header line.CLI (
src/cfdb/cli.py)Add a
dumpgroup with theduckdbcommand: a--submissionrestriction validated against the DCC registry viaclick.Choice,--to-parquetexport viaEXPORT DATABASE, a--forceoverwrite guard, a lazyduckdbimport whose failure message names the installablecfdb[duckdb] @ git+…form, and Mongo connection kwargs reused fromcfdb.db.mongo_client_kwargs. Remove the partial output on every abnormal exit path — cleanup runs in afinally, soKeyboardInterruptis covered — while a completed dump is never destroyed: the Parquet export runs outside the destroy-on-failure scope, and an export failure keeps the.duckdb, reports the error, and exits nonzero naming what survived. Fix the shared--debugcallback to honor its port argument; this deliberately changescfdb sync -das well, which previously ignored the value and listened on 5678.Packaging (
pyproject.toml)Gate the sole new dependency behind a
duckdbextra so the API image is unaffected, carry it in thedevextra for the test suite, and ship the schema DDL via package-data, which src-layout auto-discovery would otherwise omit from the wheel.Documentation (
README.md)Document the command, the schema tiers, the per-consortium common-column mapping tables, and the loader's dedup, assertion, and coercion policies under a new "DuckDB dump" section — including that counters measure occurrences rather than entities, and that a database without a
dump_inforow is a partial dump.Tests (
tests/test_dump.py,tests/test_cli.py)Mirror the module layout: loader, schema, and view tests live in
tests/test_dump.py, while the CLI command tests and their Mongo-patching helper live intests/test_cli.py, matchingcfdb/cli.py.Test cases
TestDumpReportsummary()is calledTestDumpReportsummary()is calleddump_filesdump_filesdump_filesdump_filesdump_conflictdump_filesdump_filesDumpErrornaming both submissionsdump_filesDumpErroron duplicate file keysdump_filesConstraintExceptionis raiseddump_filesDumpErrornaming both submissionsdump_filesDumpErrornaming both submissionsdump_filessize_in_bytesand sidecarfile_sizedump_filessize_in_bytesdump_filesfile_formatterm carrying an empty idterm_id_missingcounteddump_filesfile_formatterm with an id and a blank nameterm_name_missingcounteddump_filesdump_filesreplicate_infopresentdump_filesdump_filesdump_infocarries schema version, filter, and file countdump_filesdump_info.cfdb_commitis not that repository's HEADdump_filesv_subject_demographicsis querieddump_filesv_subject_demographicsis queriedage_unitdump_filesdump_filescreation_timeanddate_createdstringsdump_filescreation_timewith a +05:00 offsetdump_filesdump_conflict, no errordump_filesdump_filesdump_filesconflict_sample_cap=1dump_filesdump_infocarries the sorted seen submissionsdump_filesdump_filesVARCHAR[]dump_filesdump_filesbatch_size=1dump_filescompression_formatdump_filesv_fileis queriedv_fileinliningdump_filesv_file_biosampleis queriedv_file_biosamplegraindump_filesdump_duckdb (CLI)cfdb[duckdb]extradump_duckdb (CLI)-s 4DNdump_duckdb (CLI)--forcedump_duckdb (CLI)--forcedump_duckdb (CLI)--to-parquetdirectorydump_duckdb (CLI)--to-parquettarget inside an unwritable directory.duckdbkept on diskdump_duckdb (CLI)DumpErrormessage printed, partial output removed