archivebox.misc.db
Database utility functions for ArchiveBox.
Post-bootstrap: requires archivebox.config constants and uses Django lazily
(from django.db import ... inside functions). Not safe to import pre-bootstrap.
Module Contents
Functions
True if DATABASE_ENGINE selects postgres (sqlite is the default). |
|
Postgres connection params from config (NAME/USER/PASSWORD/HOST/PORT). |
|
True if this collection’s database has been initialized. |
|
Human-readable location of the database (file path or postgres DSN). |
|
Make sure a database server is reachable before running migrations. |
|
Cheap per-table approximate row counts from the backend’s optimizer stats. |
|
Clamp a model instance’s CharField values to their declared max_length. |
|
(non-sqlite only) Drop and recreate the given models’ tables from the current migration state. |
|
Reverse companion to |
|
Advance one step of a batched SQLite |
|
Cheaply compare migration files to django_migrations without invoking migrate. |
|
Return migration files on disk that have not been applied yet. |
|
Apply pending Django migrations |
Data
API
- archivebox.misc.db.is_postgres() bool[source]
True if DATABASE_ENGINE selects postgres (sqlite is the default).
- archivebox.misc.db.postgres_db_params() dict[str, str][source]
Postgres connection params from config (NAME/USER/PASSWORD/HOST/PORT).
- archivebox.misc.db.database_exists() bool[source]
True if this collection’s database has been initialized.
sqlite: the index.sqlite3 file exists on disk. postgres: the configured database is reachable and contains the django_migrations table. Safe to call before Django is set up.
- archivebox.misc.db.database_display_location() str[source]
Human-readable location of the database (file path or postgres DSN).
- archivebox.misc.db.ensure_database_ready() None[source]
Make sure a database server is reachable before running migrations.
sqlite: no-op (the file is created on first connection). postgres: verify the server accepts connections and create the configured database if it does not exist yet. Raises SystemExit with a helpful message if the server is unreachable.
- archivebox.misc.db.approximate_row_counts(connection) dict[str, int][source]
Cheap per-table approximate row counts from the backend’s optimizer stats.
sqlite: reads sqlite_stat1 (populated by ANALYZE). postgres: reads pg_class.reltuples (maintained by autovacuum/ANALYZE). Returns {} on any failure; tables never analyzed may be absent.
- archivebox.misc.db.truncate_overlong_charfields(instance=None, **kwargs) None[source]
Clamp a model instance’s CharField values to their declared max_length.
SQLite never enforces VARCHAR(n) limits, so ArchiveBox has always stored overlong values (e.g. long crawl labels or page titles) untruncated. PostgreSQL enforces them and would raise DataError on save instead. Truncating keeps writes succeeding identically on both backends.
Dual-use: works as a
pre_savereceiver (Django passesinstance=andsender=as kwargs; registered inCoreConfig.ready()) and as a plaintruncate_overlong_charfields(obj)call forbulk_createpaths, which bypass signals.
- archivebox.misc.db.rebuild_models_from_migration_state(apps, schema_editor, app_label: str, model_names: list[str]) None[source]
(non-sqlite only) Drop and recreate the given models’ tables from the current migration state.
ArchiveBox’s historical sqlite migrations rebuild tables with raw SQL that intentionally diverges from Django migration state (state-only AddFields reconciled by later sqlite rebuilds). Postgres support postdates all of them, so a non-sqlite database can never contain legacy data at these points in history: every affected table is empty, and dropping + recreating it from state is always equivalent, keeping the real schema in lockstep with migration state at each divergence point. No-op on sqlite.
- archivebox.misc.db.drop_models_on_postgres(apps, schema_editor, app_label: str, model_names: list[str]) None[source]
Reverse companion to
rebuild_models_from_migration_state.Drops the given models’ tables on non-sqlite backends (in the order given, so callers pass reverse-dependency order). No-op on sqlite, whose reverse is handled by the gated
RunSQLreverse_sql instead.
- archivebox.misc.db.run_db_analyze_batch(remaining: list[str] | None, *, max_seconds_per_table: float = 120.0) list[str][source]
Advance one step of a batched SQLite
ANALYZEsweep.Without periodic ANALYZE the optimizer’s table stats go stale as snapshot/archiveresult tables grow, causing it to start large joins from
auth_userinstead of using the indexed url column and blowing snapshot detail page render time from ~50ms to ~500ms+.The whole sweep is spread across many calls instead of running as one blocking
ANALYZE: passNoneto start a fresh sweep (this call enumerates user tables and runsANALYZEon the first one); pass the returned list to advance one more table on each subsequent call. An empty return value means the sweep is complete (or has been aborted) and the next caller should passNoneagain. Caller is responsible for throttling new sweeps (orchestrator starts at most one per 24hr while idle) and enforcing a hard upper bound on total sweep wall time.Safety guarantees:
Never raises: every database call is wrapped; on any failure the function returns
[](abandoning the rest of the sweep) so the orchestrator never crashes on maintenance errors.Bounded per-call wall time: a SQLite progress handler aborts the current
ANALYZEstatement oncemax_seconds_per_tableis exceeded, so a single pathological table cannot wedge the call.Never leaves the db locked: each
ANALYZEruns as a single statement transaction that auto-commits (or rolls back on abort/error). The cursor and progress handler are always cleaned up infinallyblocks even if Python raises mid-call.Silent no-op on non-SQLite backends.
WAL journal mode (set in Django settings) keeps readers fully unblocked throughout; the writer lock is only held for the brief
sqlite_stat*flush after each table completes.
- archivebox.misc.db.sqlite_lock_holders(db_path: pathlib.Path = CONSTANTS.DATABASE_FILE) list[str][source]
- archivebox.misc.db.log_sqlite_lock_holders(console: Any, *, db_path: pathlib.Path = CONSTANTS.DATABASE_FILE, limit: int = 8) None[source]
- archivebox.misc.db.retry_sqlite_locks(action: collections.abc.Callable[[], Any], *, label: str, stderr: TextIO | None = None) Any[source]
- archivebox.misc.db.migration_state(out_dir: pathlib.Path = CONSTANTS.DATA_DIR) tuple[list[str], list[str], dict[str, str]][source]
Cheaply compare migration files to django_migrations without invoking migrate.