Skip to content

Commit 22e71c6

Browse files
nyurikclaude
andcommitted
feat(mbtiles)!: split the cache schema into flat and normalized layouts
Mirror the flat vs normalized split of the regular schemas: - `CacheSchema::Flat`: a single tile_cache table with the blob inline - simple and fast when few tiles share content. - `CacheSchema::Normalized` (previously the only layout): tile_cache pointing into the xxh3-64-keyed, de-duplicated cache_data blob table - the recommended default for web caches. Both keep the tile_cache name for the coordinate/metadata table (detection tells them apart by the sixth column: tile_data vs tile_id) and both expose the spec-compatible tiles view. - `MbtType::Cache { schema: CacheSchema }`, mirroring Normalized. - CLI values `cache-flat` and `cache-normalized`; `cache` stays as an alias for cache-normalized. - Runtime API takes the schema like insert_tiles takes mbt_type; `MbtilesCache` detects and stores the layout (`open` creates normalized; `open_with_schema` chooses). - Copies preserve expires/etag in any cache-to-cache direction, including across layouts, via a shared canonical select; only normalized needs the blob-insert + collision-check path. - Flat purges skip the orphan-blob GC; per-tile validation of flat caches has nothing to hash-check, like flat. - Cache detection and reads use runtime SQL (the two layouts' tile_cache columns cannot coexist in the sqlx-prepare database), dropping two .sqlx entries. - Tests parametrized over both layouts (unit, pool, copy round-trips, 2x2 cache-to-cache matrix, diff inputs); CLI golden tests cover both; normalized golden outputs are byte-identical to before. BREAKING CHANGE: `MbtType::Cache` gains a payload, `MbtTypeCli::Cache` is replaced by `CacheFlat`/`CacheNormalized`, and the cache read/write APIs take a `CacheSchema` parameter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 2b96a28 commit 22e71c6

31 files changed

Lines changed: 1026 additions & 453 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../mbtiles/sql/init-cache-flat.sql
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../mbtiles/sql/init-cache-normalized.sql

docs/content/files/init-cache.sql

Lines changed: 0 additions & 1 deletion
This file was deleted.

docs/content/mbtiles-schema.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,25 +68,38 @@ In our next semver major, we plan to switch this default and produce `tiles_shal
6868

6969
## cache
7070

71-
The `cache` schema is similar to `normalized`, but stores extra cache metadata (`expires` and `etag`) alongside each tile.
71+
The `cache` schemas store extra cache metadata (`expires` and `etag`) alongside each tile, so a file can serve as a persistent web-tile cache.
72+
Two layouts exist, mirroring the `flat` vs `normalized` split of the regular schemas.
73+
Both center on a `tile_cache` table holding the tile Z,X,Y coordinates and the cache metadata, and both create a spec-compatible `tiles` view so the file can still be read by any standard MBTiles reader (the `expires`/`etag` columns are simply invisible to it).
74+
75+
### cache-flat
76+
77+
The tile blob is stored inline in the `tile_cache` table.
78+
Simple and fast, best when few tiles share the same content.
79+
80+
```sql
81+
--8<-- "files/init-cache-flat.sql"
82+
```
83+
84+
### cache-normalized
85+
7286
Tile blobs are de-duplicated in the `cache_data` table, keyed by an integer `tile_id` that is the [xxh3-64](https://github.com/Cyan4973/xxHash) hash of `tile_data` (stored as an `INTEGER PRIMARY KEY`, i.e. an alias for the rowid, so identical blobs collapse to a single row).
73-
The `tile_cache` table maps tile Z,X,Y coordinates (plus `expires`/`etag`) to a `tile_id`.
74-
A spec-compatible `tiles` view is also created, so the file can still be read by any standard MBTiles reader (the `expires`/`etag` columns are simply invisible to it).
87+
This is the recommended default for web-tile caches, where identical (e.g. empty or ocean) tiles are common; the `cache` CLI value is an alias for it.
7588

7689
```sql
77-
--8<-- "files/init-cache.sql"
90+
--8<-- "files/init-cache-normalized.sql"
7891
```
7992

8093
### Supported operations
8194

82-
The `mbtiles` tool treats `cache` as a first-class schema, with a few deliberate restrictions:
95+
The `mbtiles` tool treats both cache layouts as first-class schemas, with a few deliberate restrictions:
8396

8497
* `summary`, `validate`, `meta-*`, and serving the file with `martin` all work.
8598
* `copy` **from** a cache file to any schema works (reading via the `tiles` view); the per-tile `expires`/`etag` values are dropped, since standard schemas cannot store them.
86-
* `copy` **into** a cache file works from any schema (including `martin-cp --mbtiles-type cache`); the copied entries get `NULL` `expires`/`etag` (never expire). A cache-to-cache copy preserves `expires`/`etag` verbatim.
87-
* `diff`, `apply-patch`, and bin-diff **into or onto** a cache file are rejected: the `NOT NULL` blob table joined through the `tiles` view cannot represent the `NULL` "deleted tile" markers a diff needs. A cache file *can* be the compared-against or patch-source side (it is read through the view).
99+
* `copy` **into** a cache file works from any schema (including `martin-cp --mbtiles-type cache-flat|cache-normalized`); the copied entries get `NULL` `expires`/`etag` (never expire). Copies between cache files - including across the two layouts - preserve `expires`/`etag`.
100+
* `diff`, `apply-patch`, and bin-diff **into or onto** a cache file are rejected: the `NOT NULL` blob storage joined through the `tiles` view cannot represent the `NULL` "deleted tile" markers a diff needs. A cache file *can* be the compared-against or patch-source side (it is read through the view).
88101
* `cache-purge <file> [--max-size <MB>]` removes expired entries (and optionally evicts soonest-expiring entries until the file fits the size budget), then reclaims free pages via `PRAGMA incremental_vacuum`.
89102

90-
Per-tile validation checks foreign-key integrity (every `tile_cache.tile_id` must exist in `cache_data`) and that each blob is stored under its xxh3-64 content key, allowing for the small linear-probing window the runtime API uses on hash collisions. Unreferenced `cache_data` rows are legal - they appear when an entry is overwritten and disappear on the next purge.
103+
For `cache-normalized`, per-tile validation checks foreign-key integrity (every `tile_cache.tile_id` must exist in `cache_data`) and that each blob is stored under its xxh3-64 content key, allowing for the small linear-probing window the runtime API uses on hash collisions. Unreferenced `cache_data` rows are legal - they appear when an entry is overwritten and disappear on the next purge. Like `flat`, the `cache-flat` layout has no hashes to check.
91104

92-
Note that bulk SQL copies into a cache file cannot resolve xxh3-64 collisions the way the runtime `set_cached` API does (by linear probing); the copier instead verifies afterwards that no two different blobs mapped to the same key and fails the copy in that astronomically-unlikely case.
105+
Note that bulk SQL copies into a `cache-normalized` file cannot resolve xxh3-64 collisions the way the runtime `set_cached` API does (by linear probing); the copier instead verifies afterwards that no two different blobs mapped to the same key and fails the copy in that astronomically-unlikely case.

justfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,9 @@ prepare-sqlite: fetch install-sqlx
532532
trap 'rm -f "$db"' EXIT
533533
# add every possible schema to a dummy temp file so that most queries would compile.
534534
# `init-flat` is listed before the view-defining layouts so `tiles` is a table.
535-
for f in init-metadata init-flat init-flat-with-hash init-normalized init-normalized-dedup-id init-cache; do
535+
# `init-cache-flat` is omitted: its `tile_cache` table conflicts with the normalized
536+
# flavor's, and no compile-time query!() macros reference the cache tables.
537+
for f in init-metadata init-flat init-flat-with-hash init-normalized init-normalized-dedup-id init-cache-normalized; do
536538
# it is safer to handle NULLs than to expect it to never be there
537539
sed -E -e 's/[[:space:]]+NOT[[:space:]]+NULL//g' \
538540
-e 's/CREATE (TABLE|VIEW) /CREATE \1 IF NOT EXISTS /' \

martin/src/bin/martin-cp.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,12 @@ async fn init_schema(
650650
hash_view: true,
651651
schema: mbtiles::NormalizedSchema::Hash,
652652
},
653-
MbtTypeCli::Cache => MbtType::Cache,
653+
MbtTypeCli::CacheFlat => MbtType::Cache {
654+
schema: mbtiles::CacheSchema::Flat,
655+
},
656+
MbtTypeCli::CacheNormalized => MbtType::Cache {
657+
schema: mbtiles::CacheSchema::Normalized,
658+
},
654659
};
655660
init_mbtiles_schema(&mut *conn, mbt_type, false)
656661
.await

mbtiles/.sqlx/query-15a5a69f55100eb3cd6c23e896c9cb446e7f02d70cd8a61fde66f118c8dd8a79.json

Lines changed: 0 additions & 46 deletions
This file was deleted.

mbtiles/.sqlx/query-3bbfb4551cbb745ab79195a1a201176be1172dfe72cc60f1bae5958aa07e0cfb.json

Lines changed: 0 additions & 19 deletions
This file was deleted.

mbtiles/sql/init-cache-flat.sql

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
CREATE TABLE tile_cache (
2+
zoom_level INTEGER NOT NULL,
3+
tile_column INTEGER NOT NULL,
4+
tile_row INTEGER NOT NULL,
5+
expires INTEGER,
6+
etag TEXT,
7+
tile_data BLOB NOT NULL,
8+
PRIMARY KEY (zoom_level, tile_column, tile_row)
9+
);
10+
11+
CREATE VIEW tiles AS
12+
SELECT
13+
zoom_level,
14+
tile_column,
15+
tile_row,
16+
tile_data
17+
FROM tile_cache;

0 commit comments

Comments
 (0)