Skip to content

09. The database

The design of the VeeJay database, from the meaning of the data down to the tests that prove it. It is written as layers, and each layer is produced from the one above it, so a fact is written once and everything else follows from it.

Layer Section Made by
The things VeeJay knows about 9.1 The data model Written by hand
Every field and its rules 9.2 The data dictionary Stored as data; the field pages are generated from it
The fixed lists 9.3 Reference data Stored as data; the lists below are generated from it
How it is stored 9.4 The physical schema Generated from layers 2 and 3
How it changes over time 9.5 Migrations Generated, then frozen
Proof that the rules hold 9.6 Rules and tests Written by hand, run automatically
Why it is this way 13. Decisions Written by hand, append only

Two rules hold the design together:

  1. One place for every fact. The data dictionary is the only place a field is defined, and it lives as data in the database's own blueprint tables. The tables, the checks and the field pages of this site are produced from it and never typed again.
  2. The database refuses bad data. Every rule is enforced by the database file itself (types, lists, formats, keys, links and cross-field checks), so it holds whether a value arrives from a person at the screen, from a script or from an import. A rule that exists only in a script or on a screen is not a rule.

9.1 The data model

The data model names the things VeeJay knows about and how they relate. It has no columns and no storage detail. If someone who has never seen the database cannot follow this section, the model is wrong.

The things

Thing What it is Identified by
Artist A person or a group that performs or is credited. A group's members, a backing band and a side project are separate artists, linked by relations. A MusicBrainz artist ID when one exists; otherwise the row's own number
Album The original studio release (album, EP or single) that a song is filed under. A later re-release never replaces it. A MusicBrainz release-group ID when one exists
Collection A film, television show or video game that has its own songs. Songs of a collection are filed under it, not under an album. Its own name and year
Song One recording by one main performer. A cover by another artist is a different song. A MusicBrainz recording ID when one exists
Video One YouTube video. Every video belongs to one song and has a role: the Primary (the one kept as the song's video) or an Alternate. The YouTube video ID, always present and unique
File One file on disk, either a video or an audio file, with facts measured from the file itself. Its path, and the hash of its contents
MTV piece An ident, bumper, promo or similar item from MTV's own broadcast. It is not a song. The YouTube video ID
Vendor A kind of outside identifier (for example a Spotify artist ID or a Wikidata ID): how it is written and how a link to it is built. A permanent key
Data point One fact about an artist found by a search, such as a Wikipedia page or a social account. It starts as a candidate and becomes verified only when the page is shown to belong to that artist. Its artist, its vendor and its value
Review item A disagreement or a doubt that a person settles before a value is written. Its own number
Artist relation A link between two artists: member of, backing band of, side project of, alias of. The pair of artists and the kind
Score setting A named number that the identity score reads. Its name
Platform A service that holds playlists, with what it can and cannot do (add, remove, reorder, hold music videos). Its name
Song on a platform One copy of a song on a platform: the platform's own identifier for the track or the video. The song and the platform

How they relate

erDiagram
    Albums }o--|| Artists : "Artist"
    Albums }o--|| Collections : "Collection"
    Artist_Relations }o--|| Artists : "Artist"
    Artist_Relations }o--|| Artists : "Related_Artist"
    Data_Points }o--|| Artists : "Artist"
    Data_Points }o--|| Songs : "Song"
    Files }o--|| Videos : "Video"
    Review }o--|| Artists : "Artist"
    Review }o--|| Songs : "Song"
    Review }o--|| Videos : "Video"
    Song_Platform_IDs }o--|| Platforms : "Platform"
    Song_Platform_IDs }o--|| Songs : "Song"
    Songs }o--o{ Artists : "Additional_Artists"
    Songs }o--|| Albums : "Album"
    Songs }o--|| Artists : "Track_Artist"
    Videos }o--|| Collections : "Collection"
    Videos }o--|| Songs : "Song"
From table Field Points to Cardinality
Albums Artist Artists many to one
Albums Collection Collections many to one
Artist_Relations Artist Artists many to one
Artist_Relations Related_Artist Artists many to one
Data_Points Artist Artists many to one
Data_Points Song Songs many to one
Files Video Videos many to one
Review Artist Artists many to one
Review Song Songs many to one
Review Video Videos many to one
Song_Platform_IDs Platform Platforms many to one
Song_Platform_IDs Song Songs many to one
Songs Additional_Artists Artists many to many
Songs Album Albums many to one
Songs Track_Artist Artists many to one
Videos Collection Collections many to one
Videos Song Songs many to one

Principles of the model

  1. One fact, one place. A fact about an artist is stored on the artist and nowhere else. A video points to its song, the song to its album, the album to its artist, so a correction made once is right everywhere.
  2. A video is the unit of the library. A row exists because a video exists. A video that is deleted, private or blocked has no row.
  3. Audio belongs to its video. Each video has at most one audio file that goes with it. A video with no audio yet is a to-do, not an error. Audio-only files are not part of the library.
  4. Names are derived, never stored. The file name and the file tags of a video are built from its row and the rows it points to. There is no stored copy to drift.
  5. Identity before facts. Before any fact is written about an artist, the artist's identity is established by evidence and scored. Only verified facts count toward the score and may fill a field.
  6. Outside identifiers are first-class. Each outside system (MusicBrainz, Wikipedia, Wikidata, YouTube, Spotify, Apple Music, IMVDb and others) is described once in the vendor list. A stored identifier is checked against its vendor's format, and its link is built from the identifier, never typed.
  7. Provenance for every researched value. The source of each researched value is recorded, so bad data can be traced to where it came from.
  8. Files can be synchronized. Each file carries its size, content hash and modified time, so that copies on different devices can be compared exactly.

What the model leaves out

  • Lyric text beyond what is stored for a verified song. Searches are used for identity and spelling only.
  • Thumbnails, codecs, frame rates and match scores.
  • Which device holds which file. It is added when the synchronization feature is built.

9.2 The data dictionary

The data dictionary defines every field of every table once. It is the single source from which the physical schema, the checks, the reference lists and the field pages are produced. Nothing in a lower layer may add, rename or retype a field. A change starts here.

Where it lives. The dictionary is data in the blueprint tables of the database file, not text in a document. Every row has a name, a definition and a purpose, and a row cannot be saved without them. A field row also holds its type, whether it is required, its MP4 and MP3 tag (or None when it is not a file tag), its score weight and a permanent key that never changes when the field is renamed.

The field reference has one page for each table, with every field and its rules. Those pages are generated from the blueprint.

Conventions

Names.

  • Every table and field is written Title_Case_With_Underscores. An outside vendor keeps its own capitals: YouTube, MusicBrainz, IMDb, IMVDb, TikTok, SoundCloud. SQLite treats names without regard to case, so the dictionary name is used as written, with no second spelling.
  • An identifier from an outside system is Vendor_Entity_ID and is stored (Spotify_Artist_ID, MusicBrainz_Release_Group_ID). A link to it is Vendor_Entity_URL and is derived from the identifier. A link is typed only when no identifier exists (Official_Website_URL, Wikipedia_Artist_URL).
  • A field that links to another table is named for that table in the singular (Song, Album), with a role in front when a table links twice (Track_Artist).
  • Endings carry the unit: _Date (a day), _Year, _Count, _Time (a date and time, always UTC), _Seconds, _Bytes, _Kbps, _Hz, and _Hash_SHA256 for a hash. A yes or no field starts with Is_ or Has_. A choice field is a singular noun for its category (Role, Origin).
  • There is no general link field and no general platform field anywhere.

Types.

Dictionary type Meaning Stored as
Text Free or formatted text TEXT
Whole number An integer INTEGER (64-bit, so view counts never overflow)
Decimal number A number that may have a fraction REAL
Date A calendar day TEXT in the form YYYY-MM-DD, checked
Date and time (UTC) An instant TEXT in the form YYYY-MM-DDTHH:MM:SSZ, checked
Yes or no A flag INTEGER that must be 0 or 1, never empty
One value from a list A closed list A link to a reference table
Link to one row Many-to-one A foreign key
Links to several rows Many-to-many A join table
Derived text, Derived number Computed from other fields A generated column or a view, never stored by hand

The rule vocabulary. Every field may carry rules. Each rule has a short kind name, so the schema generator and the tests can treat all fields alike.

