Architecture

This page is the maintainer map for GFFBase 0.2. It describes which layer owns each invariant and where a change must be tested. The public API is Python, the parser has native and fallback implementations, and DuckDB is the durable source of truth after ingest.

End-to-end data flow

GFF3/GTF path or bytes
        │
        ▼
parser.py ── native PyO3 parser or pure-Python fallback
        │       ParsedFeature + directives + dialect + diagnostics
        ▼
ingest.py ── bounded Python/Arrow batches
        │
        ├── raw rows and attributes
        ├── duplicate / multipart resolution
        ├── optional GTF parent synthesis
        ├── direct edges and transitive closure
        └── R-tree when spatial is available, B-tree otherwise
        ▼
DuckDB schema v2
        │
        ▼
FeatureDB ── scalar iterators, batched Arrow/data-frame APIs,
             mutation, validation, migration, and export

The native parser currently reads the complete plain or gzip stream before it returns records. Arrow batching bounds the Python-to-DuckDB handoff, but it does not make native input decoding streaming. Peak input memory therefore remains a known limitation and is measured independently from database memory.

Layer responsibilities

Parsing

parser.py selects gffbase._native when its compiled version matches the Python package and otherwise uses _pyfallback. Both engines must return the same records, warnings, directives, and failures for the same profile.

Both stream their input: the Rust engine through a window of about 1 MiB refilled as lines are consumed, the fallback a line at a time. gzip is recognized by content, a one-file tar archive is read as the file it holds, and a truncated or corrupt stream ends in the same ReadError in both.

validation="gffutils" is the compatibility profile used by create_db(mode="compat"). It preserves permissive legacy input handling and attaches diagnostics. validation="ncbi" is the formal profile: malformed rows fail at the source line rather than being repaired during ingest.

Ingest and identity

ingest.py is the transaction boundary. It writes Arrow batches, resolves raw identifiers, materializes multipart features, derives relationships, and only commits after final indexes and metadata exist. A failure must leave neither a partially usable database nor an orphan temporary file.

With the Rust engine the per-record work – id resolution, the duplicate strategies, batching – happens in IngestProducer (rust/src/producer.rs), which hands each batch of two DuckDB row groups to pyarrow as column buffers without a copy. A transform, a callable id_spec or the pure-Python engine takes the Python loop in _build_database instead; it is also the oracle the producer is tested against. Ingest into a file runs under a DuckDB memory budget (_MemoryBudget) that retries a step with more memory rather than fail, and checkpoints at step boundaries.

The stored id is a unique logical key. raw_id records the identifier found in the annotation before collision handling. Physical pieces of a discontinuous feature live in segments; the feature row stores the envelope. Synthetic GTF parents use the same identity and provenance machinery as input rows. A raw parent identifier spanning incompatible sequence/strand groups is an error unless the caller explicitly selects deterministic unique creation.

Schema and relationships

Schema v2 has one logical feature row, normalized attributes, direct edges, materialized transitive closure, sparse multipart segments, provenance tables, and compatibility views. Direct edges are authoritative. Closure is a query acceleration structure and the full validator recomputes it from edges in both directions.

Relationship construction order is fixed:

load raw rows → resolve IDs/multipart rows → synthesize GTF parents
→ create direct edges → compute closure → build indexes → commit metadata

Changing that order requires migration, mutation, round-trip, and validator tests because IDs embedded in an earlier structure cannot safely be repaired afterward.

The primary key on features.id is added after the load. The closure is built one level at a time (schema.build_closure) and written sorted by descendant; edges are written sorted by parent. Indexes are single-column – DuckDB never scans a multi-column one – and built one at a time; attributes(feature_id), used only by writes, is built on the first write that needs it.

Query routing

FeatureDB owns connection lifecycle and query routing. Region queries use the R-tree only when the spatial extension and index were successfully created; otherwise they filter on seqid, start and end, which DuckDB prunes by zone map. Multipart envelope candidates are rechecked against physical segments.

A relationship query is two single-table lookups: the relatives’ ids – from edges for level 1, from the closure otherwise – then those features by primary key. A recursive CTE takes over where the walk may run past the closure’s depth. When the anchor is a feature an open iterator holds, the call is prefetched: a window of that iterator’s features is answered by one lookup of the relation pairs and one fetch of the rows – by file_order span where the rows are dense, by key otherwise – and every write invalidates it.

The batched APIs execute one set-based query and return Arrow by default; Pandas and Polars conversions are optional. A loop of scalar queries is not a substitute for a batched performance measurement.

Validation, migration, and export

Fast validation checks inexpensive structural invariants. Full validation adds closure equivalence and checks that need recursive or sampled content work. Every issue has a stable machine-readable invariant identifier and severity.

Migration changes physical schema only and remains atomic. SQLite export and text serialization consume the logical/physical views rather than inventing a second interpretation of multipart or synthetic features.

Correctness boundaries

  • Parser equivalence is checked at records, diagnostics, and exceptions—not merely at row counts.

  • Ingest correctness is checked with deterministic feature, relationship, and feature-type signatures.

  • R-tree/B-tree and scalar/batched routes must agree on IDs and ordering guarantees.

  • Direct edges, closure, normalized attributes, and synthetic parents must not disagree, even if normal API queries still appear to work.

  • Benchmarks run installed release wheels. Importing an ignored in-tree native artifact invalidates a result.

See Testing for the gates around these boundaries and Benchmark methodology for measurement rules.