Skip to main content

h2hdb

h2hdb is the database and coordination core for the H2HDB multi-repository system. It owns the durable schema and exposes backend-neutral public ports to ingest, download, Komga, and OPDS consumers.

It deliberately does not scan files, parse galleryinfo.txt, manipulate images or CBZ files, serve HTTP, serialize OPDS documents, or depend on hbrowser. Those responsibilities live in sibling packages.

Core responsibilities

  • MariaDB and SQLite connectors, transactions, and read-only access.
  • Forward-only, versioned schema migrations.
  • Durable download queue and token-fenced download/ingest leases.
  • Database maintenance gate and scheduling state. Core public operations own participation; consumers do not wrap calls themselves.
  • Neutral catalog domain models and revision-based projection.
  • Public ports: CatalogReader, CatalogPublisher, CatalogBuildCoordinator, CatalogBuildAnalyzer, CatalogBuildProjectionCoordinator, DownloadCoordinator, and DatabaseAdmin.

The catalog projection consists of catalog_publications, catalog_contributors, catalog_subjects, catalog_artifacts, and the singleton catalog_revision pointer. Immutable descriptors for every published revision live in catalog_revision_history. A complete snapshot and its history entry are inserted before the pointer advances in one transaction. The publish is fenced by a live ingest lease, unchanged projections reuse the current revision, and readers only see fully published revisions. Every newly published publication records its exact canonical source_gallery_name and, when content exists, content_sha256, so ingest can preserve deduplication incumbents.

Large filesystem imports use the separate durable source-build workflow. A UUID CatalogBuild is bound to an opaque source/config scope_key and a live, token-fenced ingest turn. Discovery, gallery headers, unordered file chunks, gallery completions, final analysis, and the global file-hash cache are written in bounded idempotent batches. The plural header, file-chunk, and completion operations commit one bounded multi-gallery scanner batch in one fenced transaction; callers do not need one transaction per gallery. A gallery keeps an O(1) durable staged-file counter, so completing even a giant gallery does not count or materialize all of its file rows.

Discovery persists a scope-unique gallery name, root-relative source locator, and optional metadata fingerprint. Source files persist their relative locator, digest, and optional full stat observation. Scan-observation digests and their version are deliberately distinct from the historical canonical source manifest. CatalogBuildAnalyzer exposes hard-capped keyset pages and durable, idempotent batches for the ordered SOURCE_MANIFESTS, FILE_SPAM, CONTENT_DIGESTS, CONTENT_OWNERS, GID_WINNERS, and FINAL_ANALYSES reducers. It preserves duplicate file occurrences and emits explicit empty gallery sentinels, so ingest can derive the historical manifest and effective content digest without hydrating the corpus. Core persists the resulting facts but leaves Python casefold, title-length, and winner policy decisions to ingest. Gallery source pages return explicit lightweight CatalogSourceGalleryRecord values, while files use a separate (casefold(name), name, key) keyset page. They never silently return a partially hydrated GallerySourceRecord.

Candidate pages prefer selected incumbents from the active source build. Before the first source build is active, they fall back to the current legacy catalog projection using exact content_sha256 or exact GID; deterministic binary gallery-name ordering resolves an invalid legacy projection containing more than one match. This fallback cannot recover a content incumbent from a legacy publication whose nullable content_sha256 was never recorded.

Partial source builds and reserved catalog revisions are invisible to readers. CatalogBuildProjectionCoordinator exposes independent, hard-capped keyset pages for selected galleries and their files. Prepared artifact receipts are written in bounded plural batches, remain provisional until a compare-and-swap page checkpoint protects them, and then feed bounded publication-selection batches directly into the reserved revision. No operation constructs the full publication list or hydrates every file in a gallery.

After final filesystem validation and projection staging reaches COMPLETE, the caller repeatedly invokes prepare_catalog_build_operations() before sealing. Each call advances at most 1,000 rows through keyset-paged time normalization, removed-GID events, and exact-token deletion-marker consumptions. The returned CatalogBuildOperationalState.complete is the durable readiness gate. A deletion request racing publication raises the public, retryable CatalogOperationalGenerationStaleError; the same SEALED build can refresh its invisible preparation and retry instead of being abandoned.

