Skip to content

feat: serve IDC v25 (idc-index 0.13.0) - #56

Merged
fedorov merged 5 commits into
mainfrom
feat/idc-v25
Oct 8, 2026
Merged

fedorov merged 5 commits into
mainfrom
feat/idc-v25

Conversation

@fedorov

@fedorov fedorov commented Oct 8, 2026

Copy link
Copy Markdown
Member

Moves the served data release from v24 to v25 and fixes a schema-discovery gap that v25's new column exposed.

Serving IDC v25

idc-index 0.12.5 → 0.13.0, pulling idc-index-data 24.2.2 → 25.0.0. That release is the data bump plus CI chores — no upstream API change. /v3/version and the MCP get_idc_version now report v25: 179 collections, 26 analysis results, 1,044,191 series, 99.9 TB, 242 DOIs (up from 237).

Schema drift across all 17 tables is two columns: analysis_results_index gains provenance, and prior_versions_index drops gcs_bucket_1 (unreferenced here).

Struct columns were undiscoverable

v25's provenance is a struct, and get_table_schema described it as the bare type RECORD — which names no field a caller could select. The same was already true of collections_index.sources, a RECORD[] whose elements nest two further structs (license, provenance). Writing SELECT provenance.data_contributor meant guessing field names, which is precisely what this server tells callers not to do.

The upstream schema JSON ships no fields for a RECORD, so those names exist nowhere but the Parquet footer. get_table_schema now reads them from there and advertises the real type:

provenance: STRUCT(data_contributor VARCHAR, source_data_provider VARCHAR,
                   deidentification_party VARCHAR, dicom_conversion_by VARCHAR)

Three constraints shaped the fix:

  • Only RECORD columns are substituted. The rest keep their BigQuery-style names, which numeric_range_attributes() matches on.
  • A repeated record's DuckDB type already ends in [], so the mode is not applied on top of it (no [][]).
  • Specialized indices have no local Parquet until fetched, so they degrade to the un-expanded RECORD rather than failing discovery.

The runtime path needed no change — structs already serialized correctly, and dot access and UNNEST already worked.

provenance is deliberately left reachable via SQL only; it is not promoted into the typed AnalysisResult / CollectionDetail responses.

Testing

  • 103 pass with the full specialized index set; 92 pass (10 skipped) on a bundled-only build, covering the degradation path.
  • Five new tests in tests/test_struct_columns.py pin the expansion, the no-double-[] rule, selectability, serialization, and the missing-Parquet fallback.
  • Both SQL examples added to the docs were run verbatim.
  • ruff check / ruff format clean; bandit shows only pre-existing nosec notes.

Docs

Struct guidance sits beside the existing array guidance in docs/user-guide.md and is mirrored into the idc://guide MCP resource. INSTRUCTIONS is untouched — it stays lean. Changelog entry added under [Unreleased].

The version bump to 3.0.0b5 is not in this PR: the runbook requires it be its own commit, cut after this merges.

🤖 Generated with Claude Code

fedorov and others added 3 commits October 7, 2026 22:10
Pulls idc-index-data 25.0.0, moving the served data release from v24 to v25:
179 collections, 26 analysis results, 1,044,191 series, 99.9 TB.

idc-index 0.13.0 is the data bump plus CI chores; no API surface changed.

Schema drift across all 17 tables is limited to two columns:
analysis_results_index gains `provenance`, and prior_versions_index drops
`gcs_bucket_1` (unreferenced here).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
get_table_schema / GET /v3/tables/{table} rendered a struct column as the
bare type `RECORD`, which names no field a caller could select. v25's new
analysis_results_index.provenance is a struct, as is the pre-existing
collections_index.sources (whose elements nest two further structs), so
writing `SELECT provenance.data_contributor` meant guessing the field
names — the one thing this server tells callers not to do.

The upstream schema JSON carries no `fields` for a RECORD, so the names
exist nowhere but the Parquet footer; read them from there and advertise
the real DuckDB type, e.g.

  STRUCT(data_contributor VARCHAR, source_data_provider VARCHAR, ...)

Only RECORD columns are substituted: the rest keep their BigQuery-style
names, which numeric_range_attributes() matches on. A repeated record's
DuckDB type already ends in `[]`, so the mode is not applied on top of it.
Specialized indices have no local Parquet until fetched, so they degrade
to the un-expanded RECORD rather than failing discovery.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds struct-column guidance beside the existing array guidance in both the
user guide and the idc://guide MCP resource (dot access for a field,
unnest first for a list of structs) with runnable examples.

Also moves the illustrative `v24` / `24.0.0` strings in the OpenAPI
examples, model and tool docstrings to v25, updates the DOI count in the
citations comments (237 -> 242), refreshes the stale pin example in the
deploy runbook, and records both user-visible changes in the changelog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 8, 2026 02:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The production build ignores the lockfile, so the lower-bound dependency can silently serve a later IDC release.

3 open findings
What changed in this PR

Updates the service to IDC v25 and expands struct schemas using Parquet metadata.

Changes:

  • Bumps idc-index data dependencies to v25.
  • Exposes nested struct field types in schema discovery.
  • Adds struct tests and updates user-facing documentation.
