Skip to content

Chunk spools by data size and by unit-bearing lengths - #833

Merged
d-chambers merged 4 commits into
devfrom
chunk-by-size
Aug 10, 2026
Merged

Chunk spools by data size and by unit-bearing lengths#833
d-chambers merged 4 commits into
devfrom
chunk-by-size

Conversation

@d-chambers

@d-chambers d-chambers commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Description

Adds a data-size chunk length so a memory budget can be stated directly:

spool.chunk(time=25 * dc.units.megabytes)

Each output patch's data array is then at most 25 MB. docs/recipes/low_freq_proc.qmd hand-rolled exactly this (pa.data.nbytes / pa.seconds → a length in seconds, which needs a loaded patch and its dtype); that recipe now uses the new API.

The same change closes a nearby gap: Spool.chunk previously rejected every pint quantity, so chunk(time=10 * dc.units.s) raised NotImplementedError from to_timedelta64's singledispatch. Unit-bearing lengths now work too, and overlap accepts both forms.

How a size resolves

Per partition, since the conversion needs that partition's sampling interval, its extent along the other dimensions, and its element dtype:

bytes_per_sample = itemsize * (product of the other dimensions' sample counts)
n_samples        = floor(requested_bytes / bytes_per_sample)
length           = n_samples * min(|step|)

Three decisions worth reviewing:

  • Floored, so the data never exceeds the request. When a single sample slab is already larger than the target, outputs hold one sample each and a warning says so — the request cannot be honored below one sample.
  • Sized against the partition's smallest step, not get_middle_value's median. Steps within config.sampling_group_tolerance (5%) share a partition, so sizing against the median lets a faster-sampled member fit more samples into the length and overshoot the byte bound. This makes the bound conservative instead of occasionally violated — and deterministic rather than flaky.
  • Mixed dtypes use np.result_type, matching the upcast patch_assembly performs. dtype is deliberately not a partition key (making it one would change which patches merge), so a partition may legitimately mix element types. result_type is also strictly more accurate than max-itemsize: int32 + float32 → float64, itemsize 8 rather than 4.

Index schema

Chunking must stay a pure metadata operation, so the element dtype comes from the index rather than from loading a patch. PatchSummary.dtype already existed and scan_to_df exposed it, but patch_record() dropped it on the way in. It is now stored (INDEX_VERSION 5 → 6) and reaches the planner as a private _dtype column.

The leading underscore is load-bearing, not cosmetic: _police_columns skips _-prefixed columns and nothing else protects it, so a public dtype column would raise CoordMergeError on every merge of patches with differing element types. There is a comment at the rename site saying so. dtype is also added to RESERVED_ATTR_COLUMNS so a patch attr of that name warns-and-drops instead of colliding.

The schema is unreleased (not even a beta), so the version bump costs only a local index rebuild.

Units

pint models byte = 8 * bit with bit dimensionless, so is_compatible_with("byte") is True for 50%, 1 strain, and any bare dimensionless quantity. is_data_size therefore tests the base unit directly. get_byte_count converts with .to("byte").magnitude and never through to_float, whose fallback is float(obj) — pint converts a dimensionless quantity to base units, so to_float(25 * MB) returns 2e8, the count in bits. A test pins that 8× difference so the two paths cannot be collapsed. (to_float's own quantity handling is tightened separately in the follow-up PR.)

Verification

Beyond the suite, checked against real data:

  • a float32 Terra15 file-backed directory spool: dtype recorded correctly, max nbytes 1999800 ≤ 2 MB
  • a partition mixing float32 and float64: sized against the upcast, bound held exactly at 100000 bytes
  • three patches whose steps differ by ~4% (one partition under the 5% tolerance): bound held, which is the case the min-step decision exists for

Known limitation

chunk(dim=...).chunk(same_dim=...) mis-assembles one patch, which breaks this bound in that specific chain. That is a pre-existing bug, filed as #832 — it reproduces on unmodified dev with plain numeric lengths and has nothing to do with sizes or units. TestChainedChunk::test_size_then_size therefore asserts on the plan (which is correct and is what this PR owns) and references #832; tighten it to assert nbytes once that is fixed.

Changelog

  • added: Spool.chunk accepts a quantity as the chunk length — a coordinate-unit length (chunk(time=10 * dc.units.s)) or a data size (chunk(time=25 * dc.units.megabytes)) bounding each output patch's array. overlap accepts both, and chunk_plan(...).params["size"] reports what a size resolved to.
  • changed: the index records each patch's element dtype, so an attr named dtype is reserved and stays unindexed. Indexes built by an earlier unreleased version must be rebuilt, which the new index format already requires.

Checklist

I have (if applicable):

Summary by CodeRabbit

  • New Features

    • Chunking and overlap support coordinate-based and data-size quantities with dtype-aware sizing and diagnostics.
    • Mixed data types are handled safely when combining patches.
    • Indexes preserve each patch’s element data type.
  • Bug Fixes

    • Improved unit validation, size calculations, rounding, overlap handling, and descending-coordinate support.
  • Documentation

    • Added guidance and examples for quantity-based chunking, data-size limits, and processing workflows.
    • Existing indexes may require rebuilding after the index schema update.

@d-chambers d-chambers added the ready_for_review PR is ready for review label Aug 7, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9587317b-63a5-4daa-bd71-b7e3d2650f82

📥 Commits

Reviewing files that changed from the base of the PR and between 9c88a80 and 9430cae.

📒 Files selected for processing (1)
  • docs/changelog.qmd
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/changelog.qmd

📝 Walkthrough

Walkthrough

The PR adds coordinate-unit and data-size support to chunking and overlap resolution. It records resolved size diagnostics, handles mixed dtypes through upcasting, and persists patch dtypes in schema version 6 indexes and private spool columns.

Changes

Chunking and dtype propagation

Layer / File(s) Summary
Quantity-aware chunk planning
dascore/units.py, dascore/utils/chunk_plan.py, dascore/core/spool.py, tests/test_units.py, tests/test_utils/test_chunk.py, tests/test_core/test_patch_chunk.py
Chunk lengths and overlap accept coordinate-unit and data-size quantities. Planning resolves values per partition using sampling metadata and combined dtypes. It records diagnostics, validates invalid inputs, and warns when one sample exceeds the requested size.
Dtype propagation and index representation
dascore/io/index/schema.py, dascore/io/index/ingest.py, dascore/io/index/planned.py, dascore/io/index/backend.py, dascore/io/index/catalog.py, dascore/io/index/indexer.py, dascore/core/spool.py, tests/test_io/test_index/test_index_contract.py
Index schema version 6 stores each patch dtype. Ingestion, planned outputs, concatenation, catalogs, and indexer results preserve dtype metadata while exposing it privately as _dtype.
Documentation and recipe integration
docs/notes/spool_chunking.qmd, docs/tutorial/spool.qmd, docs/recipes/low_freq_proc.qmd, docs/changelog.qmd
Documentation describes quantity-based chunking, size calculations, rounding, overlap, dtype handling, and the index schema update. The low-frequency recipe uses byte-based chunk sizing.

Possibly related PRs

Suggested labels: spool, patch, IO, documentation, ready_for_review

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main changes: data-size chunking and unit-bearing lengths.
Description check ✅ Passed The description explains the feature, implementation decisions, schema changes, tests, documentation, verification, checklist, and known limitation.
Docstring Coverage ✅ Passed Docstring coverage is 97.03% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chunk-by-size

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added documentation Improvements or additions to documentation IO Work for reading/writing different formats patch related to Patch class spool related to Spool class labels Aug 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
tests/test_io/test_index/test_index_contract.py (1)

213-217: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Exercise the dtype reservation through ingestion.

This test checks only the constant. The test can pass while ingestion still accepts an attr named dtype and hides or shadows the structural value. Add an end-to-end index test that verifies the intended rejection or omission behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_io/test_index/test_index_contract.py` around lines 213 - 217,
Extend test_dtype_attr_is_reserved to construct index input containing an
attribute named dtype and ingest it through the public index creation path.
Assert the documented rejection or omission behavior, including that the
structural dtype value is not shadowed, rather than checking
RESERVED_ATTR_COLUMNS directly; reuse existing ingestion fixtures and helpers in
the surrounding index contract tests.
tests/test_core/test_patch_chunk.py (1)

1055-1060: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

The clamped-size test builds a very large number of outputs.

random_spool holds three patches of roughly 300 x 2000 float64 samples. One time sample costs about 2400 bytes, so a 1 kB request clamps to one sample per output and the plan produces about 6000 output rows. The derived catalog then ingests one record per row before out[0] resolves. This makes a single assertion pay for thousands of index writes.

Use a small purpose-built patch for this case. _make already exists in this class.

♻️ Proposed change to shrink the clamped-size case
-    def test_slab_larger_than_target_warns(self, random_spool):
+    def test_slab_larger_than_target_warns(self):
         """One sample is the floor; the request cannot be honored below one sample."""
+        start = np.datetime64("2020-01-01T00:00:00")
+        # 50 distance samples of float64 = 400 bytes per time sample
+        spool = dc.spool([self._make("float64", start, distance=50, samples=20)])
         with pytest.warns(UserWarning, match="larger than the requested size"):
-            out = random_spool.chunk(time=1 * dc.units.kB)
+            out = spool.chunk(time=100 * dc.units.byte)
         patch = out[0]
         assert patch.shape[patch.get_axis("time")] == 1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_core/test_patch_chunk.py` around lines 1055 - 1060, Update
test_slab_larger_than_target_warns to use a small purpose-built patch created
via the existing _make helper instead of random_spool, while preserving the 1 kB
chunk request, warning assertion, and one-time-sample shape assertion.
dascore/utils/chunk_plan.py (1)

818-831: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Diagnostics record partitions that produce no outputs.

The loop appends a diagnostics entry at Line 784 before get_intervals runs. A partition that is too short raises ChunkError at Line 800 and is skipped, but its entry stays in size_diagnostics. The recorded first_output_id then points at an output_id that no row uses, and a clamped entry for a skipped partition still triggers the warning at Line 831.

Append the diagnostics after the interval computation succeeds.

♻️ Proposed change to defer the diagnostics append
         if per_partition and not merge_mode:
             ...
             value_c, overlap_c, diag = _resolve_partition_length(
                 value, overlap, sub, name, size_step, df[min_name].dtype
             )
-            if diag is not None:
-                size_diagnostics.append({"first_output_id": next_id, **diag})
+        else:
+            diag = None
         if merge_mode:
             start_stop = np.atleast_2d(np.asarray([g_start, g_stop]))
         else:
             try:
                 start_stop = get_intervals(...)
             except ChunkError:  # partition too short; skip (D8)
                 continue
+        if diag is not None:
+            size_diagnostics.append({"first_output_id": next_id, **diag})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dascore/utils/chunk_plan.py` around lines 818 - 831, Move the
size-diagnostics append in the chunk-planning loop to after get_intervals
completes successfully, while preserving the existing diagnostic contents.
Ensure partitions that raise ChunkError and produce no outputs are excluded from
size_diagnostics, so their first_output_id and clamped values do not affect the
recorded partitions or warning in the size_diagnostics handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_units.py`:
- Around line 553-556: Update test_undefined_byte_alias_raises so the attempted
dc.units.KB lookup is assigned to a local variable within the pytest.raises
block, preserving the UndefinedUnitError assertion while avoiding Ruff B018.

---

Nitpick comments:
In `@dascore/utils/chunk_plan.py`:
- Around line 818-831: Move the size-diagnostics append in the chunk-planning
loop to after get_intervals completes successfully, while preserving the
existing diagnostic contents. Ensure partitions that raise ChunkError and
produce no outputs are excluded from size_diagnostics, so their first_output_id
and clamped values do not affect the recorded partitions or warning in the
size_diagnostics handling.

In `@tests/test_core/test_patch_chunk.py`:
- Around line 1055-1060: Update test_slab_larger_than_target_warns to use a
small purpose-built patch created via the existing _make helper instead of
random_spool, while preserving the 1 kB chunk request, warning assertion, and
one-time-sample shape assertion.

In `@tests/test_io/test_index/test_index_contract.py`:
- Around line 213-217: Extend test_dtype_attr_is_reserved to construct index
input containing an attribute named dtype and ingest it through the public index
creation path. Assert the documented rejection or omission behavior, including
that the structural dtype value is not shadowed, rather than checking
RESERVED_ATTR_COLUMNS directly; reuse existing ingestion fixtures and helpers in
the surrounding index contract tests.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e9a38c5e-0fe0-4367-b90d-4b577d609ca8

📥 Commits

Reviewing files that changed from the base of the PR and between f48876b and 5ca8a37.

📒 Files selected for processing (17)
  • dascore/core/spool.py
  • dascore/io/index/backend.py
  • dascore/io/index/catalog.py
  • dascore/io/index/indexer.py
  • dascore/io/index/ingest.py
  • dascore/io/index/planned.py
  • dascore/io/index/schema.py
  • dascore/units.py
  • dascore/utils/chunk_plan.py
  • docs/changelog.qmd
  • docs/notes/spool_chunking.qmd
  • docs/recipes/low_freq_proc.qmd
  • docs/tutorial/spool.qmd
  • tests/test_core/test_patch_chunk.py
  • tests/test_io/test_index/test_index_contract.py
  • tests/test_units.py
  • tests/test_utils/test_chunk.py

Comment thread tests/test_units.py
Comment on lines +553 to +556
def test_undefined_byte_alias_raises(self):
"""`KB` is not a pint unit; the kilobyte spelling is `kB`."""
with pytest.raises(pint.UndefinedUnitError):
dc.units.KB

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Ruff flags the bare attribute access as a useless expression (B018).

Line 556 evaluates dc.units.KB for its side effect. Ruff cannot infer that the attribute access is the subject under test, so B018 fails the lint job. Bind the result or suppress the rule explicitly.

🧹 Proposed fix for B018
     def test_undefined_byte_alias_raises(self):
         """`KB` is not a pint unit; the kilobyte spelling is `kB`."""
         with pytest.raises(pint.UndefinedUnitError):
-            dc.units.KB
+            _ = dc.units.KB
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_undefined_byte_alias_raises(self):
"""`KB` is not a pint unit; the kilobyte spelling is `kB`."""
with pytest.raises(pint.UndefinedUnitError):
dc.units.KB
def test_undefined_byte_alias_raises(self):
"""`KB` is not a pint unit; the kilobyte spelling is `kB`."""
with pytest.raises(pint.UndefinedUnitError):
_ = dc.units.KB
🧰 Tools
🪛 Ruff (0.16.1)

[warning] 556-556: Found useless expression. Either assign it to a variable or remove it.

(B018)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_units.py` around lines 553 - 556, Update
test_undefined_byte_alias_raises so the attempted dc.units.KB lookup is assigned
to a local variable within the pytest.raises block, preserving the
UndefinedUnitError assertion while avoiding Ruff B018.

Source: Linters/SAST tools

@d-chambers

Copy link
Copy Markdown
Contributor Author

Update after adversarial review

Three internal reviews (correctness / index plumbing / test quality) found four real defects in this PR, now fixed in e59e6d36 with a regression test each. Full suite 8392 passed.

The size bound could be exceeded by up to 1.84×. An output holds the sum of its members' sample counts, which equals span/step + 1 only when the partition sits on one grid. Near-contiguous files whose boundaries miss the grid each contribute their own trailing sample, so they pack more samples into a span than the grid allows — silently, with no warning. Reproduced at 1.004×, 1.048× and 1.840× (30 files × 20 samples with a 10 ms gap; 60 files × 2 samples with a 50 ms gap), through a real directory index as well as in memory. Fixed by dividing the length by a measured packing factor — the partition's maximum local sample density in units of its step, exactly 1.0 when members tile the grid and reported in params["size"]. Those three cases now land at 96–99.6% of the request, and the gridded case is byte-identical to before.

Correction to this PR's own description: I claimed the bound depends on sizing against the partition's smallest step, and that the median would overshoot. That was true of the pre-packing code but is now wrong — packing is measured in units of whichever step is used, so the two cancel exactly and the length works out to n_samples / max_density either way. Mutation testing is what surfaced this: swapping min for median kills no test, because the code is genuinely insensitive to it. The comment and design note now describe packing as the mechanism; min-step remains only because it makes the flooring granularity finest.

Element dtype is now resolved per output, not per partition. Carrying the partition-wide np.result_type onto every output made a chunked spool compare unequal to its own materialized twin whenever the spool mixed dtypes — breaking the promise in _strip_identity's docstring, and for plain chunk(time=4), not just sizes. An output drawing only from float32 members really is float32; an output spanning a dtype boundary gets the upcast, which is what assembly produces. This also stops a chained size chunk under-filling.

Non-finite and array-valued quantities are now rejected. chunk(time=nan*MB) silently merged the entire spool into one patch (a NaN magnitude is null, so it read as merge mode) when the user asked for a size cap; inf raised a bare ValueError from the sample-count division. Both now raise ParameterError.

NaN could reach the derived catalog as the string "nan". np.nan is truthy, so str(row.get("_dtype") or "") let it through, and np.dtype("nan") then discarded every good dtype in the partition.

Test gaps closed

Mutation testing found np.result_type unguarded: both existing tests mixed float32 + float64, where it agrees with max-itemsize. The distinguishing case is int32 + float32 — both 4 bytes, promoting to 8-byte float64 — where the max-itemsize rule silently produces 2× the requested budget. Now tested, and each new test is confirmed to fail under its mutation. Also replaced test_merge_mode_unaffected, which passed with the entire feature absent, and test_dtype_attr_is_reserved, which restated a literal from the module under test rather than testing the behavior.

Cross-PR note

This PR's test_byte_count_is_not_to_float asserted to_float(25 MB) == 8*25e6, which #834 makes raise — whichever merged second would have broken. Rewritten to assert the byte count directly; verified against simulated merged semantics.

Spool.chunk now accepts a pint quantity as the chunk length. A quantity
in the coordinate's own units works (time=10*s, distance=100*ft), which
previously raised NotImplementedError, and a quantity of information
(time=25*megabytes) chunks so each output patch's data array is at most
the requested size. Overlap accepts both forms.

A size resolves per partition, since the conversion needs that
partition's sampling interval, its extent along the other dimensions,
and its element dtype:

    bytes_per_sample = itemsize * prod(other dims' sample counts)
    n_samples        = floor(requested_bytes / bytes_per_sample)

The count is floored so the data never exceeds the request, and is
computed against the partition's smallest step: steps within
sampling_group_tolerance share a partition, so sizing against the median
would let a faster-sampled member overshoot.

To make this a pure metadata operation, the index now records each
patch's element dtype (INDEX_VERSION 5 -> 6) and surfaces it to the
spool relation as a private _dtype column. The leading underscore is
load-bearing: chunk's merge-compatibility grouping compares all
non-private columns, so a public dtype column would raise
CoordMergeError on every merge of patches with differing element types.
dtype is deliberately not a partition key, so a partition may mix
dtypes; the estimate uses np.result_type, matching the upcast assembly
performs.

Spool.chunk_plan(...).params["size"] reports what a size resolved to.
The low_freq_proc recipe now uses the new API instead of hand-computing
bytes per second from a loaded patch.
Three defects in the size feature, each with a regression test that
fails without its fix.

The bound could be exceeded by up to 1.84x. An output holds the sum of
its members' sample counts, which equals span/step + 1 only when the
partition sits on one grid; members separated by less than one sample
each contribute their own trailing sample, so near-contiguous files
whose boundaries miss the grid pack more samples into a span than the
grid allows. Divide the length by the partition's measured packing
factor, which is exactly 1.0 when members tile the grid. This also
supersedes the min-step rationale: packing is measured in units of the
step used, so the two cancel and the length works out to
n_samples / max_density either way. The comment and design note now say
so instead of claiming the median step would overshoot.

Element dtype is now resolved per output rather than per partition. An
output drawing only from its float32 members really is float32, so
claiming the partition-wide upcast both over-sized a chained chunk and
made a chunked spool compare unequal to its own materialized twin --
for plain chunking too, not just sizes.

Non-finite and array-valued quantities are rejected up front. A NaN
magnitude is null, so `chunk(time=nan*MB)` silently merged the whole
spool when the user asked for a size cap, and `inf` raised a bare
ValueError from the sample-count division.

Also: never write NaN into the derived catalog's dtype column (NaN is
truthy, so `or ""` let the string "nan" through and poisoned every
later np.dtype of that column); cover np.result_type with an int32 +
float32 partition, where max-itemsize silently doubles the budget while
float32 + float64 cannot tell the two rules apart; and replace a
vacuous merge-mode test that passed with the feature absent and a
tautological one that restated a literal from the module under test.
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (99b425f) to head (9430cae).

Additional details and impacted files
@@            Coverage Diff             @@
##               dev      #833    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files          164       164            
  Lines        17936     18096   +160     
==========================================
+ Hits         17936     18096   +160     
Flag Coverage Δ
network 48.14% <11.44%> (-0.35%) ⬇️
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

test_dtype_round_trips keyed a dict on source_path, which the SQLite
round-trip returns with backslashes on Windows while the summary literal
uses forward slashes. It failed every Windows job on both the full and
min-deps matrices; normalize the separator before comparing. Nothing
about the feature is platform-dependent.

Codecov also flagged uncovered lines in the new code, all of them real
branches rather than dead ones: a frame with no `dims` column falling
back to its complete envelopes, an unknown step on the chunked
dimension, an unparseable dtype string, a missing envelope on another
dimension, and a numeric coordinate rejecting a dimensionally wrong
quantity. Each now has a test.

Two defensive branches really were unreachable and are gone rather than
pragma'd: get_interval_columns cannot fail in _packing_factor because
the caller already read those columns for the same partition, and the
density array cannot be empty once the step is validated (now a plain
assert, which documents the invariant and executes every call).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/test_core/test_patch_chunk.py (1)

1065-1072: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Correct the docstring: packing density enforces the bound, not the smallest step.

The docstring states that sizing "must" use the smallest step or the byte target is overshot. The implementation states the opposite in dascore/utils/chunk_plan.py lines 857-861: the packing factor is measured in units of the chosen step, so the choice cancels, and the smallest step only makes the flooring granularity finer. The PR description records the same correction. Align the test docstring with the implementation so a later reader does not treat the step choice as the safety guard.

📝 Proposed docstring fix
     def test_mixed_steps_in_one_partition_stay_bounded(self):
         """
-        Sizing must use the partition's smallest step, not its median.
-
-        Steps within `sampling_group_tolerance` share a partition, so a
-        member sampled faster than the median fits more samples into the
-        same length and would overshoot the byte target.
+        A partition mixing sampling steps stays inside the byte target.
+
+        Steps within `sampling_group_tolerance` share a partition, so a
+        member sampled faster than the others packs more samples into the
+        same length. The measured packing factor absorbs that density, so
+        the resolved length still fits the request.
         """
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_core/test_patch_chunk.py` around lines 1065 - 1072, Update the
docstring of test_mixed_steps_in_one_partition_stay_bounded to state that
packing density enforces the byte bound, while the smallest step only provides
finer flooring granularity; remove the claim that sizing must use the
partition’s smallest step to prevent overshooting.
tests/test_io/test_index/test_index_contract.py (1)

197-207: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Test multiple patches from the same source.

Line 205 uses summaries with one source_path per patch. summaries_to_records groups summaries by source. A regression that stores dtype on the source instead of on each patch can pass this test.

Add two summaries with the same source_path and different dtypes. Assert their dtype values separately by source_patch_id.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_io/test_index/test_index_contract.py` around lines 197 - 207,
Update test_dtype_round_trips to include two summaries sharing one source_path
but having different dtypes, then assert each returned dtype by its
source_patch_id rather than only by path. Preserve path normalization for other
entries and ensure the assertions distinguish both patches.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_io/test_index/test_index_contract.py`:
- Around line 209-216: Update test_dtype_is_private_in_flat_relation to
construct or obtain an explicitly index-backed spool, then retrieve its contents
through the backend/indexer get_contents path. Preserve the assertions that the
public dtype column is absent and _dtype contains the patch data dtype, ensuring
the test exercises the index backend’s rename behavior.

---

Nitpick comments:
In `@tests/test_core/test_patch_chunk.py`:
- Around line 1065-1072: Update the docstring of
test_mixed_steps_in_one_partition_stay_bounded to state that packing density
enforces the byte bound, while the smallest step only provides finer flooring
granularity; remove the claim that sizing must use the partition’s smallest step
to prevent overshooting.

In `@tests/test_io/test_index/test_index_contract.py`:
- Around line 197-207: Update test_dtype_round_trips to include two summaries
sharing one source_path but having different dtypes, then assert each returned
dtype by its source_patch_id rather than only by path. Preserve path
normalization for other entries and ensure the assertions distinguish both
patches.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 01e63a0b-fa7a-45fa-8002-8421f31b5aed

📥 Commits

Reviewing files that changed from the base of the PR and between 5ca8a37 and 9c88a80.

📒 Files selected for processing (15)
  • dascore/core/spool.py
  • dascore/io/index/backend.py
  • dascore/io/index/catalog.py
  • dascore/io/index/indexer.py
  • dascore/io/index/ingest.py
  • dascore/io/index/planned.py
  • dascore/units.py
  • dascore/utils/chunk_plan.py
  • docs/changelog.qmd
  • docs/notes/spool_chunking.qmd
  • docs/tutorial/spool.qmd
  • tests/test_core/test_patch_chunk.py
  • tests/test_io/test_index/test_index_contract.py
  • tests/test_units.py
  • tests/test_utils/test_chunk.py
🚧 Files skipped from review as they are similar to previous changes (10)
  • dascore/core/spool.py
  • dascore/io/index/catalog.py
  • docs/tutorial/spool.qmd
  • dascore/io/index/ingest.py
  • dascore/io/index/indexer.py
  • docs/notes/spool_chunking.qmd
  • dascore/io/index/backend.py
  • dascore/io/index/planned.py
  • dascore/units.py
  • docs/changelog.qmd

Comment on lines +209 to +216
def test_dtype_is_private_in_flat_relation(self):
"""The spool sees `_dtype`, never a public `dtype` column."""
import dascore as dc

spool = dc.get_example_spool("random_das")
df = spool.get_contents()
assert "dtype" not in df.columns
assert set(df["_dtype"]) == {str(spool[0].data.dtype)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline dascore/examples.py --match get_example_spool --view expanded
rg -n -C 5 'random_das|EXAMPLE_SPOOLS' dascore/examples.py
ast-grep outline dascore/io/index/indexer.py --match get_contents --view expanded
rg -n -C 5 'get_contents\(' tests/test_io/test_index/test_index_contract.py

Repository: DASDAE/dascore

Length of output: 4155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== examples.py random_spool implementation =="
sed -n '555,580p' dascore/examples.py

echo
echo "== dascore spool/base contract around spool and get_contents =="
rg -n -C 4 'class .*Spool|def get_contents|def _get|backend|Indexer' dascore test -g '*.py' | head -n 200

echo
echo "== get_contents call sites in index tests =="
cat -n tests/test_io/test_index/test_index_contract.py | sed -n '190,235p'

echo
echo "== index-related files =="
git ls-files | rg 'dascore/io/index|test_index_contract'

Repository: DASDAE/dascore

Length of output: 12641


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate spool files =="
git ls-files | rg 'spool|get_contents|indexer|index_contract'

echo
echo "== exact occurrences of get_contents and backend in relevant Python files =="
rg -n -C 6 'def get_contents|backend=|backend|class .*Spool|Indexer' dascore tests/test_io/test_index/test_index_contract.py tests -g '*.py' | sed -n '1,260p'

echo
echo "== dascore/examples.py dc attribute definitions =="
rg -n -C 4 'class Dascore|class .*BaseSpool|spool =|BaseSpool' dascore examples tests -g '*.py' | sed -n '1,220p'

echo
echo "== index contract around dtype tests =="
cat -n tests/test_io/test_index/test_index_contract.py | sed -n '1,260p'

Repository: DASDAE/dascore

Length of output: 1914


Use the index-backed spool for this _dtype contract.

dc.get_example_spool("random_das") returns dc.spool([random patches]); this test does not exercise the index backend’s _dtype rename path. Use an explicitly index-backed spool with backend/Indexer.get_contents or a corresponding example that is known to be index-backed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_io/test_index/test_index_contract.py` around lines 209 - 216,
Update test_dtype_is_private_in_flat_relation to construct or obtain an
explicitly index-backed spool, then retrieve its contents through the
backend/indexer get_contents path. Preserve the assertions that the public dtype
column is absent and _dtype contains the patch data dtype, ensuring the test
exercises the index backend’s rename behavior.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

✅ Documentation built:
👉 Download
Note: You must be logged in to github and a DASDAE member to access the link.

@d-chambers
d-chambers merged commit 98d1433 into dev Aug 10, 2026
27 checks passed
@d-chambers
d-chambers deleted the chunk-by-size branch August 10, 2026 09:26
d-chambers added a commit that referenced this pull request Aug 10, 2026
Fix ty diagnostics from the crossed #833/#845 merges
@d-chambers d-chambers removed the ready_for_review PR is ready for review label Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation IO Work for reading/writing different formats patch related to Patch class spool related to Spool class

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant