zerobucket 0.7.0 → 0.8.0

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.
package/README.md CHANGED
@@ -10,8 +10,7 @@ This is the Node.js/TypeScript sibling of the [Python package](https://pypi.org/
10
10
  **It uses the exact same table**, so a Node service and a Python service can share one database
11
11
  and read each other's images.
12
12
 
13
- > **Status: early (0.7.x).** PostgreSQL **and SQLite**, each in classic and dedup modes, plus **MySQL/MariaDB** (classic
14
- > mode; dedup and the CLI are not available for MySQL yet, see [What's not here yet](#whats-not-here-yet)), with
13
+ > **Status: early (0.8.x).** PostgreSQL, SQLite **and MySQL/MariaDB**, each in classic and dedup modes, with
15
14
  > S3-compatible object-storage tiering, HEIC (iPhone photo) support and a `zerobucket` command line tool.
16
15
 
17
16
  ## Install
@@ -220,10 +219,13 @@ const id2 = await images.put(iphonePhoto, {
220
219
 
221
220
  The package installs a `zerobucket` command (`npx zerobucket ...`, or install globally with `npm i -g zerobucket`).
222
221
  It has the same commands, flags, messages and exit codes as the Python package's CLI, and additionally works with
223
- **SQLite** and **dedup mode**, and understands images that were tiered to object storage. It does not know MySQL/MariaDB yet.
222
+ **SQLite**, **MySQL/MariaDB** (a `mysql://` or `mariadb://` `--database-url`, or `ZEROBUCKET_DATABASE_URL`) and **dedup
223
+ mode**, and understands images that were tiered to object storage. On MySQL the on-disk size is read from
224
+ `information_schema` and is approximate (MySQL 8 caches those statistics for up to 24 hours).
224
225
 
225
226
  ```bash
226
227
  export ZEROBUCKET_DATABASE_URL=postgresql://user:pass@localhost/mydb # or: --database-url URL
228
+ # MySQL/MariaDB: export ZEROBUCKET_DATABASE_URL=mysql://user:pass@localhost:3306/mydb # mariadb:// works too
227
229
  # SQLite instead: export ZEROBUCKET_SQLITE_PATH=./images.db # or: --sqlite PATH
228
230
 
229
231
  zerobucket init # create the schema (add --dedup for the dedup tables)
@@ -308,12 +310,16 @@ const images = new ZeroBucket({
308
310
 
309
311
  Same API as everywhere else: `put`, `get`, `getMany`, `putMany`, `delete`, `deleteMany`, `exists`, `metadata`, streaming
310
312
  and ranges, access-control hooks, the optimize pipeline, custom validators, HEIC, `onOperation` metrics, retry with
311
- backoff, pooling (`poolMaxSize`, `poolTimeoutMs`), **object-storage tiering** and `connection` (your own transaction).
312
- It uses the Python package's table, byte for byte (`SHOW CREATE TABLE` is identical), so a Node app and a Python app can
313
- share one MySQL/MariaDB database (the conformance harness verifies both directions, including tiered rows).
313
+ backoff, pooling (`poolMaxSize`, `poolTimeoutMs`), **dedup mode** (`dedup: true`), **object-storage tiering** and
314
+ `connection` (your own transaction). The [`zerobucket` CLI](#command-line) works on it too: pass a `mysql://` or
315
+ `mariadb://` `--database-url`.
314
316
 
315
- **Not available on MySQL yet:** dedup mode (`dedup: true` throws, rather than being ignored) and the `zerobucket` command
316
- line tool (it knows PostgreSQL and SQLite). The Python package has both for MySQL.
317
+ It uses the Python package's tables, byte for byte (`SHOW CREATE TABLE` is identical for the classic table and for both
318
+ dedup tables), so a Node app and a Python app can share one MySQL/MariaDB database. The conformance harness verifies both
319
+ directions in both modes, including tiered rows and shared dedup blobs.
320
+
321
+ As everywhere, dedup and tiering cannot be combined (`dedup: true` with `objectStorage` throws). Copying an existing
322
+ classic table into the dedup tables (`migrate --to-dedup`) is PostgreSQL-only, as in the Python package.
317
323
 
318
324
  What is different from Postgres, and why (each verified against a real MariaDB 10.11):
319
325
 
@@ -331,6 +337,18 @@ What is different from Postgres, and why (each verified against a real MariaDB 1
331
337
  - **Migration takes a server-side lock** (`GET_LOCK`), so many processes starting at once do not race on the DDL, and it
332
338
  issues **no DDL at all** when the schema is current. (Even a no-op `ALTER TABLE` needs an exclusive metadata lock that
333
339
  waits behind every open transaction, and everything else on that table then queues behind _it_.)
340
+ - **Dedup** stores each distinct image once (`zerobucket_blobs`, with a `ref_count`) and one row per image
341
+ (`zerobucket_image_refs`). Without `RETURNING`, a delete decrements with `UPDATE ... ref_count = ref_count - n` (which
342
+ takes the row lock) and then removes the blob once the count reaches zero; the foreign key makes deleting a blob that is
343
+ still referenced impossible. A batch groups identical images, touches each distinct blob **once, in sorted checksum
344
+ order**, and decrements in the same order, so overlapping batches cannot deadlock.
345
+ - **Batch deletes use one `SELECT ... FOR UPDATE` and then point deletes, never `DELETE ... WHERE id IN (...)`.** On
346
+ MariaDB 10.11, with a small table, the `IN` delete is planned as a full scan (`EXPLAIN` says `type=ALL`), and a scan
347
+ under REPEATABLE READ locks every row it passes, including rows another batch already holds. Two overlapping
348
+ `deleteMany` batches deadlocked in 126 of 300 transactions with an `IN` delete and in 0 with point deletes (also 0 with
349
+ a concurrent writer on the same blobs); `READ COMMITTED` and `FORCE INDEX` alone did not fix it. MySQL 8.0.46 plans the
350
+ same statement as a range over just the listed ids and did **not** deadlock in the same test. Point deletes do not
351
+ depend on the plan, so they are used on both.
334
352
  - **Large images and `max_allowed_packet`.** An image bigger than the server's limit fails with an error that says so and
335
353
  how to raise it (`SET GLOBAL max_allowed_packet = ...`). Images are sent as raw bytes, so the limit applies to the image
336
354
  size, not twice that.
@@ -343,8 +361,9 @@ What is different from Postgres, and why (each verified against a real MariaDB 1
343
361
  skipped on your connection (it cannot know whether you will commit).
344
362
  - **Streaming** uses ranged `SUBSTRING()` reads, so a delete mid-stream raises `StorageError` (like Postgres). Tiered images
345
363
  stream with real S3 `Range` requests.
346
- - **Tested on MariaDB 10.11.** Real MySQL 8.x is expected to behave the same (the SQL avoids MariaDB-only features) but has
347
- not been run here. Use MariaDB 10.2.1+ or MySQL 8.0.16+: older servers accept the `CHECK` constraint syntax but ignore it.
364
+ - **Tested on MariaDB 10.11 and MySQL 8.0.46** (the MySQL, dedup and CLI test suites, the Node/Python conformance harness
365
+ and the schema comparison all pass on both). Other versions are expected to behave the same but have not been run. Use
366
+ MariaDB 10.2.1+ or MySQL 8.0.16+: older servers accept the `CHECK` constraint syntax but ignore it.
348
367
 
349
368
  ## Deduplication
350
369
 
@@ -443,8 +462,8 @@ same corpus of valid and corrupt files identically.
443
462
 
444
463
  ## What's not here yet
445
464
 
446
- Planned for MySQL/MariaDB: **dedup mode**, the **`zerobucket` CLI**, and a MySQL-specific operations guide.
447
- Tiering and dedup cannot be combined (same rule as the Python package).
465
+ Planned for MySQL/MariaDB: a MySQL-specific operations guide, and `migrate --to-dedup` (PostgreSQL-only today, as in the
466
+ Python package). Tiering and dedup cannot be combined (same rule as the Python package).
448
467
 
449
468
  ## Runtime notes
450
469