Rule kind Meaning Example
Required The field must have a value A video must have its YouTube video ID
Unique No two rows may hold the same value A MusicBrainz artist ID belongs to one artist
Pattern The value must match a written format A MusicBrainz ID is a lowercase 36-character UUID
Range A number or date must lie between two limits A year is 1900 to 2100
List The value must be on a fixed list that is part of the field Role is Primary or Alternate
Reference The value must exist in a reference table A country code must be an ISO 3166-1 alpha-3 code in the country table
Vendor format The value must satisfy the format that the vendor list gives for that identifier A Spotify artist ID is 22 letters and digits
Cross-field A rule over several fields of one row A Group has no life status; a Person has no activity status
Cross-row A rule over several rows A song has exactly one Primary video
Normalized Text must already be in clean form No leading or trailing spaces; ASCII only where the naming rules require it

Defaults.

  • A field is optional unless the dictionary marks it Required.
  • A Yes or no field is never empty; it has a default.
  • A choice field with a natural starting value has that default, so it is never blank.
  • A derived field is read-only everywhere.

The blueprint tables

The blueprint is the one part of the database that is written by hand (the first migration). It has seven tables and two views, and every other table is generated from its rows.

Table One row for Columns
blueprint_Tables Each table of the database Table_Key (permanent, such as ARTISTS), Name, Area, Definition, Purpose, Is_Retired
blueprint_Fields Each field of each table Field_Key (permanent, such as ARTISTS.ORIGIN_COUNTRY_CODE), Table_Key, Name, Definition, Purpose, Data_Type, Is_Required, Default_Value, List_Key, Link_Table_Key, Derived_Expression, MP4_Tag, MP3_Tag, Score_Weight, Position, Is_Retired
blueprint_Rules Each rule Rule_Key (such as V-01), Field_Key (empty for a rule over a whole table), Kind, Statement, Expression, Wrong_Example, Test_Name, Is_Retired
blueprint_Lists Each small closed list List_Key, Name, Definition, Purpose, Is_Retired
blueprint_List_Values Each value of a list List_Key, Value_Key (permanent), Label, Definition, Sort_Order, Is_Retired
blueprint_Vendors Each kind of outside identifier Vendor_Key, Vendor_Name, Entity, ID_Characters, ID_Length, ID_Prefix, URL_Prefix, URL_Suffix, Example_ID, and the pattern, calculated from those parts
blueprint_Terms Each concept that is not a table or a field (Anchor, Chain of trust, Song group) Term, Definition, Purpose
  • Keys. A _Key never changes, even when the name does. The generator, the scripts and the tests find a field by its key. A name is a label and may be corrected.
  • Definition and purpose. The definition says what the thing is; the purpose says why the database has it. Both are required on every row, and neither may be empty or only spaces.
  • Tags. MP4_Tag and MP3_Tag name the tag the field is written to in each file format, or hold the word None when the field is not a file tag. A tag is never left blank, so "not decided" cannot be mistaken for "no tag".
  • Retirement. A row is retired (Is_Retired), never deleted, once anything has used it. A retired field keeps its key forever, and its key is never reused.

Two views watch the blueprint: blueprint_Drift lists every difference between the blueprint and SQLite's own list of real tables and columns (a real column with no blueprint row, a blueprint row with no real column, a type that does not match), and is empty when the database is true to its blueprint. blueprint_Incomplete lists every row that is not yet complete enough to generate from.

Rules of the blueprint itself. The blueprint is protected by the same machinery as the data.

Rule Statement
B-01 A table, field, rule, list, list value, vendor or term cannot be saved without a definition and a purpose.
B-02 A name follows the naming convention above. A permanent key is written in capitals with underscores and never changes.
B-03 A field's type is one of the dictionary types. A List field names its list; a Link field names its target table; a derived field has an expression; no other field has any of the three.
B-04 MP4_Tag and MP3_Tag are never blank.
B-05 A field's name is unique within its table, and a table's name is unique in the database.
B-06 A rule names the kind of check it is, and a Pattern, Range or Reference rule has its expression. Every rule has a tempting wrong value for its test.
B-07 A row that anything refers to cannot be deleted, only retired.
B-08 A score weight is zero or more.

Rules that apply to specific fields

These rules are enforced by the database. Each has a number that the rule catalog in 9.6 and the tests use.

