Storing records¶
A record is one administration of one instrument to one participant: a session row with its responses, its computed scores, and the provenance needed to reproduce those scores later.
from personality_questionnaire.db import Record, Repository, create_engine_from_env
repository = Repository(create_engine_from_env())
saved = repository.save(
Record(
participant_code="P01",
questionnaire="bfi2",
responses={1: 4, 2: 3, ...},
tag="pre",
experiment="fatigue-2026",
)
)
From the command line, pq run saves by default:
pq run bfi2 --participant P01 --experiment fatigue-2026
pq run bfi2 --participant P01 --no-save # score without storing
pq records # what is stored
pq export --shape long --output records.csv
Where the data lives¶
PQ_DATABASE_URL names the database. Unset, records go to
~/.personality_questionnaire/records.db, so a lab machine needs no server and no
configuration.
export PQ_DATABASE_URL="sqlite:////srv/study/records.db" # a shared file
export PQ_DATABASE_URL="mysql+pymysql://user:pw@host/records" # pip install '.[mysql]'
The schema is portable rather than merely claimed to be: tests/db/test_dialects.py
compiles every table against the MySQL dialect, which needs neither a server nor a
driver and catches the differences that actually bite — above all an indexed string
column with no declared length, which MySQL refuses outright.
Schema¶
participant ──< session ──< response
└──< score
Table |
One row per |
|---|---|
|
person, identified by a study-local code |
|
administration of one instrument |
|
item answered |
|
subscale computed |
Design decisions¶
Responses are rows, not a JSON blob. One row per item costs nothing at this scale and turns “what did everyone answer to item 5” into a query rather than a decode loop over every record — which is exactly the shape a reliability coefficient needs.
Scores are stored, not recomputed. A stored score, beside the scoring version and
a hash of the instrument that produced it, is evidence of what a participant was
actually told. Recomputing on read would silently rewrite history whenever the scorer
changed. scoring_version is bumped whenever the arithmetic changes, so an old
record stays interpretable.
The instrument is hashed, not just named. instrument_hash covers the items,
subscales, reverse keys and response range as administered. A later typo fix in an
item’s wording therefore cannot retroactively change what a stored record means.
Participants are coded, not named. This package stores human-subject data; a study-local code keeps a record pseudonymous by default. Nothing here encrypts the database — that is the operator’s responsibility, and the reason the application binds to localhost.
No migrations. The schema has one version. A research tool that owns its own database is better served by an explicit export and re-import than by a migration graph nobody exercises. This is a decision, not an omission.
Timestamps are UTC and aware. SQLite has no timezone type and returns naive values, so the repository re-attaches UTC on read. Without that, a loaded record could not be compared with a freshly built one at all.
Export shapes¶
Shape |
Rows |
Use |
|---|---|---|
|
one object per record |
Archival. Self-describing, includes provenance |
|
one per session |
A statistics package, single instrument |
|
one per value |
Tidy analysis, several instruments in one file |
The wide shape refuses a selection spanning instruments rather than emitting a union
of every item column, mostly empty. Filter by --questionnaire, or use long.
Export writes to stdout unless --output is given, and diagnostics go to stderr, so
pq export --shape long | duckdb works.