matview-cache 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. matview_cache-0.1.0/.github/dependabot.yml +12 -0
  2. matview_cache-0.1.0/.github/workflows/ci.yml +47 -0
  3. matview_cache-0.1.0/.github/workflows/docs.yml +43 -0
  4. matview_cache-0.1.0/.github/workflows/release.yml +110 -0
  5. matview_cache-0.1.0/.gitignore +17 -0
  6. matview_cache-0.1.0/.pre-commit-config.yaml +42 -0
  7. matview_cache-0.1.0/AGENTS.md +56 -0
  8. matview_cache-0.1.0/CHANGELOG.md +159 -0
  9. matview_cache-0.1.0/CITATION.cff +28 -0
  10. matview_cache-0.1.0/DECISIONS.md +859 -0
  11. matview_cache-0.1.0/LICENSE +21 -0
  12. matview_cache-0.1.0/Makefile +108 -0
  13. matview_cache-0.1.0/PKG-INFO +262 -0
  14. matview_cache-0.1.0/README.md +210 -0
  15. matview_cache-0.1.0/RELEASING.md +69 -0
  16. matview_cache-0.1.0/ROADMAP.md +210 -0
  17. matview_cache-0.1.0/SECURITY.md +51 -0
  18. matview_cache-0.1.0/SPEC.md +58 -0
  19. matview_cache-0.1.0/benchmarks/README.md +23 -0
  20. matview_cache-0.1.0/benchmarks/scale.py +306 -0
  21. matview_cache-0.1.0/docs/cli.md +140 -0
  22. matview_cache-0.1.0/docs/design-spec.md +463 -0
  23. matview_cache-0.1.0/docs/graph.md +185 -0
  24. matview_cache-0.1.0/docs/guide.md +439 -0
  25. matview_cache-0.1.0/docs/index.md +65 -0
  26. matview_cache-0.1.0/docs/performance.md +123 -0
  27. matview_cache-0.1.0/docs/reference.md +109 -0
  28. matview_cache-0.1.0/examples/finance/pipeline.py +149 -0
  29. matview_cache-0.1.0/examples/linguistics/pipeline.py +71 -0
  30. matview_cache-0.1.0/examples/linguistics/recipes/phylo_tree.py +63 -0
  31. matview_cache-0.1.0/examples/linguistics/recipes/phylo_tree.toml +20 -0
  32. matview_cache-0.1.0/mkdocs.yml +59 -0
  33. matview_cache-0.1.0/pyproject.toml +104 -0
  34. matview_cache-0.1.0/src/matview/__init__.py +78 -0
  35. matview_cache-0.1.0/src/matview/cache.py +750 -0
  36. matview_cache-0.1.0/src/matview/canon.py +116 -0
  37. matview_cache-0.1.0/src/matview/cli.py +900 -0
  38. matview_cache-0.1.0/src/matview/codecs/__init__.py +159 -0
  39. matview_cache-0.1.0/src/matview/codecs/_arrow.py +104 -0
  40. matview_cache-0.1.0/src/matview/codecs/_arrow_ipc.py +89 -0
  41. matview_cache-0.1.0/src/matview/codecs/_arrow_pandas.py +76 -0
  42. matview_cache-0.1.0/src/matview/codecs/_arrow_parts.py +99 -0
  43. matview_cache-0.1.0/src/matview/codecs/_json.py +36 -0
  44. matview_cache-0.1.0/src/matview/codecs/_npy.py +53 -0
  45. matview_cache-0.1.0/src/matview/context.py +87 -0
  46. matview_cache-0.1.0/src/matview/errors.py +96 -0
  47. matview_cache-0.1.0/src/matview/fingerprint.py +99 -0
  48. matview_cache-0.1.0/src/matview/foreign.py +436 -0
  49. matview_cache-0.1.0/src/matview/graph.py +876 -0
  50. matview_cache-0.1.0/src/matview/policy.py +67 -0
  51. matview_cache-0.1.0/src/matview/py.typed +0 -0
  52. matview_cache-0.1.0/src/matview/registry.py +293 -0
  53. matview_cache-0.1.0/src/matview/store/__init__.py +6 -0
  54. matview_cache-0.1.0/src/matview/store/base.py +99 -0
  55. matview_cache-0.1.0/src/matview/store/sqlite.py +1188 -0
  56. matview_cache-0.1.0/src/matview/types.py +341 -0
  57. matview_cache-0.1.0/tests/__init__.py +0 -0
  58. matview_cache-0.1.0/tests/conftest.py +81 -0
  59. matview_cache-0.1.0/tests/golden_codecs.json +56 -0
  60. matview_cache-0.1.0/tests/golden_vectors.json +211 -0
  61. matview_cache-0.1.0/tests/test_cli.py +682 -0
  62. matview_cache-0.1.0/tests/test_codecs.py +322 -0
  63. matview_cache-0.1.0/tests/test_concurrency.py +181 -0
  64. matview_cache-0.1.0/tests/test_examples.py +67 -0
  65. matview_cache-0.1.0/tests/test_fingerprint.py +175 -0
  66. matview_cache-0.1.0/tests/test_foreign.py +407 -0
  67. matview_cache-0.1.0/tests/test_freshness.py +206 -0
  68. matview_cache-0.1.0/tests/test_graph.py +553 -0
  69. matview_cache-0.1.0/tests/test_hard_rules.py +429 -0
  70. matview_cache-0.1.0/tests/test_public_api.py +75 -0
  71. matview_cache-0.1.0/tests/test_registry.py +162 -0
  72. matview_cache-0.1.0/tests/test_scheduler.py +948 -0
  73. matview_cache-0.1.0/tests/test_store.py +432 -0
  74. matview_cache-0.1.0/tests/test_year_codec.py +233 -0
  75. matview_cache-0.1.0/tools/gen_golden_codecs.py +96 -0
  76. matview_cache-0.1.0/tools/gen_golden_vectors.py +124 -0
@@ -0,0 +1,12 @@
1
+ # Action versions only. The Python dependencies are deliberately declared as ranges with
2
+ # floors the codec-floor job actually checks, so a bot raising the floors would be
3
+ # undoing that on purpose rather than by accident.
4
+ version: 2
5
+ updates:
6
+ - package-ecosystem: github-actions
7
+ directory: /
8
+ schedule:
9
+ interval: monthly
10
+ groups:
11
+ actions:
12
+ patterns: ["*"]
@@ -0,0 +1,47 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["**"]
6
+ pull_request:
7
+
8
+ concurrency:
9
+ group: ${{ github.workflow }}-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ jobs:
13
+ quality:
14
+ name: Lint, format & type check
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v7
18
+ - uses: astral-sh/setup-uv@v7
19
+ - run: uv python install 3.11
20
+ - run: make quality
21
+
22
+ test:
23
+ name: Tests (Python ${{ matrix.python-version }})
24
+ runs-on: ubuntu-latest
25
+ strategy:
26
+ fail-fast: false
27
+ matrix:
28
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
29
+ env:
30
+ UV_PYTHON: ${{ matrix.python-version }}
31
+ steps:
32
+ - uses: actions/checkout@v7
33
+ - uses: astral-sh/setup-uv@v7
34
+ - run: uv python install ${{ matrix.python-version }}
35
+ - name: Core only, with no codec extras installed
36
+ run: make test-core
37
+ - name: With every codec extra installed
38
+ run: make test-all
39
+
40
+ codec-determinism-floor:
41
+ name: Golden codec bytes under the oldest supported libraries
42
+ runs-on: ubuntu-latest
43
+ steps:
44
+ - uses: actions/checkout@v7
45
+ - uses: astral-sh/setup-uv@v7
46
+ - run: uv python install 3.11
47
+ - run: make codec-floor
@@ -0,0 +1,43 @@
1
+ name: Docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ concurrency:
12
+ group: docs-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ build:
17
+ name: Build site
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v7
21
+ - uses: astral-sh/setup-uv@v7
22
+ - run: uv python install 3.11
23
+ # --strict, so a broken cross-reference or a missing page fails here rather than
24
+ # shipping as a dead link.
25
+ - run: make docs
26
+ - uses: actions/upload-pages-artifact@v5
27
+ with:
28
+ path: site
29
+
30
+ deploy:
31
+ name: Deploy to Pages
32
+ needs: build
33
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
34
+ runs-on: ubuntu-latest
35
+ permissions:
36
+ pages: write
37
+ id-token: write
38
+ environment:
39
+ name: github-pages
40
+ url: ${{ steps.deployment.outputs.page_url }}
41
+ steps:
42
+ - id: deployment
43
+ uses: actions/deploy-pages@v5
@@ -0,0 +1,110 @@
1
+ # Publishing is tag-driven and credential-free: PyPI trusted publishing exchanges this
2
+ # workflow's OIDC token for an upload token, so there is no long-lived secret in the
3
+ # repository. That binds releases to *this file name* and the environment below, which is
4
+ # why renaming either means re-registering the publisher on PyPI.
5
+ name: Release
6
+
7
+ on:
8
+ push:
9
+ tags: ["v*"]
10
+ # A dry run publishes to TestPyPI instead, so the whole path can be exercised without
11
+ # burning a version number on the real index.
12
+ workflow_dispatch:
13
+ inputs:
14
+ index:
15
+ description: Where to publish
16
+ default: testpypi
17
+ type: choice
18
+ options: [testpypi, pypi]
19
+
20
+ permissions:
21
+ contents: read
22
+
23
+ jobs:
24
+ verify:
25
+ name: Checks, and the tag against __version__
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: astral-sh/setup-uv@v5
30
+ - run: uv python install 3.11
31
+ - run: make check
32
+ # The version has one home, `__version__` in src/matview/__init__.py, and hatchling
33
+ # reads the distribution's version from it. A tag that disagrees would publish a
34
+ # file whose name and metadata contradict each other, so it fails here instead.
35
+ - name: The tag must match the packaged version
36
+ if: startsWith(github.ref, 'refs/tags/v')
37
+ run: |
38
+ set -euo pipefail
39
+ packaged=$(uv run --no-project --with . python -c 'import matview; print(matview.__version__)')
40
+ tag=${GITHUB_REF_NAME#v}
41
+ if [ "$packaged" != "$tag" ]; then
42
+ echo "tag v$tag disagrees with __version__ $packaged" >&2
43
+ exit 1
44
+ fi
45
+
46
+ build:
47
+ name: Build and check the distributions
48
+ needs: verify
49
+ runs-on: ubuntu-latest
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+ - uses: astral-sh/setup-uv@v5
53
+ - run: uv python install 3.11
54
+ # Builds the sdist and wheel, validates the metadata, and installs the wheel in
55
+ # isolation to check that the package it produces actually imports, ships py.typed
56
+ # and runs its console script.
57
+ - run: make release-check
58
+ - uses: actions/upload-artifact@v4
59
+ with:
60
+ name: dist
61
+ path: dist/
62
+
63
+ publish:
64
+ name: Publish to ${{ (github.event_name == 'push' || inputs.index == 'pypi') && 'PyPI' || 'TestPyPI' }}
65
+ needs: build
66
+ runs-on: ubuntu-latest
67
+ environment:
68
+ name: ${{ (github.event_name == 'push' || inputs.index == 'pypi') && 'pypi' || 'testpypi' }}
69
+ permissions:
70
+ id-token: write
71
+ steps:
72
+ - uses: actions/download-artifact@v4
73
+ with:
74
+ name: dist
75
+ path: dist/
76
+ - uses: pypa/gh-action-pypi-publish@release/v1
77
+ with:
78
+ repository-url: ${{ (github.event_name == 'push' || inputs.index == 'pypi') && 'https://upload.pypi.org/legacy/' || 'https://test.pypi.org/legacy/' }}
79
+
80
+ github-release:
81
+ name: GitHub release
82
+ needs: publish
83
+ if: startsWith(github.ref, 'refs/tags/v')
84
+ runs-on: ubuntu-latest
85
+ permissions:
86
+ contents: write
87
+ steps:
88
+ - uses: actions/checkout@v4
89
+ - uses: actions/download-artifact@v4
90
+ with:
91
+ name: dist
92
+ path: dist/
93
+ # The release notes are the CHANGELOG section for this version, not a generated
94
+ # commit list: the changelog is written for readers and the commit list is not.
95
+ - name: Extract this version's changelog section
96
+ run: |
97
+ set -euo pipefail
98
+ version=${GITHUB_REF_NAME#v}
99
+ awk -v v="$version" '
100
+ $0 ~ "^## \\[" v "\\]" {inside = 1; next}
101
+ inside && /^## / {exit}
102
+ inside {print}
103
+ ' CHANGELOG.md > notes.md
104
+ if [ ! -s notes.md ]; then
105
+ echo "CHANGELOG.md has no section for $version" >&2
106
+ exit 1
107
+ fi
108
+ - env:
109
+ GH_TOKEN: ${{ github.token }}
110
+ run: gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --notes-file notes.md dist/*
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ venv/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+ *.db
14
+ *.db-wal
15
+ *.db-shm
16
+ uv.lock
17
+ site/
@@ -0,0 +1,42 @@
1
+ # Run on every commit, so a formatting slip is caught before the push rather than by
2
+ # a red CI run afterwards.
3
+ #
4
+ # uv run --extra dev pre-commit install
5
+ #
6
+ # ruff and mypy are pinned to exactly the versions in the `dev` extra. Unpinned, the
7
+ # hook and the local venv drift apart and the same code passes in one and fails in the
8
+ # other, which is confusing in proportion to how rarely it happens.
9
+ # SPEC.md and docs/design-spec.md are documents reproduced as given, not code. SPEC.md
10
+ # in particular states in its own header that it is verbatim, and it is the contract a
11
+ # second-language implementation is validated against. A whitespace hook quietly
12
+ # editing either would make that claim false and add noise to any diff against the
13
+ # source document, so they are out of scope for the fixers.
14
+ repos:
15
+ - repo: https://github.com/pre-commit/pre-commit-hooks
16
+ rev: v6.0.0
17
+ hooks:
18
+ - id: trailing-whitespace
19
+ exclude: ^(SPEC\.md|docs/design-spec\.md)$
20
+ - id: end-of-file-fixer
21
+ exclude: ^(SPEC\.md|docs/design-spec\.md)$
22
+ - id: check-yaml
23
+ - id: check-toml
24
+ - id: check-merge-conflict
25
+ - id: check-added-large-files
26
+ args: ["--maxkb=512"]
27
+ - id: mixed-line-ending
28
+ args: ["--fix=lf"]
29
+
30
+ - repo: https://github.com/astral-sh/ruff-pre-commit
31
+ rev: v0.16.8
32
+ hooks:
33
+ - id: ruff-check
34
+ args: ["--fix"]
35
+ - id: ruff-format
36
+
37
+ - repo: https://github.com/pre-commit/mirrors-mypy
38
+ rev: v2.3.1
39
+ hooks:
40
+ - id: mypy
41
+ files: ^src/matview/
42
+ additional_dependencies: ["rfc8785>=0.1.4"]
@@ -0,0 +1,56 @@
1
+ # Working on matview
2
+
3
+ Read `docs/design-spec.md` before changing behaviour, `ROADMAP.md` for what is and is
4
+ not planned, and `DECISIONS.md` for what has already been settled where the spec was
5
+ silent. Add an entry to `DECISIONS.md` for every new judgement call; the spec requires it.
6
+
7
+ Each of the ten rules below has a test named after it in `tests/test_hard_rules.py`, so a
8
+ violation names itself in CI output.
9
+
10
+ ## Rules that are not style preferences
11
+
12
+ These are from the spec's "Rules the implementation must not violate". Each one is
13
+ enough on its own to make the library silently wrong rather than loudly broken, so a
14
+ plausible-looking simplification that touches one of them is a bug, not a cleanup.
15
+
16
+ 1. Never delete an artifact on invalidation. Clear `valid`, keep the bytes.
17
+ 2. Never cache a failed compute, in any form.
18
+ 3. Artifact, dep edges and cascade land in one transaction or not at all.
19
+ 4. Never rewrite a row whose digest and fingerprint already match.
20
+ 5. `force` is local; only `force_deps` is transitive.
21
+ 6. `on_miss` resolves to the strictest of caller and recipe, never the caller's alone.
22
+ 7. Digests come from codec output; codecs must be byte-deterministic.
23
+ 8. A dependency's recipe version never enters a dependent's fingerprint.
24
+ 9. Empty or `None` results are values, not misses.
25
+ 10. Never use pickle.
26
+
27
+ Rule 8 in particular reads like a bug to anyone encountering it fresh. It is
28
+ deliberate: it is what buys early cutoff. The comment in the code says so; leave it there.
29
+
30
+ ## Other standing constraints
31
+
32
+ - `SPEC.md` is the cross-language interoperability contract. Changing it invalidates
33
+ every existing cache, so it changes only by bumping `fingerprint_spec_version`. If it
34
+ looks wrong, say so rather than editing it.
35
+ - Anything under the spec's "Not in v1" must not exist, including as a hidden fast path
36
+ or an unused code path.
37
+ - The recipe registry is global and import-time. `Cache` must not be. SQLite connections
38
+ are per-thread with WAL on.
39
+ - Stale is not absent. Three states, kept distinct everywhere: absent, stale, valid.
40
+
41
+ ## Conventions
42
+
43
+ - Python 3.11+, `uv` for environments, `ruff` for lint and format, `mypy --strict`.
44
+ - The package lives under `src/`, so the tests import the *installed* distribution rather
45
+ than the working directory. That is what makes a packaging mistake fail locally.
46
+ - **Run `make check` before pushing.** It is what CI runs: `quality`, `test-core`,
47
+ `test-all`, `codec-floor`. `make help` lists the rest.
48
+ - The commands live in the `Makefile` and CI calls those targets, so changing how
49
+ something is checked is an edit there — not an edit under `.github/workflows/`, which
50
+ needs a token scope this account does not have.
51
+ - `make format-check` is not implied by `make lint`. Running `ruff check --fix` and
52
+ committing on the strength of it has broken CI here before; `make quality` runs both.
53
+ - The test suite must also pass with **no** codec extras installed (`make test-core`);
54
+ the core has to work without them.
55
+ - Docstrings are the API documentation: they are rendered by mkdocstrings into the
56
+ published reference, so they are read by users, not only by readers of the source.
@@ -0,0 +1,159 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
6
+ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ The one thing that is **not** versioned by this file is the fingerprint specification.
9
+ It lives in [`SPEC.md`](SPEC.md) with its own `fingerprint_spec_version`, recorded in
10
+ every database's `meta` table. Changing it invalidates every existing cache, so it
11
+ changes only by bumping that number — never as part of an ordinary release.
12
+
13
+ ## [Unreleased]
14
+
15
+ ## [0.1.0] — 2026-10-13
16
+
17
+ First release.
18
+
19
+ ### Added
20
+
21
+ - Graph queries over the DAG the cache records as it runs, in a new `matview.graph`
22
+ module and three read-only commands. `lineage` walks upward to the raw inputs that
23
+ produced a value — the question the library recorded everything for and could not
24
+ answer. `impact` answers what a changed artifact, a recipe version bump or a `drop`
25
+ would reach. `cost` reports embodied `compute_ms` over an input closure, a per-recipe
26
+ rollup, and the critical path through a closure. Four renderings each: indented tree,
27
+ JSON, Graphviz DOT and Mermaid.
28
+
29
+ None of them needs `--recipes`: they read the recorded graph, so they work against a
30
+ database whose recipes the process has never imported. None of them writes, `impact
31
+ --drop` included.
32
+
33
+ `impact` prints per-recipe counts by default and enumerates only under `--all`. One
34
+ shared root in the 250k benchmark profile reaches 255,000 dependents.
35
+
36
+ `impact --bump` exists mainly to state plainly what rule 8 means: bumping a recipe's
37
+ version marks **nothing** stale, because a dependency's recipe version is not part of a
38
+ dependent's fingerprint. The artifacts become unreachable, each dependent re-resolves
39
+ at the new version on its next read, and the cascade runs only if the recomputed bytes
40
+ differ. That is early cutoff, and it is the part of the fingerprint spec that confuses
41
+ every new reader.
42
+
43
+ - `SqliteStore.inputs_of()`, the transitive upward walk, mirroring `dependents_of()`.
44
+ Both now take a `depth`. Both terminate on a cycle a corrupted database could contain
45
+ rather than hanging. Also `deps_of_many()`, `refs_by_ids()`, `ids_of_recipe()`,
46
+ `counts_by_recipe()` and `recipe_cost_rows()`.
47
+
48
+ - Generic recipe `max_age` freshness, backed by a distinct `refreshed_at` clock so a
49
+ successful refresh with unchanged bytes becomes fresh without rewriting content
50
+ history. Scheduling and domain calendars remain caller concerns. The new artifact
51
+ column advances the unreleased SQLite schema to version 2.
52
+
53
+ - Content-addressed compute cache over a dependency graph discovered by execution: a
54
+ suspending scheduler with constructive traces. Every `ctx.get` is recorded, and the
55
+ artifact, its dep edges and the invalidation cascade land in one transaction.
56
+ - Three artifact states kept distinct — absent, stale, valid. A stale artifact is
57
+ verified by recomputing its fingerprint from its stored edges, and returned without
58
+ recomputing when it still matches. Invalidation never deletes.
59
+ - `SqliteStore`, WAL, connection per thread, writers serialized on `BEGIN IMMEDIATE`,
60
+ with a configurable `max_artifact_bytes` (64 MiB) that raises rather than writing.
61
+ - Policy: an `on_miss` ceiling resolved against each recipe's own declaration, local
62
+ `force` and transitive `force_deps`, a propagated wall-clock `deadline`, and batched
63
+ reporting of every protected miss the walk could reach.
64
+ - `plan()`, which walks without computing and reports absent and stale separately.
65
+ - Codecs `json`, `arrow`, `arrow-series` and `npy`, each byte-deterministic, with
66
+ partitioned output supported through `decode_parts`. The arrow codecs encode through
67
+ pyarrow with pinned IPC options rather than through polars' writer, whose output is
68
+ not stable across polars releases.
69
+ - Foreign recipes: a TOML manifest plus an executable, with inputs declared upfront so
70
+ the graph can be cycle-checked and planned before anything runs. One JSON job file as
71
+ the only argument; results read from a file, never from stdout.
72
+ - `SPEC.md` as the cross-language interoperability contract, with golden vectors for
73
+ hashes and for codec bytes checked into the test suite.
74
+ - Two examples, one financial and one not, both exercised by the test suite.
75
+
76
+ - `arrow-year` and `arrow-year-pandas`, slicing a frame into one artifact per calendar
77
+ year on a column named `date`, with `ArrowYearCodec(name=..., column=...)` for any other
78
+ column. Byte-identical to each other, sorted so that a reordered frame encodes the same,
79
+ reading timezone-aware timestamps in UTC, and refusing null dates. Appending a day
80
+ rewrites one part and leaves the closed years untouched, which the tests assert end to
81
+ end rather than assuming.
82
+ - An `arrow-pandas` codec, byte-identical to `arrow`, decoding to a pandas DataFrame.
83
+ Either codec reads what the other wrote, so a cache is not partitioned by dataframe
84
+ library, and a pandas consumer does not need polars installed.
85
+ - A `matview` command-line tool for inspecting a cache: `info`, `ls`, `show`, `stats`,
86
+ `verify`, `cat` and `plan`. Read-only, `--json` everywhere, stdlib-only so it is present
87
+ on a plain install. Metadata inspection imports nothing; `--recipes` is required only
88
+ for a partial key and for `plan`.
89
+ - `SqliteStore(read_only=True)`, which requires the file to exist, creates no schema and
90
+ leaves the journal mode alone.
91
+ - `inspect_database()`, which describes a database whatever version wrote it.
92
+ - `get_many` and `plan_many`: many roots in one walk, sharing a memo. A single root
93
+ stops at its first blocked `ctx.get`; independent roots do not stop each other, which
94
+ is what makes the spec's claim about cold runs — one report of everything that needs
95
+ ingesting — actually true.
96
+ - `cache.stats.expired`, counting nodes whose `max_age` had elapsed. A reason rather
97
+ than an outcome, so it is excluded from `lookups`: without it, a rebuild driven by a
98
+ clock is indistinguishable from one driven by a changed input.
99
+ - `cache.stats`, and a `matview.scheduler` logger emitting one DEBUG event per node
100
+ resolved. The counters separate a plain hit from a revalidation-without-recompute
101
+ from a genuine rebuild, which a two-way hit/miss count hides.
102
+ - `MissingNode.path`, the resolution chain from the root, so a protected miss says which
103
+ node wanted it rather than only that it is missing.
104
+ - `SqliteStore` refuses a database whose `schema_version` it does not implement, as it
105
+ already did for `fingerprint_spec_version`.
106
+ - `matview --version`, on the top-level parser only. Several subcommands already take a
107
+ `--version` that filters by recipe version, and the two meanings cannot share a
108
+ command line.
109
+ - Python 3.14 is supported and classified. The full suite with every codec extra passes on
110
+ it, and the golden codec fixtures match, so stored artifacts are portable across it.
111
+
112
+ ### Fixed
113
+
114
+ - `SqliteStore.summary()` overcounted `compute_ms`. It summed the column over artifact
115
+ rows, and every part of a partitioned value records the cost of the single compute that
116
+ produced them all — so a three-part value was counted three times. It now groups by
117
+ `(recipe, version, key_hash)` and reports a `values` count beside `artifacts`, so the
118
+ deduplication is visible rather than implicit. Every cost aggregate added in this
119
+ release does the same.
120
+
121
+ - Maintenance operations over large artifact sets no longer exceed SQLite's bound-variable
122
+ limit, and cache verification now uses keyed rather than `OFFSET` pagination. SQLite
123
+ JSON1 (included by default in SQLite 3.38+) is required and checked when opening a store.
124
+
125
+ - The codec-floor job silently verified a stale build of matview: `uv run --no-project
126
+ --with .` reinstalls a cached wheel. It refreshes the build now, and a test asserts the
127
+ installed codec set matches the checked-in fixture, because the symptom was invisible —
128
+ fewer parametrized cases, reported as success.
129
+ - Arrow offset widths are now normalized, so the stored bytes no longer depend on which
130
+ dataframe library produced the table. polars emits `large_string`, pandas 2.2 emits
131
+ `string` and pandas 3.0 emits `large_string`, which made identical data hash differently
132
+ by producer and moved with a pandas *minor* version. Nothing already stored changed:
133
+ polars was already producing the wide variants, so the normalization is a no-op on that
134
+ path and every existing digest holds.
135
+ - Enabling WAL could fail with "database is locked" when two processes opened the same
136
+ new file at the same moment. SQLite does not invoke the busy handler for a
137
+ journal-mode change, so `busy_timeout` did not cover it — the exact deployment the
138
+ design targets. Found by the first cross-process test.
139
+ - `matview.__all__` named `CacheStats` and `BatchResult` without importing them, so
140
+ `from matview import CacheStats` raised. A test now checks every name in every
141
+ module's `__all__` actually resolves.
142
+
143
+ ### Changed
144
+
145
+ - `BaseCodec.partitions` removed: it was read nowhere, and `decode_parts` refusing is the
146
+ actual mechanism.
147
+ - The package now lives under `src/`, so the test suite exercises the installed
148
+ distribution rather than the working directory and a packaging mistake fails locally
149
+ rather than only for a user.
150
+
151
+ ### Notes
152
+
153
+ - Every choice made where the design document was silent is recorded, with its
154
+ reasoning, in [`DECISIONS.md`](DECISIONS.md).
155
+ - `plan_many()` remains roughly two orders of magnitude slower per root than
156
+ `get_many()`, and the batching that was expected to close the gap does not: the loop it
157
+ would batch is over the parts of one node, not over nodes. The real N+1 is across the
158
+ depth-first descent and removing it is a scheduler change. Measured both ways; see
159
+ DECISIONS.md D52 and [`docs/performance.md`](docs/performance.md).
@@ -0,0 +1,28 @@
1
+ cff-version: 1.2.0
2
+ message: If you use matview in published work, please cite it.
3
+ title: "matview: materialized views for derived values"
4
+ abstract: >-
5
+ A content-addressed compute cache over a dependency graph. Recipes declare how a value
6
+ is computed from parameters and from other recipes' outputs; the graph is recorded as
7
+ it runs rather than declared, staleness is decided by content digests rather than
8
+ timestamps, and results are memoized in SQLite for reuse across processes and machines.
9
+ type: software
10
+ authors:
11
+ - family-names: Tresoldi
12
+ given-names: Tiago
13
+ email: tresoldi@gmail.com
14
+ # orcid: https://orcid.org/0000-0000-0000-0000
15
+ license: MIT
16
+ repository-code: https://github.com/tresoldi/matview
17
+ url: https://tresoldi.github.io/matview/
18
+ keywords:
19
+ - cache
20
+ - build system
21
+ - dependency graph
22
+ - memoization
23
+ - reproducibility
24
+ - sqlite
25
+ version: 0.1.0
26
+ date-released: 2026-10-13
27
+ # A Zenodo doi is added here once the archive assigns one at the first tag; see
28
+ # RELEASING.md.