Rule Field or fields Kind Statement
V-01 Artists.Origin_Country_Code Reference Three capital letters that exist in the country table (ISO 3166-1 alpha-3). A well-formed but unknown code, such as USS, is refused.
V-02 Artists.Life_Status Cross-field Allowed only when the artist is a Person. Values: Living, Deceased.
V-03 Artists.Activity_Status Cross-field Allowed only when the artist is a Group. Values: Active, Dissolved.
V-04 Every Vendor_Entity_ID field Vendor format The value satisfies the format of its vendor row (characters, length, prefix).
V-05 Videos.YouTube_Video_ID, MTV.YouTube_Video_ID Required, Unique, Pattern Exactly 11 characters from letters, digits, hyphen and underscore. No two rows, in the two tables together, hold the same ID.
V-06 Videos.Song Required Every video in the core belongs to a song. A video that is not yet identified is held in staging.
V-07 Songs Cross-row A song has at most one Primary video, and exactly one once it is verified.
V-08 Videos.Role = Alternate Cross-row An Alternate has no audio file.
V-09 Files.File_Path Pattern Ends in .m4v for a video and in .m4a or .mp3 for audio, and lies under the library root.
V-10 Files.File_Hash_SHA256 Pattern 64 lowercase hexadecimal characters.
V-11 Year fields Range 1900 to 2100.
V-12 Data_Points Cross-field A verified data point names the page that proved it, and the vendor of its value exists.
V-13 Artist_Relations Cross-field An artist is never related to itself, and a pair and kind appear once, in one canonical direction.
V-18 Songs.ISRC Pattern Exactly 12 characters: two letters, three letters or digits, then seven digits.
V-14 Names, titles, albums Normalized No extra spaces, no character that a file name cannot hold, and every word capitalized except the listed stylized names (such as iHeart, deadmau5).

9.3 Reference data

Reference data is every list of allowed values that does not change from row to row. A field that must hold one of those values does not store free text. It links to a row of the list, so a value that is not on the list cannot be stored.

How a list is stored

Kind of list Stored as When to use
Small, closed, owned by one field A reference table with a key and a label Role, Origin, Performance, Version, Video type, Content, Uploader, Life status, Activity status
Large or shared by several fields A reference table with a code, a name and any extra facts Countries
Describes how to treat other data A reference table the schema reads The vendor list, the score settings

Every reference table has a permanent key that never changes, a human-readable label that may be edited, and a flag that retires a value instead of deleting it. A retired value stays on old rows and cannot be chosen for new ones. There is never an "Other" value: a value that fits no list is a missing value, and the right fix is a new value on the list.

Lists to be created

Table Holds Source of its rows
ref_Countries The ISO 3166-1 alpha-3 code, the English short name, and the alpha-2 and numeric codes The ISO standard, loaded once by a migration
ref_Life_Statuses Living, Deceased This chapter
ref_Activity_Statuses Active, Dissolved This chapter
ref_Artist_Types Person, Group This chapter
ref_Roles, ref_Origins, ref_Performances, ref_Versions, ref_Video_Types, ref_Contents, ref_Uploaders The choice lists below The blueprint
ref_Platforms The platforms that hold playlists and what each can do The blueprint
Vendors One row for each kind of outside identifier The blueprint
Score_Settings The numbers the identity score reads The blueprint

Current contents

Identifier formats