After the source and projection descriptors are sealed, publish_catalog_build_with_projection() performs one short, live-turn-fenced transaction: it verifies only scalar sealed/prepared state, inserts immutable history/receipt and operational activation rows, swaps both pointers, atomically accounts maintenance work, and clears the working build. It does not scan source, projection, or operational effect rows. A lost commit response returns the same durable receipt on retry without double-accounting. DB_COMMITTED receipts pin published artifact pages (including gallery name, GID, and upload time) so a later live lease can finish filesystem recovery and idempotently acknowledge PROJECTION_FINALIZED. Unchanged projections reuse the current catalog revision; their unused reserved rows are removed only by bounded child-first cleanup.

The older publish_catalog_build() source-only API remains available for non-user-facing compatibility workflows, performs the same bounded operational preparation, and rejects a build with an unfinished projection. Once a build and its matching activation row are the active source revision, DownloadCoordinator reads catalog membership, pending redownload state, removed-GID events, deletion markers, and stable redownload runtime from that source authority. Older active source builds without a v5 activation row fall back to the legacy tables. Legacy publish_snapshot() is rejected after any source-build activation so the two authorities cannot diverge.

Removed-GID events are prepared per build but remain invisible until the same transaction activates their source revision. Exact request-token completion advances a per-GID revision acknowledgement, so retry and response loss cannot complete a replacement request. Deletion requests use their own token plus a singleton generation fence; consuming one build's token never consumes a later re-request. Mutable accepted-submission times live in the stable catalog_gallery_redownload_times table rather than immutable source rows.

The compatibility SQL view todelete_rm_commands follows the same exact operational activation gate. With an active source authority, it resolves filesystem paths from that build's durable source_locator and includes effective explicit deletion markers, older copies of a duplicate GID, and the persisted duplicate-page deletion flag. Exact-token consumptions suppress only the request they consumed. Before activation (including a pre-v5 source pointer without its matching activation row), the view falls back to legacy todelete_galleries/galleries_names. Its single cmd column remains safely single-quote escaped and uses rm -rf -- so paths beginning with - are not parsed as options.

Source-build history is deliberately retained only while its immutable source rows remain. list_catalog_build_cleanup_candidates() lists abandoned builds, source-only inactive builds, and inactive projections whose receipt is already PROJECTION_FINALIZED; active, working, and pending-receipt builds are never listed. For a projected build, call prune_catalog_build_projection() until complete, then call prune_catalog_build() until complete. Both are bounded and child-first. The first step deletes only build-scoped staging rows, receipt, and descriptor; immutable historical catalog_* revision rows remain readable. The second removes the old source revision and its multi-million-row source copy. Activation/event rows deliberately have no foreign key to the prunable build because unacknowledged removed-GID events must survive cleanup.

Installation

This repository uses a src layout: the import package is h2hdb, while its source lives under src/h2hdb.

uv venv --python 3.14
uv pip install -e ".[dev]"

uv.lock is intentionally ignored. Rebuild the local environment with:

./scripts/rebuild-env.sh

Configuration and schema administration

{
  "database": {
    "sql_type": "sqlite",
    "database": "/var/lib/h2hdb/catalog.sqlite3",
    "access_mode": "read-write"
  },
  "maintenance": {
    "optimize_enabled": true
  },
  "logger": {
    "level": "INFO",
    "file": null
  }
}

JSON string values that consist exactly of ${ENV_NAME} are resolved from the process environment before validation, including values nested inside objects and arrays. Variable names must match [A-Za-z_][A-Za-z0-9_]*; a missing or invalid name stops startup without including the environment value in the error. Inline interpolation is deliberately unsupported, so strings such as db-${INSTANCE} remain literal. For example, keep a MariaDB password out of the file with "password": "${H2HDB_RW_DB_PASSWORD}". Unknown JSON fields are still rejected after placeholder resolution.

For MariaDB, set host, port, user, password, and database. Consumers such as OPDS should use "access_mode": "read-only"; their database account needs SELECT plus SHOW VIEW so compatibility checks can validate critical view definitions without write privileges.

Only the core administration command migrates current, versioned schema. Its three runtime operations are:

uv run --no-sync python -m h2hdb migrate --config config.json
uv run --no-sync python -m h2hdb check --config config.json
uv run --no-sync python -m h2hdb ready --config config.json

Choose the operation from database state, not from container lifecycle:

Database state Operation
Empty Run python -m h2hdb migrate once
Non-empty and has the supported h2hdb_schema_migrations ledger Run the same forward-only migrate command
Consumer startup Run python -m h2hdb check; never migrate
Container readiness probe Run python -m h2hdb ready

migrate deliberately refuses a non-empty database without h2hdb_schema_migrations. There is no old-schema recognition, adoption, backfill, config fallback, or upgrade command. Replace an old database with an empty one and rebuild it through the current ingest workflow. A database that already has the current ledger may use later forward-only migrations as they are added.

The schema version is stored in the append-only h2hdb_schema_migrations ledger and is independent of the Python package version. Consumer startup must call check_compatibility(); it must not migrate. The lightweight ready command checks only that ledger and deliberately does not wait on the database maintenance gate. It is suitable for a frequently repeated orchestrator probe; check remains the full schema-structure audit. The later ingest bootstrap-catalog.py command is a separate data operation: it does not create schema or write the schema ledger.

Public API

Create the facade from CoreConfig or load_config():

from h2hdb import H2HDB, load_config

database = H2HDB(load_config("config.json"))
database.check_compatibility()
page = database.list_publications(limit=50)
historical = database.get_catalog_revision(1)
historical_page = database.list_publications(revision=historical, limit=50)
acquirable_page = database.list_publications(
    revision=historical,
    limit=50,
    require_artifact=True,
)

get_catalog_revision() resolves the current pointer; get_catalog_revision(revision_number) loads a durable historical descriptor or raises CatalogRevisionNotFoundError.

get_publications_by_artifact_names(..., revision=descriptor) participates in the same revision pinning contract. require_artifact=True applies the artifact predicate to both page rows and total, which lets acquisition-feed consumers paginate without post-filtering.

Every supported revision persists exact source_gallery_name; consumers never infer it from mutable canonical tables. content_sha256 is nullable only for a current-domain reason: a gallery can have no non-galleryinfo content after exclusions.

Consumers should type against the exported protocols and domain models. Do not import connector, repository, or table implementation modules from another repository.

Multi-repository development

The repositories remain independent projects; this is not a uv workspace. To create an isolated editable-install integration environment for all local packages, run:

./scripts/rebuild-multirepo-integration.sh

The script uses uv venv and uv pip install -e for each repository and does not create or consume a lock file. See docs/multi-repo-deployment.md for the container split, fresh database initialization, shared database, and CBZ mount rules.

Verification

uv run --no-sync black --check src tests scripts
uv run --no-sync ruff check src tests scripts
uv run --no-sync mypy src tests scripts
uv run --no-sync pytest
uv run --no-sync python -m build
uv run --no-sync python scripts/build-and-verify-distributions.py \
  --output-directory /path/to/empty/output-directory

The final command always builds in a fresh temporary directory. It verifies the wheel boundary and confirms that an installation taken from that wheel exposes only the migrate, check, and ready CLI operations. It copies artifacts to the requested empty output directory only after every check passes.

SQLite runs locally. Set H2HDB_TEST_MARIADB=1 with a running Docker daemon to include the MariaDB testcontainer cases.

License

GNU Affero General Public License v3 or later. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

