| table | tier | rows | objects | partitioned by | bytes | last changed |
|---|---|---|---|---|---|---|
| climatology | core | 768,880 | 71 | measurement_type | 9.9 MB | v2026.09.06 |
| cruise | core | 842 | 1 | 19.6 kB | v2026.09.04 | |
| dataset | core | 16 | 1 | 14.7 kB | v2026.09.04 | |
| dataset_taxon | core | 1,917 | 1 | 57.9 kB | v2026.09.04 | |
| grid | core | 218 | 1 | 103 kB | v2026.08.25 | |
| lookup | core | 26 | 1 | 1.6 kB | v2026.08.25 | |
| measurement_type | core | 200 | 1 | 12 kB | v2026.09.04 | |
| obs_attribute | core | 458,184 | 1 | 1.8 MB | v2026.09.04 | |
| obs_bio | core | 1,258,665 | 1 | 25.3 MB | v2026.09.04 | |
| obs_env | core | 25,006,583 | 84 | measurement_type | 317.2 MB | v2026.09.04 |
| region | core | 4 | 1 | 4 kB | v2026.08.25 | |
| sample | core | 1,469,155 | 1 | 25.9 MB | v2026.09.04 | |
| sample_measurement | core | 589,603 | 1 | 2 MB | v2026.08.25 | |
| sample_spatial | core | 929,664 | 1 | 7.3 MB | v2026.09.04 | |
| ship | core | 49 | 1 | 1.2 kB | v2026.08.25 | |
| spatial | core | 13,206 | 1 | 47.8 MB | v2026.08.25 | |
| spatial_attribute | core | 148,461 | 1 | 538 kB | v2026.08.25 | |
| taxon | core | 2,614 | 1 | 87.8 kB | v2026.09.06 | |
| taxon_group | core | 441 | 1 | 2.8 kB | v2026.09.04 | |
| obs | deprecated | 26,265,248 | 16 | dataset_key | 400.6 MB | v2026.09.04 |
| obs_ctd_full | supplemental | 271,394,164 | 134 | cruise_key | 1.4 GB | v2026.08.25 |
| obs_mets_full | supplemental | 19,927,416 | 49 | cruise_key | 250.5 MB | v2026.08.25 |
| sample_root | supplemental | 421,454 | 1 | 10.5 MB | v2026.09.04 |
7 Releases
The integrated database is published as frozen releases: an immutable, versioned copy of every table, with a catalog that says exactly which bytes it holds and a DOI that names it. Nothing a consumer reads is ever edited in place. This chapter says what a release is, what ships with it, which versions are kept, and what is checked before one is cut.
7.1 A release in one line
The promoted release is v2026.09.06, cut 2026-09-06: 23 tables, 348,657,010 rows, 2.46 GB of parquet, DOI 10.5281/zenodo.22514953. A version is a date (vYYYY.MM.DD), and a release is cut whenever the data or the schema change enough to matter — every few weeks, in practice. latest.txt on the bucket names the promoted one, and it is promoted only after the consumer-contract suite has passed against the frozen files.
7.2 What ships
Everything lives under one public prefix, gs://calcofi-db/ducklake/releases/{version}/, read over HTTPS with no credentials:
catalog.json— the structural manifest and the data-access contract: the version, its DOIs and citation, and for every table its rows, itscontent_hashand itsobjects[]— one parquet object for a single-file table, one per partition for a partitioned one — each with a bucket-relativepath,bytes,sha256andsince. Resolve a table through it (calcofi4r::cc_release_sources(),calcofi4py.release_sources()); never build a path by hand.- the parquet objects, content-addressed under
gs://calcofi-db/ducklake/tables/{table}/…: immutable and shared between versions, sosinceon an object is the first release that shipped those exact bytes — the per-table changelog. A release’s ownparquet/copy exists only for the promoted version, its predecessor and the consolidated milestones (below). - the sidecars:
metadata.json(every table and column, with units and descriptions),relationships.json(every key, Keys and integrity),integrity.json(the keys measured),datasets.json(one record per dataset — the record every portal reads),eml/(one EML document per dataset),coverage.jsonandcoverage_stations.json(what the Explorer paints first),grid.geojson,spatial.geojson,spatial_layers.json, a staticstac/catalog,erd.mmd,test_results.json, andRELEASE_NOTES.md.
Table 7.1 is that catalog rendered: every table of the promoted release, how many objects carry it, and the version its bytes last changed in.
7.3 Versions, and which are kept
Every version keeps its sidecars forever, so any release can still be browsed. Its parquet copy is kept for the promoted version, the one before it, and the consolidated milestones — versions a paper, a docs example or an app pins; the rest is thinned, and a thinned version’s retired marker names the consolidated one to read instead (metadata/release_policy.yml in CalCOFI/workflows decides; scripts/thin_releases.R applies it). The content-addressed store means a kept object is stored once however many versions share it. Table 7.2 is every version, newest first, with what is kept of each.
versions.json.
| version | date | tables | rows | size (MB) | DOI | kept |
|---|---|---|---|---|---|---|
| v2026.09.06 | 2026-09-06 | 23 | 348,657,010 | 2,350 | 10.5281/zenodo.22514953 | current |
| v2026.09.04 | 2026-09-04 | 23 | 348,657,010 | 2,351 | 10.5281/zenodo.22310858 | current |
| v2026.08.25 | 2026-08-25 | 18 | 320,260,205 | 1,998 | consolidated | |
| v2026.08.14 | 2026-08-14 | 18 | 307,537,056 | 1,930 | consolidated | |
| v2026.08.11 | 2026-08-11 | 18 | 323,912,311 | 2,016 | retired → v2026.08.14 | |
| v2026.08.10 | 2026-08-11 | 18 | 323,912,364 | 2,017 | retired → v2026.08.14 | |
| v2026.08.08 | 2026-08-08 | 18 | 309,122,838 | 1,947 | retired → v2026.08.14 | |
| v2026.08.07 | 2026-08-07 | 18 | 323,733,662 | 2,024 | retired → v2026.08.14 | |
| v2026.08.06 | 2026-08-06 | 18 | 255,137,845 | 1,636 | retired → v2026.08.14 | |
| v2026.08.05 | 2026-08-05 | 18 | 255,137,845 | 1,636 | retired → v2026.08.14 | |
| v2026.08.04 | 2026-08-04 | 18 | 255,155,031 | 1,635 | retired → v2026.08.14 | |
| v2026.08.03 | 2026-08-03 | 18 | 255,037,035 | 2,057 | retired → v2026.08.14 | |
| v2026.08.02 | 2026-08-02 | 18 | 255,037,035 | 2,087 | retired → v2026.08.14 | |
| v2026.07.30 | 2026-07-30 | 17 | 233,730,737 | 1,710 | retired → v2026.08.14 | |
| v2026.07.17 | 2026-07-17 | 17 | 236,577,241 | 1,943 | consolidated | |
| v2026.07.16 | 2026-07-16 | 17 | 236,577,241 | 1,945 | retired → v2026.07.17 | |
| v2026.07.15 | 2026-07-15 | 22 | 236,371,206 | 5,472 | retired → v2026.07.17 | |
| v2026.06.26 | 2026-06-26 | 50 | 133,966,996 | 3,554 | consolidated | |
| v2026.06.08 | 2026-06-08 | 44 | 133,807,311 | 3,553 | retired → v2026.06.26 | |
| v2026.06.07 | 2026-06-07 | 28 | 133,015,544 | 3,547 | retired → v2026.06.26 | |
| v2026.05.20 | 2026-05-20 | 28 | 133,022,102 | 5,503 | retired → v2026.06.26 | |
| v2026.05.19 | 2026-05-19 | 28 | 133,022,102 | 5,503 | retired → v2026.06.26 | |
| v2026.05.15 | 2026-05-15 | 28 | 133,013,839 | 0 | retired → v2026.06.26 | |
| v2026.05.14 | 2026-05-14 | 28 | 133,013,839 | 0 | consolidated | |
| v2026.04.08 | 2026-04-08 | 28 | 357,418,040 | 0 | consolidated | |
| v2026.04.06 | 2026-04-06 | 29 | 361,200,542 | 0 | retired → v2026.04.08 | |
| v2026.04.03 | 2026-04-03 | 29 | 361,183,451 | 0 | retired → v2026.04.08 | |
| v2026.04.02 | 2026-04-02 | 43 | 361,152,303 | 11,498 | retired → v2026.04.08 | |
| v2026.03.25 | 2026-03-25 | 18 | 354,898,020 | 11,263 | retired → v2026.04.08 | |
| v2026.03.14 | 2026-03-14 | 20 | 357,001,188 | 11,330 | retired → v2026.04.08 |
Consolidated today: v2026.04.08, v2026.05.14, v2026.06.26, v2026.07.17, v2026.08.14, v2026.08.25.
To pin a version rather than follow the promoted one:
calcofi4r::cc_get_db(version = "v2026.08.25") # Rcalcofi4py.cc_get_db(version="v2026.08.25") # Python# any DuckDB, from any language: resolve a table's objects through the catalog and paste the
# read_parquet() fragment it hands back — never a typed releases/{v}/parquet path
calcofi4r::cc_read_parquet_sql(calcofi4r::cc_release_sources(calcofi4r::cc_catalog("v2026.08.25"), "sample"))7.4 Two DOIs
Each release is tagged on GitHub, and Zenodo mints a version DOI for it; one concept DOI always resolves to the newest. Cite the version DOI in a paper — it names exactly the bytes you used — and the concept DOI where a living pointer is wanted. Both are in catalog.json, in the release notes’ How to cite, and on Cite this data:
- this release: https://doi.org/10.5281/zenodo.22514953
- every release: https://doi.org/10.5281/zenodo.22281994
7.5 The changelog
RELEASES.md is the database’s NEWS file: one section per release, newest first, saying what changed and why, with a Consumers: line wherever a change could break a reader, and an # Unreleased section collecting changes until the next cut. Each version’s RELEASE_NOTES.md is its section plus a generated appendix (tables and rows, datasets, the consumer-contract result, package versions). A release with no section for itself is refused before the freeze — the notes are written by the change that made them necessary, never reconstructed afterwards.
The most recent sections of RELEASES.md:
- Unreleased
- v2026.09.06 (2026-09-06)
- v2026.09.04 (2026-09-04)
- v2026.08.25 (2026-08-25)
- v2026.08.14 (2026-08-14)
- v2026.08.11 (2026-08-11)
7.6 What a release checks before it ships
release_database.qmd assembles every ingest’s parquet shards in memory, builds the release-only tables, and runs a series of gates before the freeze. Each is a function in calcofi4db with a test of its own, and the notebook’s rendered page shows every result. The ones that stop a release outright:
| gate | what it measures | on failure |
|---|---|---|
check_release_relationships() |
every declared primary key unique and non-NULL; every non-NULL foreign key resolving; written to integrity.json |
stops the release |
check_core_pk_unique() |
the core family’s primary keys, before the release-built tables exist (the gate added after v2026.08.25 shipped 4,855 duplicate sample_keys) |
stops the release |
check_cruise_key_integrity() |
cruise_key format, its month against the NODC code, the sample/obs → cruise edge, cruise_uuid hygiene, every event’s date within its cruise’s span (± 31 days, named exceptions only) |
stops the release; three ratchets (span overlaps, derived cruises, NULL keys per dataset) may only ever go down |
validate_for_release() |
NULLs in any *_id / *_key / *_uuid column; latitude, longitude and counts in range; no empty table |
NULLs stop the release; ranges warn |
check_obs_pair_parity() |
obs_bio + obs_env reproduce obs — row counts, distinct ids, a hash of every column, per realm and dataset |
stops the release |
test_release.qmd consumer contract |
every query shape the apps and packages depend on, run against the frozen release three ways (objects, view, pair), plus the presence of an all-ok integrity.json |
latest.txt is not promoted |
And the ones that report and ratchet — a number that may only ever go down between releases, so a regression fails but a known backlog does not:
| gate | what it measures |
|---|---|
check_depth_bounds() / check_depth_vs_seafloor() |
no depth is NaN, negative or beyond 6,500 m (fails); each root sample’s deepest depth against the GEBCO seafloor near its position (ratchet — the place is imprecise, not the measurement) |
check_measurement_bounds() |
every measurement type has declared physical bounds and no value lies outside them (out of range fails; undeclared types ratchet) |
check_taxon_ids() |
every taxon key is an accepted WoRMS or ITIS id, or on an explicit allowlist of dataset-local classes (fails) |
check_dataset_citation() |
every dataset’s citation has a year and a locator, its licence is a registered id, its DOI resolves (fails unless a provider question is open on the field) |
check_dataset_catalog() / check_eml_catalog() |
the record and each EML document validate against their schemas (fails) |
observed_coverage() |
each dataset’s temporal and spatial extent, measured from the data — never typed (report) |
A failure in Table 7.3 stops the release; a number in Table 7.4 that went up since the last release stops it too, and one that went down becomes the new ceiling.
For v2026.09.06 the consumer-contract suite ran 73 queries: 69 passed, 0 failed, 4 skipped (test_results.json).
A release that passes is uploaded, tested once more from the bucket by test_release.qmd, and only then promoted; the read-only consumers are refreshed afterwards by scripts/deploy_consumers.sh — the Explorer, ERDDAP and the Shiny apps on the CalCOFI server, and the ones hosted on GitHub Actions, which the script dispatches: db-viz-station, ctd-transects and this book, whose pre-render snapshots the promoted release’s catalog.json, integrity.json and sidecars and so describes the previous version until it re-renders (Architecture). A staging run exercises the whole path under a separate prefix without touching the promoted release or the shared object store.