Skip to content

Add snapshot creation mechanism#39

Merged
leoyala merged 11 commits into
mainfrom
36-create-snapshot-mechanism
Jul 15, 2026
Merged

Add snapshot creation mechanism#39
leoyala merged 11 commits into
mainfrom
36-create-snapshot-mechanism

Conversation

@leoyala

@leoyala leoyala commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Summary

This PR introduces the capability to create a full snapshot into the local store given a database from a file. This functionality can be called from the CLI, or by importing SnapshotBuilder from dstrack.snapshot.

Changes

  • Creates classes to compute snapshot metadata and statistics.
  • Creates mechanism to register new file readers by users.
  • Creates classes to create snapshot from a given dataset.
  • Creates a Typer script to create a benchmark to compute how long the computation of a snapshot takes.

Test plan

  • uv run pytest passes

Summary by CodeRabbit

  • New Features
    • Added dstrack track to compute/store dataset snapshots with lineage continuity, plus --max-rows to cap in-memory stats.
    • Added dstrack-benchmark to generate synthetic CSV benchmarks with environment capture, console reporting, and optional profiling call-tree output.
    • Added a pluggable reader registry and per-invocation reader resolution by extension, registered name, or import spec.
  • Documentation
    • Documented per-invocation reader selection and that reader details are never persisted in stored snapshot data.
    • Added initial snapshot API docs.
  • Bug Fixes
    • Improved robustness with clearer errors for missing datasets, input-too-large datasets, and store corruption.
  • Chores
    • Added/expanded comprehensive tests for readers, snapshot metadata/stats, store/lineage writing, and benchmark/track CLIs.

@leoyala leoyala added this to the v0.1.0 milestone Jul 14, 2026
@leoyala leoyala self-assigned this Jul 14, 2026
@leoyala leoyala linked an issue Jul 14, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@leoyala, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7d4f7788-ad8f-465e-b729-7e08f6f7fb7b

📥 Commits

Reviewing files that changed from the base of the PR and between 72d0637 and 72021d1.

📒 Files selected for processing (2)
  • src/dstrack/readers/_registry.py
  • tests/test_benchmark_cli.py
📝 Walkthrough

Walkthrough

Adds reader discovery and per-invocation selection, snapshot metadata/statistics, local lineage-aware storage, dstrack track, and a synthetic benchmark CLI with profiling and console reporting.

Changes

Snapshot tracking and benchmark pipeline

