Spool index backend - #751
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe PR replaces legacy directory indexing with SQLite-backed catalogs, adds lazy spool selection, union, and chunk planning, normalizes patch identities, and migrates HDF5 utilities and tests from PyTables to h5py. ChangesIndexing, catalogs, and directory spools
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #751 +/- ##
===========================================
Coverage 100.00% 100.00%
===========================================
Files 152 161 +9
Lines 14929 16619 +1690
===========================================
+ Hits 14929 16619 +1690
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (5)
dascore/io/index/query.py (1)
216-216: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd
strict=Truetozip()for safety.If
rows["value_kind"]androws["column_name"]differ in length,zip()silently truncates. Usingstrict=Truecatches this at runtime.♻️ Proposed fix
- columns = dict(zip(rows["value_kind"], rows["column_name"])) + columns = dict(zip(rows["value_kind"], rows["column_name"], strict=True))🤖 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/io/index/query.py` at line 216, Update the zip call that builds columns in the query logic to pass strict=True, ensuring mismatched value_kind and column_name lengths raise an error instead of being silently truncated.Source: Linters/SAST tools
dascore/core/patch.py (1)
103-105: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMinor: declare
_instance_idalongside other private fields.The other private/public fields (
_data,data,coords, etc.) are declared as class-level annotations;_instance_idisn't, which makes it a bit harder to discover as part of the object's persistent state.🤖 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/core/patch.py` around lines 103 - 105, Declare the _instance_id field as a class-level annotation alongside the existing _data, data, and coords fields, while retaining its eager uuid4().hex assignment in the initializer.dascore/io/index/ingest.py (2)
233-239: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd
stacklevelto the reserved-attr-name warning.Without
stacklevel=2, the warning's reported source location points intoingest.pyinstead of the caller's ingest call, making it harder to trace which write triggered it.♻️ Suggested fix
- warnings.warn(msg, UserWarning) + warnings.warn(msg, UserWarning, stacklevel=2)🤖 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/io/index/ingest.py` around lines 233 - 239, Update the warnings.warn call in the reserved-attribute handling block to pass stacklevel=2, so the warning points to the caller of the ingest operation rather than ingest.py.Source: Linters/SAST tools
211-216: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueBare
except Exception: passswallows all errors in the datetime fallback.This is presumably intentional (classify-or-skip for unclassifiable attrs), but catching blind
Exceptioncan mask real bugs (e.g., a brokento_datetime64call) instead of just skipping non-datetime-like values.♻️ Suggested narrowing
try: return TypedValue("time", to_int(to_datetime64(value))) - except Exception: + except (TypeError, ValueError, OverflowError): pass🤖 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/io/index/ingest.py` around lines 211 - 216, Narrow the exception handling around the datetime fallback in the ingest classification flow to catch only the expected conversion/parsing errors from to_datetime64 and to_int. Preserve returning the time TypedValue for valid datetime-like inputs and returning None for unsupported values, while allowing unexpected programming or runtime errors to propagate.Source: Linters/SAST tools
dascore/io/index/indexer.py (1)
32-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider sharing the hidden-columns constant with
catalog.py.
_SPOOL_HIDDEN_COLUMNShere duplicates the inline list incatalog.py'sto_df()(["n_dims", "sample_count_total", "shape"]). Extracting a single shared constant would prevent silent drift if one site changes without the other.🤖 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/io/index/indexer.py` around lines 32 - 35, Share the hidden-column definition between the indexer and catalog code instead of maintaining duplicate values. Extract or reuse a single module-level constant for "n_dims", "sample_count_total", and "shape", update _SPOOL_HIDDEN_COLUMNS and catalog.py's to_df() to reference it, and preserve the existing filtering behavior.
🤖 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 `@dascore/core/spool.py`:
- Around line 807-834: Update _as_catalog_member and _chunk_working_df to
preserve DirectorySpool constructor select_kwargs: retain the selection in the
catalog view or apply the equivalent filter before returning catalog membership
and before constructing chunk-planner input. Ensure catalog-native unions and
chunking cannot reintroduce rows excluded by the constructor selection, while
preserving existing behavior for unrestricted spools.
- Around line 1330-1342: Update __rich__ to guard against a missing dataframe
before calling len(df) or accessing df.columns. Preserve the existing time-span
rendering for non-empty dataframes containing time_min, while allowing
MemorySpool() with _df set to None to return the base representation without
error.
In `@dascore/io/index/indexer.py`:
- Around line 92-125: Update _find_index_path so the explicit index_path branch
returns the same absolute Path value recorded in the index map, rather than the
original potentially relative Path(index_path). Preserve the existing map update
behavior and all other path-selection branches.
In `@dascore/utils/patch.py`:
- Around line 630-634: Update the field construction near coord_fields in the
patch formatting logic to flatten all zipped min/max coordinate pairs into a
single list before concatenating with attrs. Replace the invalid multi-argument
list conversion while preserving the existing field order and reindex behavior
for single- and multi-coordinate inputs.
In `@dascore/utils/paths.py`:
- Around line 33-44: The directory_writable() helper should return False for any
filesystem OSError, including read-only mount errors. Move the
probe.parent.mkdir, file creation, and cleanup operation inside the try block,
catch OSError, and preserve the existing successful cleanup and True return
behavior.
In `@dascore/utils/pd.py`:
- Around line 57-59: Update the validation guard in the relative-select logic to
require both envelope columns, lo_col and the corresponding upper-bound column,
before either is accessed. Preserve the existing InvalidSpoolQueryError path for
missing columns or empty data so incomplete metadata cannot reach the query and
raise KeyError.
In `@tests/test_core/test_patch_chunk.py`:
- Around line 309-316: Strengthen test_merge_unequal_other by comparing the
sorted distance_min/distance_max envelopes of distance_adjacent with those in
out, in addition to the existing length assertion. Verify that each unequal
coordinate is preserved exactly and outputs are not duplicated or altered.
In `@tests/test_core/test_spool.py`:
- Around line 968-974: Update test_repr_without_time_coordinate to match
MemorySpool.__rich__: since the patch has no time_min coordinate, assert that
the rendered output does not contain “Time Span,” unless the renderer is
explicitly changed to add a no-time placeholder.
---
Nitpick comments:
In `@dascore/core/patch.py`:
- Around line 103-105: Declare the _instance_id field as a class-level
annotation alongside the existing _data, data, and coords fields, while
retaining its eager uuid4().hex assignment in the initializer.
In `@dascore/io/index/indexer.py`:
- Around line 32-35: Share the hidden-column definition between the indexer and
catalog code instead of maintaining duplicate values. Extract or reuse a single
module-level constant for "n_dims", "sample_count_total", and "shape", update
_SPOOL_HIDDEN_COLUMNS and catalog.py's to_df() to reference it, and preserve the
existing filtering behavior.
In `@dascore/io/index/ingest.py`:
- Around line 233-239: Update the warnings.warn call in the reserved-attribute
handling block to pass stacklevel=2, so the warning points to the caller of the
ingest operation rather than ingest.py.
- Around line 211-216: Narrow the exception handling around the datetime
fallback in the ingest classification flow to catch only the expected
conversion/parsing errors from to_datetime64 and to_int. Preserve returning the
time TypedValue for valid datetime-like inputs and returning None for
unsupported values, while allowing unexpected programming or runtime errors to
propagate.
In `@dascore/io/index/query.py`:
- Line 216: Update the zip call that builds columns in the query logic to pass
strict=True, ensuring mismatched value_kind and column_name lengths raise an
error instead of being silently truncated.
🪄 Autofix (Beta)
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
Run ID: d2796497-c7c1-41a9-97e7-6597244d2f78
📒 Files selected for processing (62)
dascore/clients/dirspool.pydascore/clients/filespool.pydascore/config.pydascore/core/coords.pydascore/core/patch.pydascore/core/spool.pydascore/core/summary.pydascore/exceptions.pydascore/io/core.pydascore/io/index/__init__.pydascore/io/index/backend.pydascore/io/index/catalog.pydascore/io/index/dialect.pydascore/io/index/indexer.pydascore/io/index/ingest.pydascore/io/index/lite.pydascore/io/index/query.pydascore/io/index/schema.pydascore/io/indexer.pydascore/utils/chunk.pydascore/utils/chunk_plan.pydascore/utils/coordmanager.pydascore/utils/hdf5.pydascore/utils/patch.pydascore/utils/paths.pydascore/utils/pd.pydocs/changelog.qmddocs/contributing/new_format.qmddocs/notes/notes.qmddocs/notes/spool_chunking.qmddocs/notes/spool_index.qmddocs/notes/spool_selection.qmddocs/tutorial/file_io.qmdenvironment.ymlpyproject.tomlscripts/_templates/_quarto.ymltests/conftest.pytests/test_clients/test_dirspool.pytests/test_core/test_patch_chunk.pytests/test_core/test_spool.pytests/test_core/test_spool_select_spec.pytests/test_io/test_dasdae/test_dasdae.pytests/test_io/test_febus/test_febusg1.pytests/test_io/test_h5simple/test_h5simple.pytests/test_io/test_index/test_catalog.pytests/test_io/test_index/test_db_dirspool.pytests/test_io/test_index/test_heterogeneity_stress.pytests/test_io/test_index/test_index_contract.pytests/test_io/test_index/test_index_edge_cases.pytests/test_io/test_index/test_plan.pytests/test_io/test_index/test_schema.pytests/test_io/test_index/test_union.pytests/test_io/test_indexer.pytests/test_io/test_io_core.pytests/test_io/test_pickle/test_pickle.pytests/test_io/test_prodml/test_prod_ml.pytests/test_io/test_terra15/test_terra15.pytests/test_utils/test_chunk.pytests/test_utils/test_config.pytests/test_utils/test_hdf_utils.pytests/test_utils/test_io_utils.pytests/test_utils/test_patch_utils.py
💤 Files with no reviewable changes (3)
- tests/test_io/test_dasdae/test_dasdae.py
- environment.yml
- dascore/clients/filespool.py
|
✅ Documentation built: |
CodeRabbit findings on #751, verified and fixed: - Union of a directory spool built with constructor select_kwargs reintroduced the excluded rows (_as_catalog_member treated any catalog-native spool as the whole catalog). Carry the restricted patch ids when select_kwargs are set. - A bare MemorySpool() has no dataframe, so len(), iteration, and repr raised TypeError. Treat a missing frame as an empty spool. - get_patch_names crashed for multi-coordinate naming (list(*coord_fields) unpacks multiple pairs); flatten the min/max fields explicitly. - directory_writable let a read-only mount's OSError (e.g. EROFS from mkdir) escape instead of returning False; guard the whole probe. - relative_ranges_to_absolute read {name}_max without checking it, raising KeyError instead of the documented InvalidSpoolQueryError. - _find_index_path returned the un-absolutized index_path while recording the absolute form in the index map; return the absolute path. Regression tests added for each; touched modules stay at 100% coverage.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@dascore/utils/paths.py`:
- Around line 37-45: Wrap the cleanup call in the writable-directory probe’s
existing OSError handling so transient failures from os.remove(probe) return
False rather than escaping. Update the probe flow around probe.parent.mkdir,
open, and os.remove while preserving the existing True result only when creation
and cleanup both succeed.
🪄 Autofix (Beta)
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
Run ID: 3a6e4c6e-0fb8-42dd-b3db-9153ff4bd08f
📒 Files selected for processing (15)
dascore/core/spool.pydascore/io/index/indexer.pydascore/io/index/ingest.pydascore/utils/patch.pydascore/utils/paths.pydascore/utils/pd.pytests/conftest.pytests/test_clients/test_dirspool.pytests/test_core/test_spool.pytests/test_io/test_index/test_heterogeneity_stress.pytests/test_io/test_index/test_index_contract.pytests/test_io/test_index/test_union.pytests/test_utils/test_patch_utils.pytests/test_utils/test_paths.pytests/test_utils/test_pd.py
🚧 Files skipped from review as they are similar to previous changes (11)
- tests/test_utils/test_patch_utils.py
- tests/test_io/test_index/test_index_contract.py
- dascore/utils/pd.py
- dascore/io/index/indexer.py
- tests/test_core/test_spool.py
- tests/test_io/test_index/test_union.py
- tests/test_io/test_index/test_heterogeneity_stress.py
- dascore/utils/patch.py
- tests/test_clients/test_dirspool.py
- dascore/io/index/ingest.py
- dascore/core/spool.py
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
dascore/utils/paths.py (1)
36-42: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winUse a unique, exclusive temporary probe file.
The predictable sentinel can clobber an existing file, and
open(..., "w")follows an attacker-created symlink in a writable directory. Usetempfile.mkstemp(dir=probe.parent, prefix=...)(withO_EXCLsemantics) and retain the existing suppressed cleanup.🛡️ Proposed fix
+import tempfile + def directory_writable(path) -> bool: ... - name = "._dascore_write_test_delete_me" - probe = Path(path) / name + probe = None try: probe.parent.mkdir(exist_ok=True, parents=True) - open(probe, "w").close() + fd, probe_name = tempfile.mkstemp( + dir=probe.parent, + prefix="._dascore_write_test_", + ) + os.close(fd) + probe = Path(probe_name) except OSError: return False🤖 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/paths.py` around lines 36 - 42, Replace the predictable probe path and open call in the path-writability check with tempfile.mkstemp(dir=probe.parent, prefix=...), retaining the existing suppressed cleanup and closing the returned file descriptor. Preserve the surrounding mkdir and exception handling while ensuring the probe is uniquely created with exclusive semantics.
🤖 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_core/test_patch_chunk.py`:
- Around line 318-323: Update the _distance_envelopes helper’s zip call to pass
an explicit strict argument, using strict=True to preserve the expected
equal-length behavior while satisfying Ruff B905.
---
Outside diff comments:
In `@dascore/utils/paths.py`:
- Around line 36-42: Replace the predictable probe path and open call in the
path-writability check with tempfile.mkstemp(dir=probe.parent, prefix=...),
retaining the existing suppressed cleanup and closing the returned file
descriptor. Preserve the surrounding mkdir and exception handling while ensuring
the probe is uniquely created with exclusive semantics.
🪄 Autofix (Beta)
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
Run ID: e7c866a4-437f-4b87-ad4d-ab0d9c470bfa
📒 Files selected for processing (4)
dascore/core/spool.pydascore/utils/paths.pytests/test_core/test_patch_chunk.pytests/test_core/test_spool.py
🚧 Files skipped from review as they are similar to previous changes (2)
- tests/test_core/test_spool.py
- dascore/core/spool.py
Six-table summary index: sources, patches (frozen structural columns plus time/distance envelopes), attrs (lazily-added typed columns, one per attr/kind), coords (tall, typed min/max/step), attr_meta, meta_data. - DuckDB, SQLite (STRICT), and Parquet-manifest backends behind one abstract interface; shared SQL generation with a small dialect layer. - Ingest from PatchSummary with pint base-SI unit normalization and sanitized dynamic column names. - Query layer implements the selector semantics spec: attrs-first name resolution, kind dispatch, envelope candidacy with no false negatives, regex as pandas residual. - Contract test suite runs identically against all three backends (36 tests x 3). - Secondary indexes (SQLite EXISTS otherwise quadratic) and dataframe bulk ingest for DuckDB (executemany binds row-at-a-time).
- DBDirectoryIndexer: directory walk via _iter_filesystem, per-source (mtime_ns, size_bytes) change detection instead of a global watermark, stale-source removal folded into update(), scan of changed files only. - DirectorySpool gains an index_engine parameter selecting the duckdb, sqlite, or parquet backend; default behavior unchanged (HDF5 index). - Derived spools share the indexer connection (deepcopy-safe), matching the single-writer model. - Integration tests: patch loading, time select, chunk merge, and add/modify/delete lifecycle against real files for all backends.
SQLite caps bound variables (32766 by default), so replacing or deleting many sources in one call overflowed the parameter list at ~500k sources. Chunk both the path lookup and the id deletions. Also keep a failed rollback from masking the original write error.
Stress test: 300 summaries with randomized dims (1-3 of 10 names incl.
a hostile one), coord dtypes (datetime/timedelta/float/int/str), units,
and attrs with mixed kinds per name and names that collide after
sanitization. Verifies ingest completeness and the no-false-negative
query contract against references computed from the raw summaries.
Fixes:
- attr names that sanitize to the same identifier ("Shot Number" vs
"shot_number") collided on the attrs column; attr_meta is now the
single source of truth for column names with deterministic numeric
suffixes on collision.
- multi-kind attrs coalesce in object space; pandas nullable
BooleanArray refuses cross-dtype fills in Series.where.
The database index replaces the HDF5/PyTables index entirely: - Delete HDFPatchIndexManager, _HDF5Store, open_hdf5_file, the kernel query, and the PyTables reader/writer wrappers; H5Reader stands alone. io/indexer.py keeps AbstractIndexer and the index-location map helpers. - Drop tables>=3.7 from dependencies (it was only used by the index; all FiberIO readers use h5py) and remove the pytables warning filters. Add duckdb as an optional extra (needed for the duckdb/parquet engines; sqlite default has no extra dependency). - DirectorySpool now defaults to index_engine="sqlite". DBDirectoryIndexer gains: read-only-archive index relocation (config-backed, engine-keyed map), update(paths=...), one automatic update on first query of a brand-new index, and directory-format scan units - a directory FiberIO (e.g. XMLBinary) indexes as one source keyed by the directory with aggregate stats (max member mtime, summed size), mirroring dc.scan's skip protocol. Every visited path gets a sources row even when it yields no patches, so non-fiber files don't force perpetual rescans; the root-as-source path is spelled ".". - Ingest hardening: container/array values can no longer slip through the datetime fallback into the index. - Tests: port conftest and io tests from tables to h5py (pytables duck-typing tests use importorskip); rewrite indexer tests for DBDirectoryIndexer; add an edge-case suite bringing dascore/io/index to 100% line coverage. The #583 skip-warning test now asserts the fixed behavior (index-level distance selection) and the febus chunk test passes conflict="keep_first", matching in-memory spool semantics that the old index masked by dropping non-schema attrs.
Split the coords table into coord_defs (one row per unique coordinate summary) and patch_coords (patch -> name/dims -> def links). The def key is the CoordSummary fingerprint when the scan provides one (exact value identity, truncated to 128 bits) or a hash of the stored summary fields otherwise (lossless for the index, too weak for value-identity claims). Name and dims stay on the link since two patches can share values under different names. Defs are upserted with batched key lookups, reused across writes, and left orphaned on source deletion (a rebuild compacts them). Coord predicates join patch_coords x coord_defs; query timings are unchanged. This is groundwork, not a storage win: time coords are unique per file so their defs don't dedup (index grows ~40% on time-indexed archives), but shared coords collapse to single rows, chunk/merge can later recognize shared coordinates by coord_def_id equality instead of value comparison, and coord_defs is the natural home for exact coord arrays if full-coords storage lands.
The flat relation now carries {name}_min/{name}_max/{name}_step for
every coord in the result beyond the time/distance envelopes cached on
patches, restoring parity with memory-spool dataframes (chunking on any
dim now works from a directory spool). Every coord also gets a private
_{name}_def_key column: the globally-stable coordinate identity that
future chunk/merge grouping keys on (underscore-prefixed so it does not
yet participate in merge-compatibility comparisons).
Groundwork for merged/universal spools: base_uri is stored as "" (never NULL) so plain equality works on every engine; source replacement and deletion are scoped by base_uri; identical relative paths under different bases coexist. The flat relation prefixes base_uri onto paths only when non-empty.
The catalog owns the index tables (via any backend) and composed selection state; resolvers turn flat-relation rows into patches (FileResolver through dc.read with trim hints -- remoteness belongs to the path layer; LiveResolver from an in-memory registry with synthetic memory:// source identities); the directory indexer plugs in as the syncer for directory-backed catalogs. Laziness: from_patches does no metadata work until the first metadata operation (backend bootstrap costs ~10s of ms; holding a patch list is free). select composes Query predicates with eager name validation and no SQL; realization runs one query per view. samples selections are patch-local; relative bounds resolve against the view envelope; coord range predicates re-apply exactly at patch load (two-stage select). Mutation is root-only; views share backend and resolver. Also: build_query_sql accepts AND-composed query sequences.
Patch-list memory spools now build their managing dataframes from the catalog's flat relation and resolve patches through the shared LiveResolver, completing stage 1: one metadata engine for every spool type. Spools created from other spools/dataframes keep the legacy flat-dump path. Derived spools share the catalog (deepcopy-safe); catalogs pickle by rebuilding their backend from registered patches. Correctness fixes surfaced by the rewire: - ns-epoch integers never pass through float64 (which corrupts them by ~100 ns and breaks merge boundary arithmetic): exact masked conversion in _flatten, and duckdb/parquet fetch through arrow with integer_object_nulls (df() floats nullable BIGINTs). - numeric envelope columns coerce object-None to float NaN so sorting and chunking work on coords without steps. - all-relative time results serve timedelta envelopes so chunking on relative time keeps working (#553). - MemorySpool drops synthetic identity columns (path, file_format, file_version, source_patch_id) before chunk merge-compat comparisons, as DirectorySpool always has. - get_patch_names ignores memory:// synthetic paths and renders absent name-field columns as empty, so generated names (and DASDAE group names) are identical whichever metadata engine produced the frame.
DataFrameSpool.select now implements the selector semantics spec at the user-facing surface, for memory and directory spools alike: - Unknown names raise InvalidSpoolQueryError with the valid attribute and coordinate names (closes #435). Bare names resolve attrs-first, then coords. - _attrs / _coords dict kwargs disambiguate explicitly and validate against their own namespace only. - samples=True selections are coordinate-only and patch-local: they never exclude patches; the selection is recorded and applied to each patch as it loads, surviving chunk and other derived spools (closes the second failure mode of #447). - relative=True resolves range bounds against the spool's coordinate envelope, mirroring Patch.select semantics at spool scope (closes #362). InvalidSpoolQueryError moves to dascore.exceptions (avoiding a circular import); the index query module re-imports it from there.
One relative-bound resolver (query.relative_offset) serves catalog and spool selects; ingest uses dataclasses.replace; select drops its split_df_query call, which strict name validation made dead (every validated name matches a dataframe column or coord range, so the extra kwargs were always empty).
patch.summary is a cached_property; building fresh PatchSummary objects in _live_records discarded fingerprints and summaries the patch already had. Reusing them makes catalog ingest of previously-summarized patches ~3x faster (first get_contents at 300 patches: 208 -> 75 ms) and turns the remaining summary cost into once-per-patch-lifetime instead of once-per-spool.
…e removed The PatchAssembler loses its orphaned indexing front end and post- select plumbing (the plan resolver only consumes the merge internals), get_column_names_from_dim loses its last caller, and the planned- catalog converters simplify to the pandas scalar forms that actually reach them. New tests pin the operation-order compositions the review demanded: collapse with value and quantity residuals, regex+window+ attr-sort chains, attr membership arrays, envelope-column sort names, membership-restricted pickling of union views, third-party BaseSpool members, negative samples windows, and complete-envelope-overlap merges. Touched modules are back to 100% coverage locally.
…eep every coordinate Combining spools now preserves each operand's current contents: __add__ auto-materializes operands carrying union-lossy lazy state (coordinate and samples residual trims, sort specs) into identity-plan derived catalogs — table work only, no patch loads — while membership-style state still unions by rows so identity dedup keeps working. Derived catalogs record every coordinate of their members (numeric, time, and string), aggregated from the member source rows, with def-key identity kept only when values provably survive assembly; identity claims are likewise dropped for any coordinate riding a residual-trimmed dim. Sorting accepts any known coordinate (non-hot names order by their coord_defs envelope minimum through a correlated subquery), samples envelopes use the last included index and respect orientation, sampling groups compare magnitudes against a stable anchor (descending contiguous patches now merge; tolerance chains can no longer drift), concatenating an empty spool returns an empty spool, and the removed PyTables reader/writer aliases are documented in the changelog. The _attrs/_coords namespaces additionally accept a name or collection of names tagging bare kwargs.
…ings at merge The planner grouped patches by raw SI envelope magnitudes with no knowledge of the chunked dimension's units, so a metre patch and a seconds patch with contiguous magnitudes planned into one output whose assembly failed only at patch access. The canonical (base) unit now rides the flat relation as a private per-coord column and joins the sampling partition: incompatible dimensionality — and unitful next to unitless, which assembly refuses too — can never share an output. Compatible spellings of one dimensionality (metres with feet) still plan together and now genuinely merge: assembly converts each member's merge-dim units to the first member's, and the raw-concatenation merge fallback reattaches the verified common unit it previously dropped. Derived catalogs carry the canonical units on their coordinate definitions, so unit metadata survives restructuring.
…ectories, whole transactions Chunking a restructured spool along a different dimension now plans over the spool's current output rows (loaded through the plan resolver), so it keeps the boundaries the earlier operation assembled; re-chunking the same dimension still collapses to the trimmed members. Directory catalogs carry a per-patch default presentation order (ORDER BY time with the ordinal/patch-id tiebreak) because source-grain ordinals cannot interleave a multi-patch file that straddles a patch of another file; the default order is a catalog contract, not view state, so roots still update. The membership-restricted resolver keeps the plan routes its rows reference (mixed planned/live views survive pickling and process-backed map), the segmented-coordinate write guard keys on what a spool resolves rather than where its members live (plan- assembled file-backed spools are guarded; purely file-backed spools still skip inspection), the SQLite statement lock now spans whole transactions so shared-connection readers can never observe a half-applied source replacement, and only mark_initial_update_done — after renumbering succeeds — sets the initial-update marker, keeping the reopen recovery path alive when a sync dies mid-way.
A patch carrying the chunk name solely as a non-dimensional coordinate cannot be trimmed or merged along it, but envelope presence made the planner treat it as chunkable: merge outputs failed with CoordError only at patch access, and segmenting happened to slice the riding dimension for numeric coordinates while producing wrong plans for datetime ones. Such patches now fall under missing_dim with the rest of the dimension-less rows — the default raise names how many ride the name as a coordinate, and missing_dim='drop' excludes them.
The per-patch directory order lived only in the catalog's default-order spec, so combining a directory spool fell back to source-record transfer and re-lost the interleaved presentation the order exists to provide — even against an empty spool, breaking order-sensitive equality with itself. The union handoff now bakes the effective order into an identity plan, but only when record-grain transfer would actually present rows differently, so ordinary archives keep record transfer and same-source deduplication. Ordering also treats missing values consistently: rows without a value for the order key sort last under any direction, matching the ordinal renumberer's missing-time-last rule instead of SQLite's NULLs-first default.
812a6c4 to
5ca467d
Compare
Spool equality compared raw rows plus residual state, so a trimmed view never equaled its union-materialized twin despite identical contents, and the view's rows still carried the untrimmed coordinate def keys — an identity a view cannot restate honestly without loading. Equality now folds samples residuals into the compared envelopes (value residuals already present in the realized rows), keeps presented-but-empty rows, and drops def keys and private bookkeeping from the comparison; data values were never compared here anyway. The planner's samples adjustment also resolves negative indices per patch from the envelope-derived sample count (unknown counts keep the candidacy envelope), so chunking a tail selection reports the envelopes the loaded patches actually have.
The ordering section still described source-ordinal renumbering as the whole directory contract; it now documents the per-patch time presentation (missing-time last, ordinal/patch-id tiebreaks) with renumbering demoted to the stability/dedup grain. Federation documents the row-vs-baked transfer split, plan routing, and effective-contents equality; the flat relation lists the canonical-units column; the chunking note covers one-dim-per-call chaining, unit and orientation partitioning, and non-dim coordinates counting as missing — each new claim with an executable cell. The spool tutorial loses the stale warning that some spool types lack concatenate.
Replaces DASCore's PyTables/HDF5 spool index with a relational SQLite metadata engine and unifies every spool type (in-memory, directory, and their unions) on top of it. Selection, candidate identification, chunk planning, and union now go through one lazy
PatchCatalog, so the same pushdown-capable semantics apply whether patches come from files or memory. The oldChunkManagerand PyTables index are removed.Motivation
The old index stored a flat, per-directory PyTables table and only served directory spools; in-memory and union spools each had their own ad-hoc metadata paths, and chunking went through a separate
ChunkManager. Behavior diverged across spool types, queries kept everything in memory, and scaling to large archives was awkward. A relational index with a real query planner lets one code path answer counts, selections, and chunk plans lazily and push work down to the database.What changed
io/index/). A normalized seven-table schema (sources, patches, attrs, coordinate definitions, links) with keys, value-kind checks, and cascade foreign keys, validated before any mutation. Coordinate definitions separate an exact fingerprint from a summary-only dedup key. Nullable nanosecond columns are fetched without a float64 round trip (ns epochs exceed 2**53). Ingest normalizes unit-bearing values to canonical base SI, and the query builder compiles the selector spec to SQL (attrs exact, regex as candidate + residual, coordinate predicates candidacy-only with exact re-trim at load).PatchCatalogowns the backend, a resolver, and the directory synchronizer; directory and memory spools share it, so lazy selection, pushdown, and chunk planning are identical across types.spool + spoolunions catalogs table-to-table. In-memory patches carry an eager instance identity, giving spools set semantics by patch identity.ChunkManageris replaced by an inspectableChunkPlan, exposed publicly asSpool.chunk_plan(...)with the same arguments aschunk.(start, stop)range or a boolean mask; scalar/membership selectors are rejected eagerly.pyproject.toml/environment.yml; the chunk planner, path classifiers, and relative-selection helpers moved toutils/.Summary by CodeRabbit
New Features
selectoptions (_attrs/_coords,samples,relative),chunk_plan, andchunk(..., group, missing_dim), plus spool-union support (spool + spool).sampling_group_tolerance,groupby_attrs).Bug Fixes
Documentation
Chores
Changelog
dc.Spool;MemorySpool,DirectorySpool,FileSpool, thedascore.clientspackage, and theselect_kwargsparameter are removed — usedc.spool(...)andspool.has_live_patches.Spool.update()is allowed only on a root spool; any derived spool (select, slice, sort, chunk,+) raises.Spool.selectno longer accepts coordinate boolean masks — usespool.map(lambda p: p.select(dim=mask));spool[bool_array]still selects membership andsamples=Trueranges still apply per patch..dascore_index.sqlite3; the PyTables index is gone, so existing indexes must be deleted and rebuilt.PyTablesReader,PyTablesWriter, and theHDF5Reader/HDF5Writeraliases fromdascore.utils.hdf5; useH5Reader/H5Writer.a + bbakes each operand's pending trims and sort order into the union instead of dropping them, including per-patch time order for interleaved multi-patch files.Spool.sort(...)accepts any coordinate of the spool, not justtime/distance, and contiguous descending-coordinate patches now merge underchunk(dim=None).Spool.selectvalidates unknown names and quantity dimensionality, composes chained regexes with AND, supports explicit_attrs/_coordsnamespaces, and skips (with a warning) attribute values whose units are dimensionally incompatible across files instead of failing the index update.