h2hdb-0.21.0.1.tar.gz (193.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

h2hdb-0.21.0.1-py3-none-any.whl (161.4 kB view details)

Uploaded Python 3

File details

Details for the file h2hdb-0.21.0.1.tar.gz.

File metadata

  • Download URL: h2hdb-0.21.0.1.tar.gz
  • Upload date:
  • Size: 193.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for h2hdb-0.21.0.1.tar.gz
Algorithm Hash digest
SHA256 14b0dda2757de10ec0156ad89818aaf49c4acc6ec97869cb55296567a9575972
MD5 56a2a60f6eadb9353c3693b968b35f73
BLAKE2b-256 deb68357a394b2c52f6095b50b9a6075554197330959ebb08737b78d19267035

See more details on using hashes here.

Provenance

The following attestation bundles were made for h2hdb-0.21.0.1.tar.gz:

Publisher: publish.yml on Kuan-Lun/h2hdb

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file h2hdb-0.21.0.1-py3-none-any.whl.

File metadata

  • Download URL: h2hdb-0.21.0.1-py3-none-any.whl
  • Upload date:
  • Size: 161.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for h2hdb-0.21.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 c49de25325588a87a939c31ce1adc19160b01fe0e38bc3d16048ab45b2d66099
MD5 090b6fae7253f58716a7844f2d211bc4
BLAKE2b-256 630162e18ed29354e67eead79a02dfd40798ef4e91987eb83f753d58d76a6b90

See more details on using hashes here.

Provenance

The following attestation bundles were made for h2hdb-0.21.0.1-py3-none-any.whl:

Publisher: publish.yml on Kuan-Lun/h2hdb

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.31.0

2 files

0.29.0

2 files

0.28.3

2 files

0.28.2

2 files

0.28.1

2 files

0.27.0

2 files

0.26.0

2 files

0.25.0

2 files

0.24.0

2 files

0.23.1

2 files

0.23.0.11

2 files

0.23.0.10

2 files

0.23.0.9

2 files

0.23.0.8

2 files

0.23.0.7

2 files

0.23.0.6

2 files

0.23.0.5

2 files

0.23.0.4

2 files

0.23.0.3

2 files

0.23.0.2

2 files

0.23.0.1

2 files

0.22.0.2

2 files

0.22.0.1

2 files

This release

0.21.0.1 This release

2 files

0.21.0.0

2 files

0.20.0.2

2 files

0.20.0.1

2 files

0.20.0.0

2 files

0.19.0.0

2 files

0.18.0.0

2 files

0.17.0.0

2 files

0.16.0.0

2 files

0.15.0.0

2 files

0.14.0.2

2 files

0.14.0.1

2 files

0.14.0.0

2 files

0.13.0.2

2 files

0.13.0.1

2 files

0.13.0.0

2 files

0.12.0.6

2 files

0.12.0.5

2 files

0.12.0.4

2 files

0.12.0.3

2 files

0.12.0.2

2 files

0.12.0.1

2 files

0.12.0.0

2 files

0.11.0.5

2 files

0.11.0.4

2 files

0.11.0.3

2 files

0.11.0.2

2 files

0.11.0.1

2 files

0.11.0.0

2 files

0.10.8.4

2 files

0.10.8.3

2 files

0.10.8.2

2 files

0.10.8.1

2 files

0.10.8.0

2 files

0.10.7.5

2 files

0.10.7.4

2 files

0.10.7.3

2 files

0.10.7.2

2 files

0.10.7.1

2 files

0.10.7.0

2 files

0.10.6.0

2 files

0.10.5.8

2 files

0.10.5.7

2 files

0.10.5.6

2 files

0.10.5.5

2 files

0.10.5.4

2 files

0.10.5.3

2 files

0.10.5.2

2 files

0.10.5.1

2 files

0.10.5.0

2 files

0.10.4.2

2 files

0.10.4.1

2 files

0.10.4.0

2 files

0.10.3.0

2 files

0.10.2.0

2 files

0.10.1.1

2 files

0.10.0.0

2 files

0.9.1.9

2 files

0.9.1.8

2 files

0.9.1.7

2 files

0.9.1.6

2 files

0.9.1.5

2 files

0.9.1.4

2 files

0.9.1.3

2 files

0.9.1.2

2 files

0.9.1.1

2 files

0.9.1.0

2 files

0.9.0.5

2 files

0.9.0.4

2 files

0.9.0.3

2 files

0.9.0.2

2 files

0.9.0.1

2 files

0.9.0.0

2 files

0.8.0.2

2 files

0.8.0.1

2 files

0.8.0.0

2 files

0.7.0.9

2 files

0.7.0.8

2 files

0.7.0.5

2 files

0.7.0.4

2 files

0.7.0.3

2 files

0.7.0.2

2 files

0.7.0.1

2 files

0.7.0.0

2 files

0.6.68.63

2 files

0.6.68.62

2 files

0.6.68.61

2 files

0.6.68.60

2 files

0.6.68.59

2 files

0.6.68.58

2 files

0.6.68.57

2 files

0.6.68.56

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page