python-musefs 0.0.1__tar.gz → 1.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.
- python_musefs-1.1.0/PKG-INFO +27 -0
- python_musefs-1.1.0/README.md +6 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/pyproject.toml +1 -1
- {python_musefs-0.0.1 → python_musefs-1.1.0}/src/musefs_common/__init__.py +13 -1
- {python_musefs-0.0.1 → python_musefs-1.1.0}/src/musefs_common/scan.py +11 -7
- python_musefs-1.1.0/src/musefs_common/schema.py +270 -0
- python_musefs-1.1.0/src/musefs_common/store.py +314 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/src/musefs_common/sync.py +44 -15
- python_musefs-1.1.0/src/python_musefs.egg-info/PKG-INFO +27 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/src/python_musefs.egg-info/SOURCES.txt +2 -0
- python_musefs-1.1.0/tests/test_atomicity.py +208 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/tests/test_constants.py +1 -1
- python_musefs-1.1.0/tests/test_merge_tags.py +114 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/tests/test_public_api.py +6 -1
- python_musefs-1.1.0/tests/test_store_db.py +268 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/tests/test_sync.py +110 -3
- python_musefs-0.0.1/PKG-INFO +0 -73
- python_musefs-0.0.1/README.md +0 -52
- python_musefs-0.0.1/src/musefs_common/schema.py +0 -243
- python_musefs-0.0.1/src/musefs_common/store.py +0 -119
- python_musefs-0.0.1/src/python_musefs.egg-info/PKG-INFO +0 -73
- python_musefs-0.0.1/tests/test_store_db.py +0 -76
- {python_musefs-0.0.1 → python_musefs-1.1.0}/LICENSE +0 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/setup.cfg +0 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/src/musefs_common/constants.py +0 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/src/musefs_common/contract.py +0 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/src/musefs_common/errors.py +0 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/src/musefs_common/paths.py +0 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/src/python_musefs.egg-info/dependency_links.txt +0 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/src/python_musefs.egg-info/requires.txt +0 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/src/python_musefs.egg-info/top_level.txt +0 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/tests/test_contract.py +0 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/tests/test_errors.py +0 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/tests/test_paths.py +0 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/tests/test_scan.py +0 -0
- {python_musefs-0.0.1 → python_musefs-1.1.0}/tests/test_store_art.py +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-musefs
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Shared musefs SQLite-store contract for the beets and Picard plugins
|
|
5
|
+
Author: Conor Futro
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Sohex/musefs
|
|
8
|
+
Project-URL: Repository, https://github.com/Sohex/musefs
|
|
9
|
+
Project-URL: Issues, https://github.com/Sohex/musefs/issues
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: POSIX
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# python-musefs
|
|
23
|
+
|
|
24
|
+
The full guide now lives in the musefs documentation site:
|
|
25
|
+
|
|
26
|
+
- **Published:** <https://sohex.github.io/musefs/integrations/python-musefs.html>
|
|
27
|
+
- **In-repo source:** [`docs/src/integrations/python-musefs.md`](../../docs/src/integrations/python-musefs.md)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# python-musefs
|
|
2
|
+
|
|
3
|
+
The full guide now lives in the musefs documentation site:
|
|
4
|
+
|
|
5
|
+
- **Published:** <https://sohex.github.io/musefs/integrations/python-musefs.html>
|
|
6
|
+
- **In-repo source:** [`docs/src/integrations/python-musefs.md`](../../docs/src/integrations/python-musefs.md)
|
|
@@ -11,18 +11,24 @@ from .errors import ScanError, SchemaMismatch
|
|
|
11
11
|
from .paths import realpath_key
|
|
12
12
|
from .scan import run_scan
|
|
13
13
|
from .store import (
|
|
14
|
+
TagRow,
|
|
14
15
|
check_schema_version,
|
|
15
16
|
connect,
|
|
17
|
+
delete_tracks,
|
|
18
|
+
merge_tags,
|
|
16
19
|
prune_missing,
|
|
17
20
|
replace_tags,
|
|
18
21
|
replace_track_art,
|
|
19
22
|
sniff_mime,
|
|
23
|
+
tags_for_track,
|
|
20
24
|
track_id_for_path,
|
|
25
|
+
track_ids_by_tag,
|
|
26
|
+
track_ids_for_paths,
|
|
21
27
|
upsert_art,
|
|
22
28
|
)
|
|
23
29
|
from .sync import ArtImage, Record, SyncStats, sync_files, sync_one
|
|
24
30
|
|
|
25
|
-
__version__ = "
|
|
31
|
+
__version__ = "1.1.0"
|
|
26
32
|
|
|
27
33
|
__all__ = [
|
|
28
34
|
"EXPECTED_USER_VERSION",
|
|
@@ -35,7 +41,13 @@ __all__ = [
|
|
|
35
41
|
"connect",
|
|
36
42
|
"check_schema_version",
|
|
37
43
|
"track_id_for_path",
|
|
44
|
+
"track_ids_for_paths",
|
|
45
|
+
"track_ids_by_tag",
|
|
46
|
+
"tags_for_track",
|
|
47
|
+
"TagRow",
|
|
48
|
+
"delete_tracks",
|
|
38
49
|
"prune_missing",
|
|
50
|
+
"merge_tags",
|
|
39
51
|
"replace_tags",
|
|
40
52
|
"upsert_art",
|
|
41
53
|
"replace_track_art",
|
|
@@ -4,19 +4,23 @@ import subprocess
|
|
|
4
4
|
from .errors import ScanError
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def run_scan(binary, db_path, target, *, timeout=None):
|
|
8
|
-
"""Run ``<binary> scan <target...> --db <db_path
|
|
9
|
-
path or an iterable of paths; all targets precede the ``--db``
|
|
10
|
-
scanned under one process (one DB open). Creates the DB if
|
|
11
|
-
the structural columns a plugin can't compute.
|
|
12
|
-
``
|
|
13
|
-
|
|
7
|
+
def run_scan(binary, db_path, target, *, revalidate=False, timeout=None):
|
|
8
|
+
"""Run ``<binary> scan <target...> --db <db_path> [--revalidate]``. ``target``
|
|
9
|
+
is a single path or an iterable of paths; all targets precede the ``--db``
|
|
10
|
+
flag and are scanned under one process (one DB open). Creates the DB if
|
|
11
|
+
absent and fills the structural columns a plugin can't compute. With
|
|
12
|
+
``revalidate``, the scanner re-checks stamps, prunes rows whose backing file
|
|
13
|
+
is gone, and GCs orphaned art. Raises ``ScanError`` (with ``kind`` in
|
|
14
|
+
``"not_found" | "timeout" | "failed"``) on failure; the caller formats its
|
|
15
|
+
own user-facing message from the exception attributes."""
|
|
14
16
|
if isinstance(target, (str, os.PathLike)):
|
|
15
17
|
targets = [target]
|
|
16
18
|
else:
|
|
17
19
|
targets = list(target)
|
|
18
20
|
display = str(targets[0]) if len(targets) == 1 else f"{len(targets)} target(s)"
|
|
19
21
|
argv = [binary, "scan", *(str(t) for t in targets), "--db", str(db_path)]
|
|
22
|
+
if revalidate:
|
|
23
|
+
argv.append("--revalidate")
|
|
20
24
|
try:
|
|
21
25
|
result = subprocess.run(argv, capture_output=True, timeout=timeout)
|
|
22
26
|
except FileNotFoundError as exc:
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# GENERATED from musefs-db/src/schema.rs — do not edit.
|
|
2
|
+
# Regenerate: MUSEFS_REGEN_SCHEMA_PY=1 cargo test -p musefs-db schema_py
|
|
3
|
+
# Re-vendor: python contrib/python-musefs/vendor_to_picard.py
|
|
4
|
+
|
|
5
|
+
SCHEMA_SQL = """\
|
|
6
|
+
-- ── MIGRATION_V1 ──
|
|
7
|
+
CREATE TABLE tracks (
|
|
8
|
+
id INTEGER PRIMARY KEY,
|
|
9
|
+
backing_path TEXT NOT NULL UNIQUE,
|
|
10
|
+
format TEXT NOT NULL,
|
|
11
|
+
audio_offset INTEGER NOT NULL,
|
|
12
|
+
audio_length INTEGER NOT NULL,
|
|
13
|
+
backing_size INTEGER NOT NULL,
|
|
14
|
+
backing_mtime_ns INTEGER NOT NULL,
|
|
15
|
+
content_version INTEGER NOT NULL DEFAULT 0,
|
|
16
|
+
updated_at INTEGER NOT NULL,
|
|
17
|
+
backing_ctime_ns INTEGER NOT NULL DEFAULT 0 CHECK (backing_ctime_ns >= 0),
|
|
18
|
+
CHECK (format IN ('flac','mp3','m4a','opus','vorbis','oggflac','wav')),
|
|
19
|
+
CHECK (audio_offset >= 0),
|
|
20
|
+
CHECK (audio_length >= 0),
|
|
21
|
+
CHECK (backing_size >= 0),
|
|
22
|
+
CHECK (backing_mtime_ns >= 0),
|
|
23
|
+
CHECK (content_version >= 0),
|
|
24
|
+
CHECK (updated_at >= 0),
|
|
25
|
+
CHECK (audio_offset + audio_length <= backing_size)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE tags (
|
|
29
|
+
track_id INTEGER NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
|
|
30
|
+
key TEXT NOT NULL,
|
|
31
|
+
value TEXT NOT NULL,
|
|
32
|
+
ordinal INTEGER NOT NULL DEFAULT 0,
|
|
33
|
+
value_blob BLOB,
|
|
34
|
+
PRIMARY KEY (track_id, key, ordinal),
|
|
35
|
+
CHECK (ordinal >= 0),
|
|
36
|
+
CHECK (value_blob IS NULL OR value = ''),
|
|
37
|
+
CHECK (length(key) <= 256),
|
|
38
|
+
CHECK (length(key) >= 1
|
|
39
|
+
AND key NOT GLOB '*[' || char(1) || '-' || char(31) || ']*'),
|
|
40
|
+
CHECK (length(value) <= 262144),
|
|
41
|
+
CHECK (value_blob IS NULL OR length(value_blob) <= 16711680)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE TABLE art (
|
|
45
|
+
id INTEGER PRIMARY KEY,
|
|
46
|
+
sha256 TEXT NOT NULL UNIQUE,
|
|
47
|
+
mime TEXT NOT NULL,
|
|
48
|
+
width INTEGER,
|
|
49
|
+
height INTEGER,
|
|
50
|
+
byte_len INTEGER NOT NULL,
|
|
51
|
+
data BLOB NOT NULL,
|
|
52
|
+
CHECK (byte_len = length(data)),
|
|
53
|
+
CHECK (length(sha256) = 64),
|
|
54
|
+
CHECK (width IS NULL OR width >= 0),
|
|
55
|
+
CHECK (height IS NULL OR height >= 0),
|
|
56
|
+
CHECK (length(mime) <= 255),
|
|
57
|
+
CHECK (byte_len <= 16711680)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE TABLE track_art (
|
|
61
|
+
track_id INTEGER NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
|
|
62
|
+
art_id INTEGER NOT NULL REFERENCES art(id),
|
|
63
|
+
picture_type INTEGER NOT NULL DEFAULT 3,
|
|
64
|
+
description TEXT NOT NULL DEFAULT '',
|
|
65
|
+
ordinal INTEGER NOT NULL DEFAULT 0,
|
|
66
|
+
PRIMARY KEY (track_id, ordinal),
|
|
67
|
+
CHECK (picture_type BETWEEN 0 AND 20),
|
|
68
|
+
CHECK (ordinal >= 0),
|
|
69
|
+
CHECK (length(description) <= 1024)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
-- Read-only, derived-from-file structural metadata (FLAC STREAMINFO/SEEKTABLE).
|
|
73
|
+
-- NOT part of the editable `tags` contract: external tools never touch it.
|
|
74
|
+
CREATE TABLE structural_blocks (
|
|
75
|
+
track_id INTEGER NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
|
|
76
|
+
kind TEXT NOT NULL,
|
|
77
|
+
ordinal INTEGER NOT NULL DEFAULT 0,
|
|
78
|
+
body BLOB NOT NULL,
|
|
79
|
+
PRIMARY KEY (track_id, kind, ordinal),
|
|
80
|
+
CHECK (kind IN ('STREAMINFO','SEEKTABLE')),
|
|
81
|
+
CHECK (ordinal >= 0),
|
|
82
|
+
CHECK (length(body) <= 16777215)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
-- Bounded changelog ring for O(changed) refresh. Every metadata edit funnels
|
|
86
|
+
-- through an UPDATE on the tracks row (the tags/track_art triggers), so
|
|
87
|
+
-- triggers on tracks alone capture all writers. Relies on SQLite nested
|
|
88
|
+
-- trigger activation (on by default; distinct from PRAGMA recursive_triggers).
|
|
89
|
+
CREATE TABLE track_changes (
|
|
90
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
91
|
+
track_id INTEGER NOT NULL
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
-- Index the reverse art -> track_art edge so bulk orphan-GC and the art delete
|
|
95
|
+
-- trigger below do not scan the whole join table per deleted row.
|
|
96
|
+
CREATE INDEX track_art_art_id_idx ON track_art(art_id);
|
|
97
|
+
|
|
98
|
+
CREATE TRIGGER tags_ai AFTER INSERT ON tags BEGIN
|
|
99
|
+
UPDATE tracks SET content_version = content_version + 1,
|
|
100
|
+
updated_at = CAST(strftime('%s','now') AS INTEGER)
|
|
101
|
+
WHERE id = NEW.track_id;
|
|
102
|
+
END;
|
|
103
|
+
CREATE TRIGGER tags_au AFTER UPDATE ON tags BEGIN
|
|
104
|
+
UPDATE tracks SET content_version = content_version + 1,
|
|
105
|
+
updated_at = CAST(strftime('%s','now') AS INTEGER)
|
|
106
|
+
WHERE id = NEW.track_id;
|
|
107
|
+
END;
|
|
108
|
+
CREATE TRIGGER tags_ad AFTER DELETE ON tags BEGIN
|
|
109
|
+
UPDATE tracks SET content_version = content_version + 1,
|
|
110
|
+
updated_at = CAST(strftime('%s','now') AS INTEGER)
|
|
111
|
+
WHERE id = OLD.track_id;
|
|
112
|
+
END;
|
|
113
|
+
|
|
114
|
+
CREATE TRIGGER track_art_ai AFTER INSERT ON track_art BEGIN
|
|
115
|
+
UPDATE tracks SET content_version = content_version + 1,
|
|
116
|
+
updated_at = CAST(strftime('%s','now') AS INTEGER)
|
|
117
|
+
WHERE id = NEW.track_id;
|
|
118
|
+
END;
|
|
119
|
+
CREATE TRIGGER track_art_au AFTER UPDATE ON track_art BEGIN
|
|
120
|
+
UPDATE tracks SET content_version = content_version + 1,
|
|
121
|
+
updated_at = CAST(strftime('%s','now') AS INTEGER)
|
|
122
|
+
WHERE id = NEW.track_id;
|
|
123
|
+
END;
|
|
124
|
+
CREATE TRIGGER track_art_ad AFTER DELETE ON track_art BEGIN
|
|
125
|
+
UPDATE tracks SET content_version = content_version + 1,
|
|
126
|
+
updated_at = CAST(strftime('%s','now') AS INTEGER)
|
|
127
|
+
WHERE id = OLD.track_id;
|
|
128
|
+
END;
|
|
129
|
+
|
|
130
|
+
CREATE TRIGGER tracks_changelog_ai AFTER INSERT ON tracks BEGIN
|
|
131
|
+
INSERT INTO track_changes (track_id) VALUES (NEW.id);
|
|
132
|
+
END;
|
|
133
|
+
CREATE TRIGGER tracks_changelog_au AFTER UPDATE ON tracks BEGIN
|
|
134
|
+
INSERT INTO track_changes (track_id) VALUES (NEW.id);
|
|
135
|
+
END;
|
|
136
|
+
CREATE TRIGGER tracks_changelog_ad AFTER DELETE ON tracks BEGIN
|
|
137
|
+
INSERT INTO track_changes (track_id) VALUES (OLD.id);
|
|
138
|
+
END;
|
|
139
|
+
|
|
140
|
+
-- Self-pruning ring: writers maintain it; the mount's read-only connections
|
|
141
|
+
-- never need to. Deletes only from the old end, so retained seqs stay contiguous.
|
|
142
|
+
CREATE TRIGGER track_changes_prune AFTER INSERT ON track_changes BEGIN
|
|
143
|
+
DELETE FROM track_changes WHERE seq <= NEW.seq - 8192;
|
|
144
|
+
END;
|
|
145
|
+
|
|
146
|
+
-- art rows are content-addressed by sha256: once written, their content
|
|
147
|
+
-- columns are immutable. A writer needing different bytes/metadata inserts a
|
|
148
|
+
-- NEW row and relinks via track_art (which bumps content_version through the
|
|
149
|
+
-- track_art triggers). width/height use IS NOT (NULL-safe) because they are
|
|
150
|
+
-- nullable; the NOT NULL columns use <>.
|
|
151
|
+
CREATE TRIGGER art_reject_content_update
|
|
152
|
+
BEFORE UPDATE ON art
|
|
153
|
+
WHEN NEW.data <> OLD.data
|
|
154
|
+
OR NEW.sha256 <> OLD.sha256
|
|
155
|
+
OR NEW.mime <> OLD.mime
|
|
156
|
+
OR NEW.byte_len <> OLD.byte_len
|
|
157
|
+
OR NEW.width IS NOT OLD.width
|
|
158
|
+
OR NEW.height IS NOT OLD.height
|
|
159
|
+
BEGIN
|
|
160
|
+
SELECT RAISE(ABORT,
|
|
161
|
+
'art rows are immutable; insert a new content-addressed row and relink via track_art');
|
|
162
|
+
END;
|
|
163
|
+
|
|
164
|
+
-- Deleting an art row that still has track_art references (an orphan an
|
|
165
|
+
-- external writer can produce with foreign_keys OFF) bumps every referencing
|
|
166
|
+
-- track, so the mount rebuilds and serves a clean EIO on the orphan rather
|
|
167
|
+
-- than streaming stale bytes from an old cached layout. Inert on the normal
|
|
168
|
+
-- gc_orphan_art path, where the deleted row has no references.
|
|
169
|
+
CREATE TRIGGER art_ad AFTER DELETE ON art BEGIN
|
|
170
|
+
UPDATE tracks SET content_version = content_version + 1,
|
|
171
|
+
updated_at = CAST(strftime('%s','now') AS INTEGER)
|
|
172
|
+
WHERE id IN (SELECT track_id FROM track_art WHERE art_id = OLD.id);
|
|
173
|
+
END;
|
|
174
|
+
|
|
175
|
+
-- Scanner-owned geometry feeds the synthesized layout, but upsert_track does
|
|
176
|
+
-- not touch content_version. Bump it whenever a geometry column actually
|
|
177
|
+
-- changes, making content_version a true superset of served-byte inputs. The
|
|
178
|
+
-- WHEN guard is false on this trigger's own nested UPDATE (only content_version
|
|
179
|
+
-- changes), so the recursion terminates after exactly one bump.
|
|
180
|
+
CREATE TRIGGER tracks_geometry_au
|
|
181
|
+
AFTER UPDATE ON tracks
|
|
182
|
+
WHEN NEW.format <> OLD.format
|
|
183
|
+
OR NEW.audio_offset <> OLD.audio_offset
|
|
184
|
+
OR NEW.audio_length <> OLD.audio_length
|
|
185
|
+
OR NEW.backing_size <> OLD.backing_size
|
|
186
|
+
OR NEW.backing_mtime_ns <> OLD.backing_mtime_ns
|
|
187
|
+
BEGIN
|
|
188
|
+
UPDATE tracks SET content_version = content_version + 1 WHERE id = NEW.id;
|
|
189
|
+
END;
|
|
190
|
+
|
|
191
|
+
-- FLAC structural blocks feed synthesized headers and flip the synthesis path
|
|
192
|
+
-- (legacy front-read fallback vs streamed fast path), so a change must bump.
|
|
193
|
+
-- set_structural_blocks is DELETE-then-INSERT (no UPDATE path exists), so these
|
|
194
|
+
-- fire on every rewrite; the resulting over-bump on a byte-identical re-probe
|
|
195
|
+
-- is harmless monotone churn (content_version is compared only for equality).
|
|
196
|
+
CREATE TRIGGER structural_blocks_ai AFTER INSERT ON structural_blocks BEGIN
|
|
197
|
+
UPDATE tracks SET content_version = content_version + 1 WHERE id = NEW.track_id;
|
|
198
|
+
END;
|
|
199
|
+
CREATE TRIGGER structural_blocks_ad AFTER DELETE ON structural_blocks BEGIN
|
|
200
|
+
UPDATE tracks SET content_version = content_version + 1 WHERE id = OLD.track_id;
|
|
201
|
+
END;
|
|
202
|
+
PRAGMA user_version = 1;
|
|
203
|
+
|
|
204
|
+
-- ── MIGRATION_V2 ──
|
|
205
|
+
-- fingerprint/content_hash are scanner-owned content identities. Neither is
|
|
206
|
+
-- UNIQUE and the index is NON-unique BY DESIGN: duplicate-content tracks (same
|
|
207
|
+
-- album in two places, genuine dupes) legitimately share both values, and a
|
|
208
|
+
-- UNIQUE constraint would abort the scan batch on the second copy. Correctness
|
|
209
|
+
-- comes from the refind logic (unique-missing candidate + confirmation), not
|
|
210
|
+
-- from DB uniqueness. Both columns carry a length(x) = 64 CHECK locking them
|
|
211
|
+
-- to SHA-256 hex (Task E2 benchmark locked the hash to SHA-256: under a
|
|
212
|
+
-- realistic SSD/HDD I/O profile the fingerprint adds ~8.6%; the RAM
|
|
213
|
+
-- microbench's higher ratio is an I/O-elimination artifact — see
|
|
214
|
+
-- the benchmarks docs). Hash function is now fixed, so the CHECK is added here.
|
|
215
|
+
ALTER TABLE tracks ADD COLUMN fingerprint TEXT
|
|
216
|
+
CHECK (fingerprint IS NULL OR length(fingerprint) = 64);
|
|
217
|
+
ALTER TABLE tracks ADD COLUMN content_hash TEXT
|
|
218
|
+
CHECK (content_hash IS NULL OR length(content_hash) = 64);
|
|
219
|
+
CREATE INDEX tracks_fingerprint_idx ON tracks(fingerprint);
|
|
220
|
+
|
|
221
|
+
-- Rebuild `tags` with a byte-accurate value cap (#505). SQLite's length() on
|
|
222
|
+
-- TEXT counts characters, so the V1 `CHECK (length(value) <= 262144)` was up to
|
|
223
|
+
-- ~4x looser than the documented 256 KiB byte bound; length(CAST(value AS BLOB))
|
|
224
|
+
-- counts bytes. SQLite cannot alter a CHECK in place, so recreate the table
|
|
225
|
+
-- (V2 is unreleased — this is folded in rather than added as a new migration).
|
|
226
|
+
-- Pre-existing over-cap rows (only reachable on an upgraded store) are dropped:
|
|
227
|
+
-- the read-time guard already counts bytes, so they were unreadable anyway, and
|
|
228
|
+
-- carrying them would abort the rebuild on the new CHECK.
|
|
229
|
+
CREATE TABLE tags_new (
|
|
230
|
+
track_id INTEGER NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
|
|
231
|
+
key TEXT NOT NULL,
|
|
232
|
+
value TEXT NOT NULL,
|
|
233
|
+
ordinal INTEGER NOT NULL DEFAULT 0,
|
|
234
|
+
value_blob BLOB,
|
|
235
|
+
PRIMARY KEY (track_id, key, ordinal),
|
|
236
|
+
CHECK (ordinal >= 0),
|
|
237
|
+
CHECK (value_blob IS NULL OR value = ''),
|
|
238
|
+
CHECK (length(key) <= 256),
|
|
239
|
+
CHECK (length(key) >= 1
|
|
240
|
+
AND key NOT GLOB '*[' || char(1) || '-' || char(31) || ']*'),
|
|
241
|
+
CHECK (length(CAST(value AS BLOB)) <= 262144),
|
|
242
|
+
CHECK (value_blob IS NULL OR length(value_blob) <= 16711680)
|
|
243
|
+
);
|
|
244
|
+
INSERT INTO tags_new (track_id, key, value, ordinal, value_blob)
|
|
245
|
+
SELECT track_id, key, value, ordinal, value_blob FROM tags
|
|
246
|
+
WHERE length(CAST(value AS BLOB)) <= 262144;
|
|
247
|
+
DROP TABLE tags;
|
|
248
|
+
ALTER TABLE tags_new RENAME TO tags;
|
|
249
|
+
|
|
250
|
+
-- DROP TABLE tags dropped its INSERT/UPDATE/DELETE triggers; recreate them
|
|
251
|
+
-- verbatim so the content_version/updated_at bump contract is unchanged.
|
|
252
|
+
CREATE TRIGGER tags_ai AFTER INSERT ON tags BEGIN
|
|
253
|
+
UPDATE tracks SET content_version = content_version + 1,
|
|
254
|
+
updated_at = CAST(strftime('%s','now') AS INTEGER)
|
|
255
|
+
WHERE id = NEW.track_id;
|
|
256
|
+
END;
|
|
257
|
+
CREATE TRIGGER tags_au AFTER UPDATE ON tags BEGIN
|
|
258
|
+
UPDATE tracks SET content_version = content_version + 1,
|
|
259
|
+
updated_at = CAST(strftime('%s','now') AS INTEGER)
|
|
260
|
+
WHERE id = NEW.track_id;
|
|
261
|
+
END;
|
|
262
|
+
CREATE TRIGGER tags_ad AFTER DELETE ON tags BEGIN
|
|
263
|
+
UPDATE tracks SET content_version = content_version + 1,
|
|
264
|
+
updated_at = CAST(strftime('%s','now') AS INTEGER)
|
|
265
|
+
WHERE id = OLD.track_id;
|
|
266
|
+
END;
|
|
267
|
+
PRAGMA user_version = 2;
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
USER_VERSION = 2
|