Key Vendor Entity Characters Length Prefix Pattern Example
ALLMUSIC_ARTIST AllMusic Artist Digits 10 mn ^mn[0-9]{10}$ mn0000123456
APPLE_MUSIC_ARTIST Apple_Music Artist Digits 0 ^[0-9]+$ 159260351
APPLE_MUSIC_SONG Apple_Music Song Digits 0 ^[0-9]+$ 1440800000
BILLBOARD_ARTIST Billboard Artist Slug 0 ^[A-Za-z0-9._~+-]+$ billy-joel
BRITANNICA_ARTIST Britannica Artist Slug 0 ^[A-Za-z0-9._~+-]+$ Elvis-Presley
DEEZER_ARTIST Deezer Artist Digits 0 ^[0-9]+$ 1125
DISCOGS_ARTIST Discogs Artist Digits 0 ^[0-9]+$ 1
FACEBOOK_PAGE Facebook Page Username 0 ^[A-Za-z0-9._]+$ TaylorSwift
GENIUS_ARTIST Genius Artist Slug 0 ^[A-Za-z0-9._~+-]+$ Drake
IMDB_ARTIST IMDb Artist Digits 0 nm ^nm[0-9]+$ nm2357847
IMVDB_ARTIST IMVDb Artist Digits 0 ^[0-9]+$
INSTAGRAM_ACCOUNT Instagram Account Username 0 ^[A-Za-z0-9._]+$ taylorswift
LAST_FM_ARTIST Last_FM Artist Slug 0 ^[A-Za-z0-9._~+-]+$ Taylor+Swift
MUSICBRAINZ_ARTIST MusicBrainz Artist UUID 0 ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ 070d193a-845c-479f-980e-bef15710653e
MUSICBRAINZ_RECORDING MusicBrainz Recording UUID 0 ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ 5f3b1c2e-8a4d-4e0f-9b6a-1d2c3e4f5a6b
MUSICBRAINZ_RELEASE_GROUP MusicBrainz Release Group UUID 0 ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ 2b7c7e92-2f64-4d0f-a46c-3f9b5f6b1b7a
SOUNDCLOUD_ARTIST SoundCloud Artist Slug 0 ^[A-Za-z0-9._~+-]+$ example-artist
SPOTIFY_ARTIST Spotify Artist Letters_And_Digits 22 ^[A-Za-z0-9]{22}$ 06HL4z0CvFAxyc27GXpf02
SPOTIFY_TRACK Spotify Track Letters_And_Digits 22 ^[A-Za-z0-9]{22}$ 6ndmKwWqMozN2tcZqzCX4K
TIKTOK_ACCOUNT TikTok Account Username 0 ^[A-Za-z0-9._]+$ taylorswift
VEVO_CHANNEL VEVO Channel Base64_Url 22 UC ^UC[A-Za-z0-9_-]{22}$ UCqECaJ8Gagnn7YCbPEzWH6g
WIKIDATA_ARTIST Wikidata Artist Digits 0 Q ^Q[0-9]+$ Q42
WIKIDATA_COLLECTION Wikidata Collection Digits 0 Q ^Q[0-9]+$ Q42
X_ACCOUNT X Account Username 0 ^[A-Za-z0-9._]+$ taylorswift13
YOUTUBE_CHANNEL YouTube Channel Base64_Url 22 UC ^UC[A-Za-z0-9_-]{22}$ UCqECaJ8Gagnn7YCbPEzWH6g
YOUTUBE_CHANNEL_OF_VIDEO YouTube Channel Base64_Url 22 UC ^UC[A-Za-z0-9_-]{22}$ UCqECaJ8Gagnn7YCbPEzWH6g
YOUTUBE_LABEL_CHANNEL YouTube Label Channel Base64_Url 22 UC ^UC[A-Za-z0-9_-]{22}$ UCqECaJ8Gagnn7YCbPEzWH6g
YOUTUBE_OFFICIAL_VIDEO YouTube Official Video Base64_Url 11 ^[A-Za-z0-9_-]{11}$ oXm2AuqwPlc
YOUTUBE_TOPIC_CHANNEL YouTube Topic Channel Base64_Url 22 UC ^UC[A-Za-z0-9_-]{22}$ UCqECaJ8Gagnn7YCbPEzWH6g

Choice lists

Table Field Values
Artist_Relations Relation Member of, Backing band of, Side project of, Alias of
Artists Activity_Status Active, Dissolved
Artists Artist_Type Person, Group
Artists Life_Status Living, Deceased
Collections Collection_Type Movie, Television, Video Game
Data_Points Found_By Google, Bing, MusicBrainz, Wikidata, Our videos, Lidarr, Owner
Data_Points Point_Status Unverified, Verified, Rejected
Data_Points Verified_By MusicBrainz, Wikidata, Wikipedia, Artist's own page, Our videos, Lidarr, Owner
Files File_Kind Video, Audio
Files File_Status Monitored, Downloaded, Replaced, Needed, Pending, Filed
Files Obtained_From YouTube, Soulseek, Tracks folder
MTV Category Ident, Bumper, Promo, Top Of The Hour, Broadcast Excerpt, Outro
Review Owner_Decision Accept proposed, Keep current, Use my value
Review Recommendation Accept proposed, Keep current
Review Review_Status Open, Decided, Applied
Song_Platform_IDs Item_Kind Track, Video
Vendors ID_Characters Digits, Letters_And_Digits, Base64_Url, Hex, UUID, Username, Slug
Videos Content Clean, Explicit, None
Videos Origin Original, Cover
Videos Performance Studio, Live
Videos Role Primary, Alternate
Videos Uploader Artist Or Label, Broadcaster, Auto-Generated, Fan
Videos Version Original, Remix, Edit, Instrumental, Acoustic, Remastered, Orchestral
Videos Video_Type Music Video, Lyric, Visualizer, Live Footage, Montage, Documentary

Score settings