Layer / File(s) Summary
Reader registry and resolution
src/dstrack/readers/*, tests/test_reader_registry.py
Adds validated reader factories, registry and plugin discovery, extension/name inference, import-spec loading, and reader-resolution tests.
Snapshot metadata and statistics
src/dstrack/snapshot/*, tests/test_snapshot_metadata.py, tests/test_snapshot_stats.py
Adds versioned metadata, source and schema hashing, snapshot construction, typed dataset statistics, row limits, and coverage tests.
Tracking command and local snapshot storage
src/dstrack/_track.py, src/dstrack/_cli.py, src/dstrack/store.py, src/dstrack/errors.py, docs/decisions/0003-local-snapshot-store-layout.md, tests/test_store.py, tests/test_track.py
Adds dstrack track, reader selection, lineage-aware atomic persistence, storage errors, CLI wiring, and non-persistent per-invocation reader rules.
Synthetic benchmark generation and execution
src/dstrack/_benchmark/_synthetic.py, src/dstrack/_benchmark/_runner.py, src/dstrack/_benchmark/_environment.py, src/dstrack/_benchmark/__init__.py
Adds reproducible CSV generation, benchmark phase timing, environment capture, profiling control, result types, and package exports.
Profiling model and console reporting
src/dstrack/_benchmark/_profiling.py, src/dstrack/_benchmark/_report.py
Adds scoped call-graph construction and Rich rendering for environment, benchmark results, and limited call trees.
Benchmark CLI entry point
src/dstrack/_benchmark/_cli.py, pyproject.toml, tests/test_benchmark_cli.py, codecov.yaml, docs/API/snapshot.md
Adds configurable benchmark execution, temporary CSV lifecycle, result reporting, the dstrack-benchmark console script, coverage configuration, API documentation, and CLI behavior tests.
Documentation cross-reference cleanup
src/dstrack/_logging.py, src/dstrack/paths.py, src/dstrack/readers/*
Updates documentation references for logging, paths, reader protocols, and CSV reader behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant BenchmarkCLI
  participant SyntheticCSV
  participant BenchmarkRunner
  participant SnapshotPipeline
  participant ConsoleReporter
  User->>BenchmarkCLI: run benchmark options
  BenchmarkCLI->>SyntheticCSV: generate temporary dataset
  BenchmarkCLI->>BenchmarkRunner: run generated CSV
  BenchmarkRunner->>SnapshotPipeline: build metadata and compute statistics
  BenchmarkRunner->>ConsoleReporter: report timings and call graph
  ConsoleReporter-->>User: render benchmark results
Loading

Possibly related PRs

  • leoyala/dstrack#13: Adds related reader protocol and CSV reader work used by this reader factory and resolution layer.
  • leoyala/dstrack#29: Introduces the local snapshot store layout extended here with reader-selection and non-persistence rules.

Suggested labels: enhancement

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding snapshot creation functionality.
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 36-create-snapshot-mechanism

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.

@leoyala leoyala linked an issue Jul 14, 2026 that may be closed by this pull request
@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

Test Results

   48 files  ±    0     48 suites  ±0   1m 28s ⏱️ +50s
  206 tests +  147    206 ✅ +  147  0 💤 ±0  0 ❌ ±0 
4 944 runs  +3 528  4 944 ✅ +3 528  0 💤 ±0  0 ❌ ±0 

Results for commit 72021d1. ± Comparison against base commit 70c632e.

♻️ This comment has been updated with latest results.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 11

🤖 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 `@src/dstrack/_benchmark/_profiling.py`:
- Around line 109-126: Update the root selection in the graph-building flow so
frames with at least one caller outside the scoped frame set are included as
roots, alongside frames not present in callees. Preserve recursive components by
tracking out-of-scope callers while iterating through callers, then pass the
combined root candidates to graph._by_cumulative.

In `@src/dstrack/_benchmark/_runner.py`:
- Around line 121-132: Reset the profiler at the start of each run in
BenchmarkRunner.run so reused BenchmarkRunner instances produce call graphs
containing only the current invocation. Recreate or clear self._profiler while
preserving the existing disabled behavior when profile is false.

In `@src/dstrack/_benchmark/_synthetic.py`:
- Around line 86-92: Update SyntheticCsvSpec in
src/dstrack/_benchmark/_synthetic.py to validate every num_* count as
non-negative and require null_rate to be finite and within [0, 1]. Update
CallGraph.trees in src/dstrack/_benchmark/_profiling.py to validate
profile_limit/max_children as non-negative before applying the child limit.
Update the Typer options in src/dstrack/_benchmark/_cli.py to expose the same
bounds and reject invalid values at CLI parsing.

In `@src/dstrack/readers/_registry.py`:
- Around line 143-154: Validate each normalized extension in the registration
flow before duplicate checks or any mutation of _BY_NAME and _BY_EXTENSION.
Require every extension in claimed to use the documented leading-dot format,
rejecting malformed values such as “csv” with a clear ValueError; preserve valid
extension registration behavior.
- Around line 79-105: The ReaderFactory validation in check_reader_class and
build_reader must verify that from_path is specifically a classmethod, not
merely an existing attribute, before invoking it. Use descriptor-level
inspection to enforce the classmethod contract, and validate that build_reader
returns a TabularReader, rejecting any other constructed value.

In `@src/dstrack/snapshot/_builder.py`:
- Around line 81-91: Update the snapshot-building flow around
_metadata_builder.build and _stats_computer.compute so source_hash and stats are
derived from the same immutable file state. Hash and compute statistics from one
read snapshot, or detect a file change between operations and abort or retry
instead of persisting mixed-state metadata.

In `@src/dstrack/snapshot/_stats.py`:
- Around line 131-132: Define and implement an explicit policy for non-finite
values in the stats accumulator: detect NaN and infinities in update and prevent
them from reaching histogram step or bucket calculations. Apply the same
handling in the related histogram-building paths at the noted ranges, and add
tests covering both NaN and infinite inputs according to the chosen policy.
- Around line 128-129: Update the snapshot statistics state around vals,
null_count, and the distinct-value tracking at the referenced logic to avoid
retaining unbounded input data in memory. Replace full numeric and row
collections with bounded sketches or spill-to-disk storage; if that is not
supported, enforce and document a clear maximum input size before processing.

In `@src/dstrack/store.py`:
- Around line 115-128: Serialize snapshot creation and recovery with a
cross-process lock covering parent selection, snapshot-file write, log append,
and HEAD update. Update recovery/path-matching logic to treat the snapshot
referenced by HEAD as the latest committed entry, ignoring any trailing or
orphaned log records after a crash; ensure concurrent writers cannot use the
same parent or leave log.jsonl ahead of HEAD.
- Around line 96-121: Validate dataset_id and snapshot_id as canonical UUIDs, or
at minimum reject path components containing separators or traversal segments,
before any filesystem access or path joins in the snapshot-writing flow. Apply
the checks before _check_dataset_exists, dataset directory construction, and
snapshot file construction, while preserving the existing lineage and snapshot
behavior for valid identifiers.

In `@tests/test_snapshot_metadata.py`:
- Around line 80-83: Update test_dataset_path_stored_as_string to define the
expected dataset path as a Path and compare meta.dataset_path with
str(expected_path), rather than asserting a hard-coded slash-separated string.
Preserve the test’s verification that the built metadata stores the path as a
string.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9425a2ae-4d62-4c10-9aaa-52aa38a9b67f

📥 Commits

Reviewing files that changed from the base of the PR and between 70c632e and ffa828c.

📒 Files selected for processing (27)
  • docs/decisions/0003-local-snapshot-store-layout.md
  • pyproject.toml
  • src/dstrack/_benchmark/__init__.py
  • src/dstrack/_benchmark/_cli.py
  • src/dstrack/_benchmark/_environment.py
  • src/dstrack/_benchmark/_profiling.py
  • src/dstrack/_benchmark/_report.py
  • src/dstrack/_benchmark/_runner.py
  • src/dstrack/_benchmark/_synthetic.py
  • src/dstrack/_cli.py
  • src/dstrack/_track.py
  • src/dstrack/errors.py
  • src/dstrack/readers/__init__.py
  • src/dstrack/readers/_csv.py
  • src/dstrack/readers/_protocol.py
  • src/dstrack/readers/_registry.py
  • src/dstrack/readers/_resolve.py
  • src/dstrack/snapshot/__init__.py
  • src/dstrack/snapshot/_builder.py
  • src/dstrack/snapshot/_metadata.py
  • src/dstrack/snapshot/_stats.py
  • src/dstrack/snapshot/_version.py
  • src/dstrack/store.py
  • tests/test_reader_registry.py
  • tests/test_snapshot_metadata.py
  • tests/test_snapshot_stats.py
  • tests/test_store.py

Comment thread src/dstrack/_benchmark/_profiling.py
Comment thread src/dstrack/_benchmark/_runner.py
Comment thread src/dstrack/_benchmark/_synthetic.py
Comment thread src/dstrack/readers/_registry.py Outdated
Comment thread src/dstrack/readers/_registry.py
Comment thread src/dstrack/snapshot/_stats.py
Comment thread src/dstrack/snapshot/_stats.py Outdated
Comment thread src/dstrack/store.py Outdated
Comment thread src/dstrack/store.py Outdated
Comment thread tests/test_snapshot_metadata.py

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/test_benchmark_cli.py (1)

22-31: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer field(default_factory=list) over a hand-written __init__ on the dataclass.

_RecordingReporter declares kept_paths/reported as dataclass fields but then overrides __init__ to initialize them manually — this defeats the point of @dataclass generating __init__.

♻️ Proposed fix
+from dataclasses import dataclass, field
+
 `@dataclass`
 class _RecordingReporter:
     """Stands in for ConsoleReporter, recording what the CLI asked it to print."""
 
-    kept_paths: list[Path]
-    reported: list[tuple[BenchmarkRun, int]]
-
-    def __init__(self) -> None:
-        self.kept_paths = []
-        self.reported = []
+    kept_paths: list[Path] = field(default_factory=list)
+    reported: list[tuple[BenchmarkRun, int]] = field(default_factory=list)
🤖 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_benchmark_cli.py` around lines 22 - 31, Update the
_RecordingReporter dataclass to use field(default_factory=list) for kept_paths
and reported, and remove its hand-written __init__ so the dataclass-generated
initializer handles both fields.
🤖 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_benchmark_cli.py`:
- Around line 57-68: Update _StubRunner to declare instances as a class-level
ClassVar initialized to an empty list, and remove the per-instance
self.instances assignment from __init__. Keep type(self).instances.append(self)
so all constructed runners remain recorded through the class attribute without
relying on fixture setup.

---

Nitpick comments:
In `@tests/test_benchmark_cli.py`:
- Around line 22-31: Update the _RecordingReporter dataclass to use
field(default_factory=list) for kept_paths and reported, and remove its
hand-written __init__ so the dataclass-generated initializer handles both
fields.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b24755f5-9dfc-4ec3-8523-22b4cdbf4761

📥 Commits

Reviewing files that changed from the base of the PR and between 41e5dc8 and 4613394.

📒 Files selected for processing (23)
  • codecov.yaml
  • docs/API/snapshot.md
  • src/dstrack/_benchmark/__init__.py
  • src/dstrack/_benchmark/_cli.py
  • src/dstrack/_benchmark/_environment.py
  • src/dstrack/_benchmark/_profiling.py
  • src/dstrack/_benchmark/_report.py
  • src/dstrack/_benchmark/_runner.py
  • src/dstrack/_benchmark/_synthetic.py
  • src/dstrack/_cli.py
  • src/dstrack/_logging.py
  • src/dstrack/paths.py
  • src/dstrack/readers/__init__.py
  • src/dstrack/readers/_csv.py
  • src/dstrack/readers/_protocol.py
  • src/dstrack/readers/_registry.py
  • src/dstrack/readers/_resolve.py
  • src/dstrack/snapshot/__init__.py
  • src/dstrack/snapshot/_metadata.py
  • src/dstrack/snapshot/_stats.py
  • src/dstrack/snapshot/_version.py
  • src/dstrack/store.py
  • tests/test_benchmark_cli.py
🚧 Files skipped from review as they are similar to previous changes (18)
  • src/dstrack/_benchmark/init.py
  • src/dstrack/snapshot/_version.py
  • src/dstrack/snapshot/init.py
  • src/dstrack/readers/init.py
  • src/dstrack/readers/_csv.py
  • src/dstrack/_cli.py
  • src/dstrack/snapshot/_metadata.py
  • src/dstrack/_benchmark/_report.py
  • src/dstrack/_benchmark/_synthetic.py
  • src/dstrack/_benchmark/_runner.py
  • src/dstrack/store.py
  • src/dstrack/_benchmark/_profiling.py
  • src/dstrack/_benchmark/_cli.py
  • src/dstrack/readers/_protocol.py
  • src/dstrack/readers/_resolve.py
  • src/dstrack/_benchmark/_environment.py
  • src/dstrack/readers/_registry.py
  • src/dstrack/snapshot/_stats.py

Comment thread tests/test_benchmark_cli.py

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
tests/test_track.py (1)

82-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Misleading comment on directory-entry count.

The comment says "2 snapshots + HEAD file" but the dataset directory actually contains a snapshots/ subdirectory (holding the 2 JSON files), log.jsonl, and HEAD — three top-level entries, not two snapshot files plus HEAD. The assertion passes by coincidence (3 == 3); consider asserting the actual structure directly (e.g., snapshot count via glob("snapshots/*.json"), and existence of log.jsonl/HEAD) so a future change to the on-disk layout doesn't produce a confusing failure.

✏️ Suggested clarification
-    # Both snapshots live under the same dataset directory.
-    dataset_dirs = list((store / "datasets").iterdir())
-    assert len(dataset_dirs) == 1
-    # Snapshot dir should contain 2 snapshots + HEAD file
-    snapshot_dir = dataset_dirs[0]
-    assert len(list(snapshot_dir.iterdir())) == 3
+    # Both snapshots live under the same dataset directory.
+    dataset_dirs = list((store / "datasets").iterdir())
+    assert len(dataset_dirs) == 1
+    dataset_dir = dataset_dirs[0]
+    assert len(list((dataset_dir / "snapshots").glob("*.json"))) == 2
+    assert (dataset_dir / "log.jsonl").exists()
+    assert (dataset_dir / "HEAD").exists()
🤖 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_track.py` around lines 82 - 86, Update the assertions following
dataset_dirs in the track test to validate the actual on-disk structure: assert
that snapshots contains two JSON files, and explicitly assert the existence of
log.jsonl and HEAD. Remove the misleading top-level entry-count assertion and
replace the inaccurate comment with structure-specific validation.
🤖 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.

Nitpick comments:
In `@tests/test_track.py`:
- Around line 82-86: Update the assertions following dataset_dirs in the track
test to validate the actual on-disk structure: assert that snapshots contains
two JSON files, and explicitly assert the existence of log.jsonl and HEAD.
Remove the misleading top-level entry-count assertion and replace the inaccurate
comment with structure-specific validation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e32979f7-1570-473c-a424-f67f4cdb277b

📥 Commits

Reviewing files that changed from the base of the PR and between 4613394 and 72d0637.

📒 Files selected for processing (16)
  • docs/decisions/0001-dataset-snapshot-content.md
  • src/dstrack/_benchmark/_cli.py
  • src/dstrack/_benchmark/_profiling.py
  • src/dstrack/_benchmark/_runner.py
  • src/dstrack/_benchmark/_synthetic.py
  • src/dstrack/_track.py
  • src/dstrack/errors.py
  • src/dstrack/readers/_registry.py
  • src/dstrack/snapshot/_builder.py
  • src/dstrack/snapshot/_stats.py
  • src/dstrack/store.py
  • tests/test_benchmark_cli.py
  • tests/test_reader_registry.py
  • tests/test_snapshot_stats.py
  • tests/test_store.py
  • tests/test_track.py
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/dstrack/_track.py
  • src/dstrack/errors.py
  • src/dstrack/snapshot/_builder.py
  • src/dstrack/_benchmark/_profiling.py
  • src/dstrack/_benchmark/_synthetic.py
  • tests/test_benchmark_cli.py
  • src/dstrack/_benchmark/_runner.py
  • src/dstrack/_benchmark/_cli.py
  • src/dstrack/snapshot/_stats.py
  • src/dstrack/readers/_registry.py

@leoyala
leoyala merged commit 15cf68b into main Jul 15, 2026
33 checks passed
@leoyala
leoyala deleted the 36-create-snapshot-mechanism branch July 15, 2026 16:34
@leoyala leoyala linked an issue Jul 15, 2026 that may be closed by this pull request
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.

benchmark Create snapshot mechanism choose reader strategy

1 participant