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:
- 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.
- 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¶
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- Provenance for every researched value. The source of each researched value is recorded, so bad data can be traced to where it came from.
- 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_IDand is stored (Spotify_Artist_ID,MusicBrainz_Release_Group_ID). A link to it isVendor_Entity_URLand 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_SHA256for a hash. A yes or no field starts withIs_orHas_. 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
_Keynever 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_TagandMP3_Tagname the tag the field is written to in each file format, or hold the wordNonewhen 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 | 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 | 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¶
- A list changes only through a migration, never by editing a row in the application's screens.
- A value is never reused for a different meaning. A value that turns out wrong is retired and a new one added.
- 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.
- The reference tables are loaded before any data and are the same in every copy of the database.
- 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, andNULLs 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¶
- The
CREATE TABLE, constraint, index and trigger statements, written into migration files. - A comment on every table and column carrying its dictionary name, definition and purpose.
- The reference-table loads, taken from 9.3.
- 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¶
- 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.
- Migrations are ordered. Each one names the one before it, so there is exactly one history.
- 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.
- Each migration runs in one transaction. It either completes or leaves nothing behind.
- 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.
- 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.
- 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¶
- The change is made in the blueprint rows. Each change is audited.
- 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.
- A person reads the generated migration. Generated structure is reviewed like code.
- The copy is made, the migration is applied, and the tests run.
- 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:
- a constraint exists in the database that no rule in the catalog claims,
- a rule in the catalog has no test,
- a test is not wired to a rule, or
- 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.