Setting Value
Cap_Identity_Databases 22
Cap_Listening_Services 16
Cap_Official_Presence 24
Cap_Profile 12
Cap_Ties_To_Our_Data 26
Cutoff_Complete 85
Cutoff_Probable 30
Cutoff_Verified 50
Minimum_Families 2

Rules for changing a list

  1. A list changes only through a migration, never by editing a row in the application's screens.
  2. A value is never reused for a different meaning. A value that turns out wrong is retired and a new one added.
  3. A value must not overlap another value of the same list or be ambiguous. When two values could both describe a case, the list is revised so they cannot.
  4. The reference tables are loaded before any data and are the same in every copy of the database.
  5. The country table is loaded from the ISO standard by a migration. When the standard changes, a new migration changes the table.

9.4 The physical schema

The physical schema is the real SQLite structure: tables, keys, types and constraints. Nobody writes it by hand, apart from the first migration that creates the blueprint tables themselves. A generator reads the blueprint and produces it, so the schema can never disagree with its definition.

Where it runs

The database is one SQLite file inside the VeeJay program. It needs nothing installed and a backup is a copy of the file. The program is the only writer: the web interface, the API and the background worker all reach the file through one storage layer. That layer is the only code that opens the database, and it opens it the same way every time:

Setting Why
PRAGMA foreign_keys = ON SQLite does not enforce links unless this is switched on for each connection. The storage layer switches it on at every open.
PRAGMA journal_mode = WAL Readers and the one writer do not block each other.
PRAGMA busy_timeout A second writer waits instead of failing.
PRAGMA trusted_schema = OFF A schema cannot run code the program did not write.

Because a program that opens the file in some other way would skip the foreign-key setting, every rule that can be written as a check, a unique key or a trigger is written that way, so it travels inside the file. Only the links between tables depend on the setting. A test (see 9.6) opens the file the wrong way and records exactly what still holds.

Five areas

SQLite has no schemas, so the area is the first part of a table's name.

Area Prefix Holds Rule
Blueprint blueprint_ The data dictionary: tables, fields, rules, lists, tags and terms The source of truth. Read by scripts at run time. Each row needs a definition and a purpose.
Reference ref_ Countries, statuses and the choice lists Loaded by migrations; never edited by hand.
Core core_ Verified music facts: artists, albums, collections, songs, videos, files, MTV pieces, artist relations Strict. Every rule applies, and a row cannot enter unless it satisfies them all.
Research research_ Data points, review items and their evidence Strict about format and links; may hold unverified candidates.
Staging staging_ Raw data as it arrived (the playlist read from YouTube, search results) before it is identified Loose by design. Nothing in staging counts as a fact.

A row moves from staging to core only through a promotion step. It checks the rules a row must meet to be a fact (for example, that its song is identified) and refuses the move if any fail. This is what lets the raw playlist load in one batch while the core tables stay strict.

The tables are named for the area and the dictionary name, such as core_Artists and ref_Countries.

Keys

  • Every table has a surrogate key, ID INTEGER PRIMARY KEY. It is meaningless and never changes, and all links use it, so renaming or correcting a real-world identifier never breaks a link.
  • Real-world identifiers are separate columns with unique keys (MusicBrainz IDs, the YouTube video ID, the vendor key). A blank identifier is NULL, never an empty string, and NULLs do not collide.
  • The YouTube video ID is unique across videos and MTV pieces together, enforced by a shared registry table that both write to.

How each rule becomes a constraint

Rule kind SQLite mechanism
Required NOT NULL
Unique UNIQUE constraint or unique index
Pattern CHECK built from length(), GLOB and NOT GLOB (see below), with a named constraint
Range CHECK (column BETWEEN a AND b)
List, Reference A foreign key to the reference table
Vendor format A CHECK generated from the vendor's characters, length and prefix. A vendor change is a migration.
Cross-field A table-level CHECK
Cross-row A trigger that raises an error, or a partial unique index when the rule is "at most one" (for example, one Primary video for each song)
Normalized A CHECK with a normalizing expression, or a trigger that refuses unclean text
Dates and times A CHECK that the value survives a round trip through date() or strftime() unchanged
Yes or no INTEGER NOT NULL CHECK (value IN (0, 1)) DEFAULT ...
Derived A generated column, or a view

Every table is declared STRICT, so a column holds only its declared type.