File Description
pyproject.toml Updates the index dependency.
uv.lock Locks IDC v25 packages.
src/​idc_api/​core/​schema.py Expands RECORD column types.
src/​idc_api/​core/​models.py Updates version examples.
src/​idc_api/​core/​services/​citations.py Updates DOI-count documentation.
src/​idc_api/​rest/​app.py Updates REST examples to v25.
src/​idc_api/​mcp/​server.py Adds struct-query guidance.
tests/​test_struct_columns.py Tests struct discovery and serialization.
tests/​test_citations.py Updates citation documentation.
docs/​user-guide.md Documents struct queries and v25.
dev/​deployment.md Updates release-pin example.
CHANGELOG.md Records v25 and schema discovery changes.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread pyproject.toml Outdated
Comment thread src/idc_api/core/services/citations.py Outdated
Comment thread tests/test_citations.py Outdated
Addresses Copilot review on #56.

Pin idc-index==0.13.0 rather than >=0.13.0. The production image installs
from pyproject.toml alone — the Dockerfile copies pyproject.toml and src,
never uv.lock — so the lockfile constrains CI but not the image. Under a
lower bound, rebuilding this very commit after a later idc-index ships
would bake a different idc-index-data, and the service would advertise an
IDC release at /v3/version that neither this PR nor the changelog claims.
dev/deployment.md already prescribed the exact pin; the constraint now
matches, with a comment saying why this one dependency differs.

Also finish the DOI-count update: the v24 -> v25 edit caught the mentions
that named a version and left four bare "237"s reading inconsistently
beside "242 at v25". All are prose; no assertion depends on the number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@fedorov

fedorov commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

Opened #57 for the root cause behind the exact-pin discussion above.

The idc-index==0.13.0 pin in this PR fixes the one dependency where resolution drift is user-visible (it decides the IDC release advertised at /v3/version). The general problem is wider: Dockerfile:14-16 installs from pyproject.toml alone, so uv.lock constrains CI but not the image — meaning every other runtime dependency can still differ between the versions CI tested and audited and the versions that ship.

Out of scope here; tracked in #57.

Drafted with Claude Code.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The fallback test can pass without exercising fallback behavior, and schema adapter parity remains untested.

0 open findings

3 resolved since last review
Previously missed (2)

In code that hasn't changed since last review

Medium severity Add REST and MCP schema parity assertions

tests/​test_struct_columns.py:22

The new schema contract is asserted only through the core service. Repository guidance requires capability changes to retain core/REST/MCP parity; add an async parity assertion comparing this expanded schema with GET /v3/tables/analysis_results_index and the MCP get_table_schema result so adapter serialization regressions are covered.

Medium severity Test does not verify missing-Parquet fallback behavior

tests/​test_struct_columns.py:74

This test does not actually pin the missing-Parquet fallback: it verifies that seg_index lacks a path but then describes clinical_index, and the final assertion explicitly accepts STRUCT(...), which is the non-fallback result. Use the same table throughout, force that table's Parquet lookup to be absent (clearing the schema caches), and assert the exact bare RECORD/RECORD[] result so a regression cannot pass.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

…review)

Addresses the second Copilot review pass on #56.

The missing-Parquet test was close to vacuous: it asserted on seg_index
then described clinical_index, and its final `or startswith("STRUCT(")`
accepted the non-fallback result, so it could not fail. Replaced with
three that can: one table throughout asserting the exact bare RECORD[],
direct _column_type assertions for the fallback rendering, and the real
degradation path forced on a table that otherwise expands (drop its
parquet_filepath, clear the lru_caches, assert RECORD). Verified by
mutation — rendering an empty struct as "STRUCT()" fails all three, and
would have passed the old one.

Also adds the table-schema parity test the repo's own invariant asks for:
test_parity.py covered version, counts, citations and clinical, but not
the payload this PR changes. It pins core == REST == MCP for both struct
tables, and that collections_index.sources keeps its embedded quoted
identifier (`"Access" VARCHAR`) through JSON on both surfaces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@fedorov

fedorov commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

Both points from the second review pass are fixed in 3257d3a — they were fair.

The fallback test was close to vacuous. It asserted on seg_index, then described clinical_index, and its final or ... startswith("STRUCT(") accepted the non-fallback result, so nothing could make it fail. Replaced with three tests that can:

  • one table throughout, asserting the exact bare RECORD[]
  • direct _column_type assertions for the fallback rendering, including that an unrelated expansion is not borrowed for a column that has none
  • the real degradation path forced on a table that otherwise expands — drop analysis_results_index's parquet_filepath, clear the lru_caches, assert RECORD (with a fixture clearing the caches on both sides, so a fake value cannot outlive the monkeypatch)

Verified by mutation rather than by eye: rendering an empty struct as "STRUCT()" fails all three. The old test passed that mutation, which is precisely the criticism.

Parity was genuinely missing. test_parity.py covered version, counts, citations and clinical, but not table schemas — the payload this PR changes. Added test_table_schema_parity_including_struct_columns: core == REST == MCP for both struct tables, plus an assertion that collections_index.sources keeps its embedded quoted identifier ("Access" VARCHAR) through JSON on both surfaces. That last one is the concrete serialization risk an expanded struct introduces, and it was untested.

Suite is 106 passing with all indices, 95 + 10 skipped bundled-only.

Drafted with Claude Code.

@fedorov
fedorov merged commit 7844100 into main Oct 8, 2026
6 checks passed
@fedorov
fedorov deleted the feat/idc-v25 branch October 8, 2026 02:48
@fedorov fedorov mentioned this pull request Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants