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.
- matview_cache-0.1.0/.github/dependabot.yml +12 -0
- matview_cache-0.1.0/.github/workflows/ci.yml +47 -0
- matview_cache-0.1.0/.github/workflows/docs.yml +43 -0
- matview_cache-0.1.0/.github/workflows/release.yml +110 -0
- matview_cache-0.1.0/.gitignore +17 -0
- matview_cache-0.1.0/.pre-commit-config.yaml +42 -0
- matview_cache-0.1.0/AGENTS.md +56 -0
- matview_cache-0.1.0/CHANGELOG.md +159 -0
- matview_cache-0.1.0/CITATION.cff +28 -0
- matview_cache-0.1.0/DECISIONS.md +859 -0
- matview_cache-0.1.0/LICENSE +21 -0
- matview_cache-0.1.0/Makefile +108 -0
- matview_cache-0.1.0/PKG-INFO +262 -0
- matview_cache-0.1.0/README.md +210 -0
- matview_cache-0.1.0/RELEASING.md +69 -0
- matview_cache-0.1.0/ROADMAP.md +210 -0
- matview_cache-0.1.0/SECURITY.md +51 -0
- matview_cache-0.1.0/SPEC.md +58 -0
- matview_cache-0.1.0/benchmarks/README.md +23 -0
- matview_cache-0.1.0/benchmarks/scale.py +306 -0
- matview_cache-0.1.0/docs/cli.md +140 -0
- matview_cache-0.1.0/docs/design-spec.md +463 -0
- matview_cache-0.1.0/docs/graph.md +185 -0
- matview_cache-0.1.0/docs/guide.md +439 -0
- matview_cache-0.1.0/docs/index.md +65 -0
- matview_cache-0.1.0/docs/performance.md +123 -0
- matview_cache-0.1.0/docs/reference.md +109 -0
- matview_cache-0.1.0/examples/finance/pipeline.py +149 -0
- matview_cache-0.1.0/examples/linguistics/pipeline.py +71 -0
- matview_cache-0.1.0/examples/linguistics/recipes/phylo_tree.py +63 -0
- matview_cache-0.1.0/examples/linguistics/recipes/phylo_tree.toml +20 -0
- matview_cache-0.1.0/mkdocs.yml +59 -0
- matview_cache-0.1.0/pyproject.toml +104 -0
- matview_cache-0.1.0/src/matview/__init__.py +78 -0
- matview_cache-0.1.0/src/matview/cache.py +750 -0
- matview_cache-0.1.0/src/matview/canon.py +116 -0
- matview_cache-0.1.0/src/matview/cli.py +900 -0
- matview_cache-0.1.0/src/matview/codecs/__init__.py +159 -0
- matview_cache-0.1.0/src/matview/codecs/_arrow.py +104 -0
- matview_cache-0.1.0/src/matview/codecs/_arrow_ipc.py +89 -0
- matview_cache-0.1.0/src/matview/codecs/_arrow_pandas.py +76 -0
- matview_cache-0.1.0/src/matview/codecs/_arrow_parts.py +99 -0
- matview_cache-0.1.0/src/matview/codecs/_json.py +36 -0
- matview_cache-0.1.0/src/matview/codecs/_npy.py +53 -0
- matview_cache-0.1.0/src/matview/context.py +87 -0
- matview_cache-0.1.0/src/matview/errors.py +96 -0
- matview_cache-0.1.0/src/matview/fingerprint.py +99 -0
- matview_cache-0.1.0/src/matview/foreign.py +436 -0
- matview_cache-0.1.0/src/matview/graph.py +876 -0
- matview_cache-0.1.0/src/matview/policy.py +67 -0
- matview_cache-0.1.0/src/matview/py.typed +0 -0
- matview_cache-0.1.0/src/matview/registry.py +293 -0
- matview_cache-0.1.0/src/matview/store/__init__.py +6 -0
- matview_cache-0.1.0/src/matview/store/base.py +99 -0
- matview_cache-0.1.0/src/matview/store/sqlite.py +1188 -0
- matview_cache-0.1.0/src/matview/types.py +341 -0
- matview_cache-0.1.0/tests/__init__.py +0 -0
- matview_cache-0.1.0/tests/conftest.py +81 -0
- matview_cache-0.1.0/tests/golden_codecs.json +56 -0
- matview_cache-0.1.0/tests/golden_vectors.json +211 -0
- matview_cache-0.1.0/tests/test_cli.py +682 -0
- matview_cache-0.1.0/tests/test_codecs.py +322 -0
- matview_cache-0.1.0/tests/test_concurrency.py +181 -0
- matview_cache-0.1.0/tests/test_examples.py +67 -0
- matview_cache-0.1.0/tests/test_fingerprint.py +175 -0
- matview_cache-0.1.0/tests/test_foreign.py +407 -0
- matview_cache-0.1.0/tests/test_freshness.py +206 -0
- matview_cache-0.1.0/tests/test_graph.py +553 -0
- matview_cache-0.1.0/tests/test_hard_rules.py +429 -0
- matview_cache-0.1.0/tests/test_public_api.py +75 -0
- matview_cache-0.1.0/tests/test_registry.py +162 -0
- matview_cache-0.1.0/tests/test_scheduler.py +948 -0
- matview_cache-0.1.0/tests/test_store.py +432 -0
- matview_cache-0.1.0/tests/test_year_codec.py +233 -0
- matview_cache-0.1.0/tools/gen_golden_codecs.py +96 -0
- 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,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.
|