Patterns without regular expressions. SQLite has no regular-expression operator of its own. A program can register one, but then every other tool that opens the file would either fail or skip the check. So the schema uses only functions that are built into SQLite. A UUID is length(x) = 36 plus a GLOB of the exact shape. "Exactly 22 letters and digits" is length(x) = 22 AND x NOT GLOB '*[^A-Za-z0-9]*'. This is enough for every identifier format in the vendor list.

Constraint names follow one pattern, <table>_<column>_<kind>, so an error message names the exact rule that refused the value.

Audit trail

Every table in the core and research areas has Created_Time and Updated_Time, filled by triggers. A trigger copies the old row into an audit table on every update and delete, with the time and the program part that made the change. Nothing is changed or removed without a record that can undo it.

Access

SQLite has no database logins. What a person or program may do is decided by the application, which is the only writer. The database still refuses a wrong value however it arrives, because the rules are inside the file.

Derived fields

Fields the dictionary calls derived (a link built from an identifier, the expected file name, the album artist, the identity score and level) are not stored by hand. A value computed from the same row is a generated column. A value that needs other rows is a view.

What the generator produces

  1. The CREATE TABLE, constraint, index and trigger statements, written into migration files.
  2. A comment on every table and column carrying its dictionary name, definition and purpose.
  3. The reference-table loads, taken from 9.3.
  4. A constraint list that the tests use to check that every rule has a test.

The generator is deterministic: the same blueprint always produces the same files.

9.5 Migrations

A migration is one numbered script that moves the database from one known state to the next. The database is always the result of running every migration, in order, from empty. The structure is changed in no other way.

VeeJay uses Alembic, the migration tool that goes with SQLAlchemy. Because SQLite cannot alter most constraints in place, Alembic's batch mode rebuilds a table by creating the new one, copying the rows and swapping it in, all inside one transaction.

Rules

  1. Every change is a migration. Adding a table, a column, a constraint, a reference value or a trigger is a new script. Changing the structure by hand is not allowed.
  2. Migrations are ordered. Each one names the one before it, so there is exactly one history.
  3. A migration never changes after it has been applied. A mistake is fixed by a new migration. This keeps the history honest and the same everywhere.
  4. Each migration runs in one transaction. It either completes or leaves nothing behind.
  5. The database records what ran. Alembic keeps the current revision in the file. A migration whose text has changed since it ran stops the run.
  6. Forward only, with a copy first. A migration has no automatic undo. Before any migration runs against data, a full copy of the file is made with SQLite's backup function, and the run refuses to start without it. Rolling back means restoring that copy.
  7. The database can be rebuilt from nothing. Running every migration on an empty file must produce the current schema, with every reference table loaded. This is tested every time a migration is added.

The first migration is the only one written by hand

The first migration creates the blueprint tables, which must exist before anything can be generated from them. Every later migration is produced from the blueprint's rows. The blueprint_Drift view compares the blueprint with SQLite's own list of real columns, and a test fails if they disagree.

Where the files live

The migrations live in the veejay-app-code repository beside the generator and the tests, in version control, so a reader can see which blueprint change caused which migration.

How a change flows

  1. The change is made in the blueprint rows. Each change is audited.
  2. The generator compares the blueprint with the schema that the migrations have built so far, and writes the new migration for the difference. A file export of the blueprint is written beside it.
  3. A person reads the generated migration. Generated structure is reviewed like code.
  4. The copy is made, the migration is applied, and the tests run.
  5. The decision and its reason are added to the decision log.

Data migrations

A change that must move existing values (for example, splitting a field in two) is written inside one migration as a sequence: first the new structure is added, then the values are copied and verified, and only a later migration removes the old structure. Nothing is dropped in the same step that replaces it.

Two copies of the database

There are two: the working database, and a throwaway one that the tests rebuild from empty. Both are produced by the same migrations. A migration is applied to the throwaway one first.

9.6 Rules and tests

A rule is finished only when a test proves the database refuses what the rule forbids. This section holds the catalog of every rule and describes the tests that try to break each one.

The principle

For every rule there are at least three kinds of test:

Test What it does Passes when
Accepts valid Inserts a value that obeys the rule The row is stored
Refuses invalid Inserts a value that breaks the rule, in the most tempting wrong form The database refuses it, with the error that names this rule
Edges Inserts the boundary values: shortest, longest, empty, just inside and just outside Each is accepted or refused exactly as the rule says

