Skip to content

Architecture

The project is a small assembly line where each package has one job. Data flows left to right: connect → decide per column → transform → verify.

flowchart TD
    A["cli.py<br/>(the buttons)"] --> B["runner.py<br/>(the manager)"]
    B --> C["connectors/<br/>(database talker)"]
    C --> D["detection/<br/>(decision team)"]
    D -- "sensitive" --> E["masking/<br/>(the scrambler)"]
    D -- "unknown" --> R["review queue<br/>(reported, untouched)"]
    E --> C
    B --> V["validation/<br/>(the inspector)"]
    D <--> F["history/<br/>(memory)"]
    D -. "only if enabled" .-> G["llm/<br/>(AI helper)"]
Piece Job
cli.py The dbmask commands (scan, mask, validate, history, seeds, strategies). Owns all safety UX: --apply gating, redacted previews, warnings.
config.py Typed dataclasses for the YAML config, with ${ENV} expansion.
runner.py Orchestration and the fail-closed rules (an incomplete scan refuses to mask). The library entry point.
connectors/ One SQLAlchemy code path for every dialect: introspection, sampling, keyset-paginated read→write (iter_pages), row updates. Subclass Connector for non-SQL sources.
detection/ The layered pipeline: overrides → history → patterns → LLM → UNKNOWN.
history/ Decision store (any SQLAlchemy URL). Reproducibility and auditability.
llm/ OpenAI-compatible + local providers behind one interface.
masking/ Strategies, bundled dictionaries, the seed map, and the page-by-page ETL engine.
validation/ Row counts, schema comparison, PK-aligned completeness.

Design choices worth knowing

Keyset-paginated apply. The engine reads one key-ordered page, writes it back, then reads the next — reads and writes never overlap. (A streaming read with interleaved writes deadlocks SQLite: the in-flight SELECT holds a SHARED lock that blocks the writer's COMMIT.) Pages advance with an expanded row-value comparison, so composite keys work on engines without native row-value support.

Decisions are data. Every conclusive classification is a Decision row in the history store — who decided (override/pattern/LLM/history), with what confidence, when. dbmask scan --json and dbmask history expose the same records for audit.

Fail closed, everywhere. Unanalyzed column → refuse to mask. Unclassifiable column → UNKNOWN, reported, untouched, re-examined next run. Unmaskable (key) column → excluded and announced. Unverifiable check → warning that --strict turns into failure.

The engine never invents data. NULL in, NULL out, for every strategy.

Testing philosophy

Every bug fix lands with a regression test that fails on the old code, and most tests drive the real CLI against real config files and throwaway SQLite databases — the same code path a user hits. The suite (121 tests as of 0.1.1) doubles as documentation of every sharp edge found so far: tests/.