For contributors

The data model

What the tables are, how they differ from the legacy schema, and what has not been migrated yet.

The schema is derived from the previous system's, which had roughly sixty tables in five clusters. Most of it carries over unchanged in meaning. Three things were changed deliberately, and one list is deliberately still open.

The clusters

Topology. nets, vertices and edges, plus net_relations for derivation between nets. nets.name is unique and is the identifier. Coordination sequences and fractional coordinates live on the vertex.

edges carries something the legacy schema did not: the periodic image the far end of each edge sits in. Legacy had a redundant conn and a non-redundant edge table, but neither recorded which neighbouring cell an edge crossed into — so nothing could draw a net without re-parsing the uploaded topology file, and any drawing that did not re-parse it was wrong. Vertices, edges and coordination sequences are all read or computed from the deposited .mfpx at deposition, which keeps the record and the file it serves in step.

nets also carries provenance columns the legacy schema had no equivalent of — source, field_sources, rcsr_index and complexity_rank among them. The last two are two different orderings and are routinely mistaken for each other; before using either, read the two orderings a net carries, which also records which source supplies which field and which columns are NULL on every ingested record.

net_analyses is the fourth table in this cluster and the newest. One row per (net, kind, basis), where kind is periodicity, rings, invariants or systre and basis is the graph analysed — mofplus-deposit or rcsr-archive-key. Each row carries the tool and its version, the status (ok, refused, failed, timed-out) with a reason where it is not ok, the JSON result, the seconds it took, and input_checksum: the checksum of the graph that was actually analysed. That last column is what makes a result auditable — a row computed before the net's deposit changed can be told from one computed after, and a stale row is counted rather than used. UNIQUE(net_id, kind, basis) keeps one answer per question. 19 196 rows cover the collection today; see the 2026-09-12 analysis report, docs/reports/net-analysis-2026-09-12.md in the repository (the dated reports are repository records and are not served as site pages).

Three layers, deliberately kept apart

The nets side of this schema is written by three steps that must not be confused, because they make different kinds of claim:

Layer Writes Claim
Ingest (ingest.py, reconcile.py) nets, vertices, edges what the sources said
Analyse (analysis.py, mofpp analyse-nets) net_analyses only what the graph says, per kind and per basis, never merged
Apply (analysis_apply.py, mofpp apply-analysis) selected nets columns + field_sources which computed value is authoritative enough to filter and sort on

Operational order is ingest-nets → analyse-nets → apply-analysis. The analyse step never writes nets or vertices; the apply step never overwrites a source column on a vertex, so cs and vs stay the source's own strings and the computed values are served beside them under their own keys. A re-ingest may reset columns, and apply is simply re-run. The separation is what lets a record say "MOF+ sent this, and the graph says that" instead of silently presenting one as the other.

A fourth thing is not a layer, because it writes nothing. Where those two statements contradict each other the served record carries a curation call: what the source claims, what was computed, the evidence, and the decision somebody has to take. A call is assembled in services/nets.py when the record is served, from field_sources and the net_analyses rows the record already carries. There is no table for it and no column, nothing is written because of one, and no list of affected names exists to fall out of date — a record that stops meeting a rule stops carrying the call at its next request. A ruling on a call is the one part that cannot be derived, so it is recorded as a literal, nets.CURATION_DECISIONS, keyed to the exact question it answered rather than to a record; a decision changes what is presented and still writes nothing. The two orderings a net carries has the rules, and the API reference the served shape.

Building blocks. bbs, keyed by a name unique across organic and inorganic together.

Frameworks. frameworks (the legacy mofs), each optionally pointing at the net it is built on, with structures hanging off it — one row per geometry per level of theory, because one framework legitimately has several.

Force fields. atypes, ff_frags, ff_refs, ff, ff_fits, params, plus the join tables that connect fragments and atom types to reference systems, and the topoqeq pair. See How force-field data is organised.

Accounts. users, groups and their join, plus tokens and legacy_auth_events.

Three deliberate departures

1. Every record carries a licence. The legacy schema had no licence column anywhere. Here it is non-null and set at deposition, per record — see Identifiers and citation.

2. Four parameter tables became one. The legacy schema had onebody, twobody, threebody and fourbody — four tables differing only in how many atom-type columns they had. They are one params table now, with the atom types and fragments as lists. Which table a row used to live in is just len(atypes).

The wire contract did not change: parameters were already addressed as params/{ric}/{fitid}, so no client noticed. Four near-identical tables becoming one is exactly the kind of simplification a two-person team should take when it is free.

3. File columns are paths. Legacy stored uploads under mangled generated filenames. Here a file column holds a relative path under the file store, so the store can be listed, rsynced and backed up without the database.

What has not been migrated

Some legacy tables have no counterpart here yet. That list is not a judgement about whether they matter — it is a list of things whose migration is still open:

  • supercell, scaledtopo, orients — needed by get_scaledtopo and get_orients, which currently answer an explicit fault naming this.
  • prop_skal, prop_xy, prop_type — computed properties of structures.
  • bbshapes, special_conn, scvertices, shapes, geoms — connection geometry, used by the assembly step.
  • the job queue — firejobs, fireweaver, firerta, fireanalyzer, cputime.
  • questionmarks, software, and the CMS tables. article is superseded by the Markdown files these pages are written in.

Some of those are certainly dormant. Which ones is a question that gets answered by counting rows in a database dump, not by reading the schema — a sweep of row counts, latest timestamps, and reachability from nets, bbs, frameworks and ff. Until that has been run, this list stays as it is: open, and honest about being open.

Where documentation lives now

Not in the database. These pages are Markdown files in the repository, foldered by Diátaxis, rendered at request time. The previous system kept documentation in a text column edited through an admin panel, which meant it was unversioned, unreviewable and edited live in production. A correction is now a pull request.