README.md

AirLock DB

Local-first migration tool for air-gapped environments. It reads legacy files, validates every record against a strict schema, writes accepted records to SQLite or a JSONL file, and produces one self-contained HTML report.

  • Makes no network requests.
  • Embeds CSS and SVG in the report, so it opens on a workstation with no internet access.
  • Never crashes on a bad row. Rejected rows get per-field reasons and the run continues.

Contents

Features

AreaWhat you get
InputJSON, JSONL, NDJSON, XML, CSV, TSV, plain or compressed (.gz, .zip), plus rows from a local SQLite database
MemoryStreams one record at a time, so memory stays bounded as files grow
ValidationBuilt-in financial-transaction model, or any client layout from a JSON or YAML config
Schema draftingairlock-db infer drafts a starter layout from your files and marks conflicts for review
Drift reportingMissing fields, undocumented extras, type mismatches, new enum values
OutputSQLite, a JSONL migration file, or both, with skip or replace conflict handling for SQLite
Verificationairlock-db verify rebuilds every stored row and reports changed, missing, or unexpected records
PrivacyOptional pseudonymization of sensitive fields with a run-scoped key that is never stored
RepairInteractive session for rejected rows, with crash-safe checkpoints
Large inputs--sample N previews, --commit-every N with --resume recovers, --source-query reads custom SQL

Install

Requirements:

  • Python 3.10 or newer
  • Pydantic 2.6 or newer, below 3
  • PyYAML 6.x, installed with the package and read only for YAML schema configs
  • SQLite, included with Python

On a connected machine:

powershell
1python -m pip install .

In an air-gapped environment, stage the Pydantic and PyYAML wheels in an approved local wheelhouse, then install without an index:

powershell
1python -m pip install --no-index --find-links C:\offline\wheelhouse .

The guide covers staging the wheelhouse, the manifest check, and the move to the offline machine.

From a source checkout, python main.py works directly if Pydantic 2 is installed. Every airlock-db command below also runs as python main.py.

Quick start

powershell
1python main.py --input examples/messy_transactions.json --constraints examples/constraints.json --output-dir output

Output:

text
1Records checked: 32
2Valid: 8; invalid: 24
3SQLite: output/target_migration.db (committed)
4Report: output/airlock_report.html

Open output/airlock_report.html to see the chart, drift counts, and issue table.

Use a fresh output directory for each run. Existing rows count as duplicates or conflicts.

Commands

CommandPurpose
airlock-dbMigrate sources into the target
airlock-db inferDraft a schema config from source files
airlock-db checkValidate schema, constraints, and fixed-width files without input
airlock-db verifyCheck a finished target against its sources
airlock-db repairFix rejected rows interactively or from a decisions file
airlock-db reportRebuild the HTML report from an output directory

schema-sentry is a second name for the same entry point.

Key options

OptionPurpose
--input PATHFile, archive, or directory. Repeatable
--source-db PATHLocal SQLite database as a source. Repeatable
--config PATHJSON or YAML schema layout
--constraints JSONExtra business rules. Built-in keys serve the built-in schema; field_rules serves dynamic schemas
--output-dir DIROutput directory. Default output
--output-formatsqlite (default), jsonl, or both
--dry-runValidate every source and write nothing
--conflict-policyskip (default) or replace existing record IDs
--batch-size NSQLite write batch, 1 to 10,000. Default 500
--max-report-issues NIssues embedded in the report, 0 to 10,000. Default 1000
--verboseProgress and summary log messages
--versionPrint the version

Every option is in the CLI reference.

Output files

FileWritten when
airlock_report.htmlAlways
airlock_summary.jsonAlways. Machine-readable counts, drift, storage stats, exit code
target_migration.dbSQLite target
target_migration.jsonlJSONL target
airlock_errors.jsonl--error-log is set
airlock_verify.jsonverify finds a mismatch

Common tasks

Read inputs

Several sources

Repeat --input. Each path is a supported file or a directory, scanned recursively.

powershell
1airlock-db --input C:\legacy\batch-a --input C:\legacy\batch-b.jsonl --output-dir C:\migration\run-01

CSV or TSV

Header names must match the schema. The delimiter defaults by file suffix. Override it and the encoding with --csv-delimiter and --csv-encoding.

powershell
1python main.py --input C:\legacy\accounts.csv --output-dir csv-output

Compressed files

Gzip files and zip archives stream directly and are never extracted. Every zip member with a supported suffix is validated.

powershell
1python main.py --input C:\legacy\export.jsonl.gz --output-dir gz-output
2python main.py --input C:\legacy\batch.zip --output-dir zip-output

A legacy SQLite database

Read rows directly, with no file export. Column names must match the target schema.

powershell
1airlock-db --source-db C:\legacy\records.sqlite --output-dir C:\migration\run-02
  • Choose tables with --source-table. Repeat it to read several tables in one run.
  • Read a filtered view with one --source-query per database.
powershell
1airlock-db --source-db C:\legacy\records.sqlite --source-query "SELECT * FROM records WHERE status = 'POSTED'" --output-dir C:\migration\run-03

Write outputs

JSONL migration file

Write accepted records as clean JSONL instead of SQLite. Use --output-format both for both targets.

powershell
1python main.py --input examples/messy_transactions.json --constraints examples/constraints.json --output-format jsonl --output-dir jsonl-output

Pseudonymized fields

Account IDs, counterparties, and descriptions become one-run tokens. Report details are redacted.

powershell
1airlock-db --input C:\legacy\export.jsonl --anonymize --output-dir C:\migration\run-02

Handle large migrations

Resume an interrupted run

Commit and save progress every N records. Rerun the same command with --resume to continue where the run stopped.

powershell
1python main.py --input C:\legacy\big.jsonl --commit-every 50000 --output-dir C:\migration\run-05
2python main.py --input C:\legacy\big.jsonl --commit-every 50000 --resume --output-dir C:\migration\run-05
  • Both options need the SQLite target.
  • --resume cannot combine with --anonymize, because pseudonyms are run-scoped.
  • --resume skips writes only when the source file is unchanged (same size and modification time).

Check a large source before writing

powershell
1python main.py --input C:\legacy\big.jsonl --dry-run

Counts and issue codes print to the terminal. No target, report, or summary is written.

Preview before the full run

Stop after N accepted records. The output directory holds only the scanned prefix. The report, run summary, and any repair log record that the run was a preview.

powershell
1python main.py --input C:\legacy\big.jsonl --sample 5000 --output-dir preview-output

A preview is not a handoff. verify reports the unscanned records as missing, so rerun without --sample before verifying.

Work with schemas

Validate against a client schema

Replace the built-in model with a JSON or YAML layout. The bundled example is a support-ticket schema.

powershell
1python main.py --input legacy/tickets.json --config examples/schema_config.json --output-dir ticket-output

Draft a schema from your data

infer reads the source with the normal readers and drafts a config from what is in the files. It uses conservative types and marks fields that need review.

powershell
1python main.py infer --input legacy/tickets.json --output schema_draft.yaml
2python main.py --input legacy/tickets.json --config schema_draft.yaml --output-dir ticket-output

Check a schema before you run

check loads a schema, constraints file, or fixed-width layout with the migration's own loaders, then prints the counts and fingerprints. It reads no input and writes nothing.

powershell
1python main.py check --config schema_draft.yaml --constraints field_rules.json

A bad file exits 2 before any source is read.

Verify a handoff

Before a target database leaves the machine, confirm it still matches its sources. verify re-reads the sources, rebuilds every stored row with the migration's own rules, and compares per-record digests against the committed database, which it opens read-only.

powershell
1python main.py verify --input legacy/tickets.json --config schema_draft.yaml --output-dir ticket-output
  • Pass the same source and schema flags as the original run.
  • Exit code 0 means the target matches the sources under the recorded conflict policy. Exit code 1 means a mismatch or a missing target. Exit code 2 means bad flags or config files.
  • A JSONL-only run is verified from target_migration.jsonl. When both targets exist, verify checks the database, which is canonical.
  • A changed, missing, or extra row fails the check, lists the row IDs, and writes airlock_verify.json into the output directory.
  • Anonymized fields stay out of the digests, because their tokens use a run-scoped key that is never stored.

Repair rejected rows

Write a repair log during migration, then review and fix rows.

powershell
1python main.py --input examples/messy_transactions.json --constraints examples/constraints.json --error-log --output-dir output
2python main.py repair --error-log output/airlock_errors.jsonl --constraints examples/constraints.json

Before you repair:

  • Do not add --anonymize to a migration whose log you plan to repair. Anonymized logs omit rejected values and are not repairable.
  • Pass the same --config and --constraints used for the migration. A fingerprint mismatch stops the session.
  • The database defaults to target_migration.db beside the error log. Change it with --output-db.

Prompt keys:

KeyAction
eEdit the row
pSet a field
rRemove a field
dDrop the row
qFinish

Related options:

  • repair --emit-decisions decisions.jsonl writes an inert template with one skip line per repairable entry. Edit it to script fixes. It refuses to replace an existing file without --overwrite.
  • repair --decisions decisions.jsonl applies a decisions file.
  • repair --error-log output/airlock_errors.jsonl --summary prints entry, repairability, raw-row, and issue-category counts without opening a session or writing.
  • python main.py report --output-dir output rebuilds the report with a Repair outcomes section.

Resume and checkpoint rules are in the guide.

Try real data and measure speed

Public dataset sample

A 10-row sample of real UCI Online Retail records runs through the dynamic-schema path.

powershell
1python main.py --input examples/real_data/online_retail_sample.jsonl --config examples/real_data/online_retail_schema.json --anonymize --output-dir real-data-output

examples/real_data/README.md also covers converting and running MoneyData, a real anonymized bank ledger, through the built-in schema. It includes attribution and license terms.

Benchmark throughput

Measures JSONL parsing, dynamic-schema validation, SQLite writes, and report generation on synthetic records.

powershell
1python scripts/benchmark_migration.py --rows 100000 --runs 3 --batch-size 500

Soak a large migration

Generates a synthetic JSONL source, migrates it with the real CLI, and asserts the stored row count, summary counts, wall time, and peak memory against an optional ceiling. Results land in soak_verification.json.

powershell
1python scripts/soak_migration.py --rows 1000000 --max-peak-rss-mib 300
  • On the development machine, 1,000,000 rows migrate in about 37 seconds (27k records/s) at a 41 MiB peak. Peak memory is the same at 20,000 rows, so buffers stay flat as row count grows.
  • Peak RSS is exact for a single run on Linux or macOS, where the platform reports the finished child's high-water mark.
  • Other setups sample the live process every 50 ms.
  • A platform that cannot report memory leaves it unmeasured instead of failing the ceiling check.
  • CI runs a 100,000-row variant with the same ceiling.

Exit codes

CodeMeaning
0No record or source issues, and SQLite committed
1Completed with issues: rejected rows, an incomplete source, a skipped conflicting ID, or a storage failure. The report is still written when the output directory permits it
2Command-line, configuration, output-directory, requested repair-log, or report-writing error

Documentation

DocCovers
CLI referenceEvery option, output file, size limit, conflict rule, anonymization detail, and the constraints and dynamic-schema formats
GuideRepair sessions, scripted decisions, dry runs, and the air-gap transfer
RoadmapPlanned upgrades and how each one is verified
Real-data examplesPublic datasets, attribution, conversion notes
Verification skillEnd-to-end checks for this repo, plus a reusable output verifier

License

MIT. See LICENSE.