A rule also has a fourth test when it can be broken by a change and not only by an insert: the same bad value is tried as an update, and as a bulk import. The wrong value must be refused by every route.

The tests are written with pytest and run against a throwaway file rebuilt from empty by the migrations.

The rule catalog

Each rule has a number, the field it protects, its kind, a statement, the tempting wrong value the test uses, and the name of its test. The number never changes. A rule that is withdrawn is marked Retired and is never reused.

Rule Protects Kind Statement Tempting wrong value Test
V-01 Artists.Origin_Country_Code Reference A code in the country table USS, US, usa, 123 t_country_code
V-02 Artists.Life_Status Cross-field Only for a Person A Group with Deceased t_life_status_person_only
V-03 Artists.Activity_Status Cross-field Only for a Group A Person with Active t_activity_status_group_only
V-04 Every vendor ID field Vendor format Satisfies its vendor row A 21-character Spotify ID; a lowercase Wikidata ID t_vendor_id_formats (one case per vendor)
V-05 YouTube_Video_ID Required, Unique, Pattern 11 allowed characters, unique across videos and MTV 10 characters; a space inside; the same ID in both tables t_youtube_id
V-06 Videos.Song Required A core video has a song A video with no song t_video_has_song
V-07 Songs Cross-row At most one Primary video A second Primary t_one_primary
V-08 Alternate videos Cross-row No audio file Audio attached to an Alternate t_alternate_no_audio
V-09 Files.File_Path Pattern Extension matches the kind A .mp3 marked Video t_file_path_kind
V-10 File_Hash_SHA256 Pattern 64 lowercase hexadecimal characters Uppercase; 63 characters t_file_hash
V-11 Year fields Range 1900 to 2100 1899, 2101, 0 t_year_range
V-12 Verified data points Cross-field Names the proving page Verified with no page t_verified_needs_evidence
V-13 Artist_Relations Cross-field No self relation; no duplicate pair and kind An artist related to itself t_relation_rules
V-14 Names and titles Normalized Clean spacing and characters A double space; a slash in a name t_name_clean
V-15 Choice fields List Only values of the reference list Orginal, a blank, Other t_choice_lists
V-16 Links between tables Reference The target row exists A song pointing at a missing album t_links_exist
V-17 All tables Audit Every update and delete leaves an audit row An update with no audit row t_audit_trail
V-18 Songs.ISRC Pattern Two letters, three letters or digits, seven digits 11 characters; a lowercase code; a hyphen inside t_isrc

The catalog grows with the blueprint. The generator writes the list of constraints it created, and a check compares that list with this catalog and fails if a constraint has no rule number or a rule has no test.

Coverage check

A coverage test runs after every migration and fails when:

  1. a constraint exists in the database that no rule in the catalog claims,
  2. a rule in the catalog has no test,
  3. a test is not wired to a rule, or
  4. a field that the dictionary marks Required or Unique has no constraint.

Tests of the process, not only the data

Test Passes when
Rebuild Applying every migration to an empty file reproduces the current schema exactly
Wrong way in The file is opened without the foreign-key setting, and every rule that is meant to hold inside the file still refuses its wrong value. The test records which rules depend on the setting, which is only the links between tables.
Promotion A staging row that breaks a core rule cannot be promoted, and the refusal names the rule
Screen A bad value typed into the application's screens is refused, and the screen shows the database's message
Import A bulk import of 1,000 rows with one bad value stores none of them

When a test fails

A failing test blocks the change. The fix is to repair the rule or the blueprint and regenerate. The test is never loosened to make it pass.

9.7 The database and the files

The database holds every fact about the library. VeeJay writes the file names, the tags in the files and the NFO files from it, so they cannot disagree with each other. A fact exists in one place, and the files are made from it.

When the database and a file disagree

The database wins. A file is made from the database, and nothing reads a tag back from a file into it.

VeeJay acts only when a database value changes. It then rewrites that one value in the file and leaves every other tag alone. It never scans the files by itself and never asks you anything about them. A tag you fix by hand in another tool therefore stays until the database changes that same value, and then the database's value replaces it.

Check files

The app has a Check files button. When you press it, VeeJay compares every file with the database and lists what differs. You can restore the differences in one click. Nothing happens unless you press the button.

Edits

An edit made in the database changes the files, the names and the NFO files that come from it. When a name changes, VeeJay renames the file and updates its path in the database. Changing an album's title or an artist's name renames the files of every song that depends on it.