From 46aedfd2b92ba75b8f0dc1e92e3fb7fcda30da15 Mon Sep 17 00:00:00 2001 From: eth0net Date: Wed, 16 Sep 2026 21:42:16 +0100 Subject: [PATCH] test: check every schema against one the reference built Running the reference's own migrator produces the four databases; its schema is vendored and compared against ours. It caught the ledger DDL, which said the same thing in a different order. Signed-off-by: eth0net --- src/store/db.rs | 4 +- tests/fixtures/README.md | 7 +++ tests/fixtures/schema/account.sql | 30 +++++++++++ tests/fixtures/schema/actor.sql | 15 ++++++ tests/fixtures/schema/did_cache.sql | 3 ++ tests/fixtures/schema/sequencer.sql | 6 +++ tests/store.rs | 83 +++++++++++++++++++++++++++++ 7 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/schema/account.sql create mode 100644 tests/fixtures/schema/actor.sql create mode 100644 tests/fixtures/schema/did_cache.sql create mode 100644 tests/fixtures/schema/sequencer.sql diff --git a/src/store/db.rs b/src/store/db.rs index 8a4a863..1996c6d 100644 --- a/src/store/db.rs +++ b/src/store/db.rs @@ -9,8 +9,8 @@ use super::Error; /// The ledger the reference's migrator keeps. Writing it is what lets that /// server open the same file and agree it is already migrated. const LEDGER: &str = r#" -create table if not exists "kysely_migration" ("name" varchar(255) primary key not null, "timestamp" varchar(255) not null); -create table if not exists "kysely_migration_lock" ("id" varchar(255) primary key not null, "is_locked" integer default 0 not null); +create table if not exists "kysely_migration" ("name" varchar(255) not null primary key, "timestamp" varchar(255) not null); +create table if not exists "kysely_migration_lock" ("id" varchar(255) not null primary key, "is_locked" integer default 0 not null); insert or ignore into "kysely_migration_lock" ("id", "is_locked") values ('migration_lock', 0); "#; diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md index 3372017..59dfc76 100644 --- a/tests/fixtures/README.md +++ b/tests/fixtures/README.md @@ -14,6 +14,13 @@ the private scalar differently: hex for K-256, base58 for P-256. `car/` is `packages/repo/tests/`, where a CAR file sits beside the blocks it was written from. +`schema/` is what each of the reference's four databases looks like once its +own migrator has run: `sqlite3 .schema`, sorted, with `sqlite_sequence` +dropped because SQLite writes that one itself. Regenerating it means running +those migrations, which needs `kysely` and `better-sqlite3` and a copy of +`packages/pds/src/*/db/migrations/`; the account set also imports two runtime +values that only a data-cleanup `where` clause uses, so they can be stubbed. + Those files are under the same `MIT OR Apache-2.0` terms as this repository. [atproto]: https://github.com/bluesky-social/atproto diff --git a/tests/fixtures/schema/account.sql b/tests/fixtures/schema/account.sql new file mode 100644 index 0000000..faeb401 --- /dev/null +++ b/tests/fixtures/schema/account.sql @@ -0,0 +1,30 @@ +CREATE INDEX "account_device_did_idx" on "account_device" ("did"); +CREATE INDEX "actor_cursor_idx" on "actor" ("createdAt", "did"); +CREATE INDEX "authorization_request_expires_at_idx" on "authorization_request" ("expiresAt"); +CREATE INDEX "invite_code_for_account_idx" on "invite_code" ("forAccount"); +CREATE INDEX "lexicon_failures_idx" on "lexicon" ("updatedAt" DESC) WHERE ("lexicon" is NULL); +CREATE INDEX "refresh_token_did_idx" on "refresh_token" ("did"); +CREATE INDEX "token_did_idx" on "token" ("did"); +CREATE INDEX "used_refresh_token_id_idx" on "used_refresh_token" ("tokenId"); +CREATE TABLE "account_device" ("did" varchar not null, "deviceId" varchar not null, "createdAt" varchar not null, "updatedAt" varchar not null, constraint "account_device_pk" primary key ("deviceId", "did"), constraint "account_device_did_fk" foreign key ("did") references "account" ("did") on delete cascade on update cascade, constraint "account_device_device_id_fk" foreign key ("deviceId") references "device" ("id") on delete cascade on update cascade); +CREATE TABLE "account" ("did" varchar primary key, "email" varchar not null, "passwordScrypt" varchar not null, "emailConfirmedAt" varchar, "invitesDisabled" int2 default 0 not null); +CREATE TABLE "actor" ("did" varchar primary key, "handle" varchar, "createdAt" varchar not null, "takedownRef" varchar, "deactivatedAt" varchar, "deleteAfter" varchar); +CREATE TABLE "app_password" ("did" varchar not null, "name" varchar not null, "passwordScrypt" varchar not null, "createdAt" varchar not null, "privileged" integer default 0 not null, constraint "app_password_pkey" primary key ("did", "name")); +CREATE TABLE "authorization_request" ("id" varchar primary key, "did" varchar, "deviceId" varchar, "clientId" varchar not null, "clientAuth" varchar not null, "parameters" varchar not null, "expiresAt" varchar not null, "code" varchar); +CREATE TABLE "authorized_client" ("did" varchar not null, "clientId" varchar not null, "createdAt" varchar not null, "updatedAt" varchar not null, "data" varchar not null, constraint "authorized_client_pk" primary key ("did", "clientId"), constraint "authorized_client_did_fk" foreign key ("did") references "account" ("did") on delete cascade on update cascade); +CREATE TABLE "device_account" ("did" varchar not null, "deviceId" varchar not null, "authenticatedAt" varchar not null, "remember" boolean not null, "authorizedClients" varchar not null, constraint "device_account_pk" primary key ("deviceId", "did"), constraint "device_account_device_id_fk" foreign key ("deviceId") references "device" ("id") on delete cascade on update cascade); +CREATE TABLE "device" ("id" varchar primary key, "sessionId" varchar not null, "userAgent" varchar, "ipAddress" varchar not null, "lastSeenAt" varchar not null, constraint "device_session_id_idx" unique ("sessionId")); +CREATE TABLE "email_token" ("purpose" varchar not null, "did" varchar not null, "token" varchar not null, "requestedAt" varchar not null, constraint "email_token_pkey" primary key ("purpose", "did"), constraint "email_token_purpose_token_unique" unique ("purpose", "token")); +CREATE TABLE "invite_code_use" ("code" varchar not null, "usedBy" varchar not null, "usedAt" varchar not null, constraint "invite_code_use_pkey" primary key ("code", "usedBy")); +CREATE TABLE "invite_code" ("code" varchar primary key, "availableUses" integer not null, "disabled" int2 default 0, "forAccount" varchar not null, "createdBy" varchar not null, "createdAt" varchar not null); +CREATE TABLE "kysely_migration_lock" ("id" varchar(255) not null primary key, "is_locked" integer default 0 not null); +CREATE TABLE "kysely_migration" ("name" varchar(255) not null primary key, "timestamp" varchar(255) not null); +CREATE TABLE "lexicon" ("nsid" varchar primary key, "createdAt" varchar not null, "updatedAt" varchar not null, "lastSucceededAt" varchar, "uri" varchar, "lexicon" varchar); +CREATE TABLE "refresh_token" ("id" varchar primary key, "did" varchar not null, "expiresAt" varchar not null, "nextId" varchar, "appPasswordName" varchar); +CREATE TABLE "repo_root" ("did" varchar primary key, "cid" varchar not null, "rev" varchar not null, "indexedAt" varchar not null); +CREATE TABLE "token" ("id" integer primary key autoincrement, "did" varchar not null, "tokenId" varchar not null, "createdAt" varchar not null, "updatedAt" varchar not null, "expiresAt" varchar not null, "clientId" varchar not null, "clientAuth" varchar not null, "deviceId" varchar, "parameters" varchar not null, "details" varchar, "code" varchar, "currentRefreshToken" varchar, "scope" varchar, constraint "token_current_refresh_token_unique_idx" unique ("currentRefreshToken"), constraint "token_id_unique_idx" unique ("tokenId")); +CREATE TABLE "used_refresh_token" ("refreshToken" varchar primary key, "tokenId" integer not null, constraint "used_refresh_token_fk" foreign key ("tokenId") references "token" ("id") on delete cascade on update cascade); +CREATE UNIQUE INDEX "account_email_lower_idx" on "account" (lower("email")); +CREATE UNIQUE INDEX "actor_handle_lower_idx" on "actor" (lower("handle")); +CREATE UNIQUE INDEX "authorization_request_code_idx" on "authorization_request" (code DESC) WHERE (code IS NOT NULL); +CREATE UNIQUE INDEX "token_code_idx" on "token" (code DESC) WHERE (code IS NOT NULL); diff --git a/tests/fixtures/schema/actor.sql b/tests/fixtures/schema/actor.sql new file mode 100644 index 0000000..587f200 --- /dev/null +++ b/tests/fixtures/schema/actor.sql @@ -0,0 +1,15 @@ +CREATE INDEX "backlink_link_to_idx" on "backlink" ("path", "linkTo"); +CREATE INDEX "blob_tempkey_idx" on "blob" ("tempKey"); +CREATE INDEX "record_cid_idx" on "record" ("cid"); +CREATE INDEX "record_collection_idx" on "record" ("collection"); +CREATE INDEX "record_repo_rev_idx" on "record" ("repoRev"); +CREATE INDEX "repo_block_repo_rev_idx" on "repo_block" ("repoRev", "cid"); +CREATE TABLE "account_pref" ("id" integer primary key autoincrement, "name" varchar not null, "valueJson" text not null); +CREATE TABLE "backlink" ("uri" varchar not null, "path" varchar not null, "linkTo" varchar not null, constraint "backlinks_pkey" primary key ("uri", "path")); +CREATE TABLE "blob" ("cid" varchar primary key, "mimeType" varchar not null, "size" integer not null, "tempKey" varchar, "width" integer, "height" integer, "createdAt" varchar not null, "takedownRef" varchar); +CREATE TABLE "kysely_migration_lock" ("id" varchar(255) not null primary key, "is_locked" integer default 0 not null); +CREATE TABLE "kysely_migration" ("name" varchar(255) not null primary key, "timestamp" varchar(255) not null); +CREATE TABLE "record_blob" ("blobCid" varchar not null, "recordUri" varchar not null, constraint "record_blob_pkey" primary key ("blobCid", "recordUri")); +CREATE TABLE "record" ("uri" varchar primary key, "cid" varchar not null, "collection" varchar not null, "rkey" varchar not null, "repoRev" varchar not null, "indexedAt" varchar not null, "takedownRef" varchar); +CREATE TABLE "repo_block" ("cid" varchar primary key, "repoRev" varchar not null, "size" integer not null, "content" blob not null); +CREATE TABLE "repo_root" ("did" varchar primary key, "cid" varchar not null, "rev" varchar not null, "indexedAt" varchar not null); diff --git a/tests/fixtures/schema/did_cache.sql b/tests/fixtures/schema/did_cache.sql new file mode 100644 index 0000000..463ad1c --- /dev/null +++ b/tests/fixtures/schema/did_cache.sql @@ -0,0 +1,3 @@ +CREATE TABLE "did_doc" ("did" varchar primary key, "doc" text not null, "updatedAt" bigint not null); +CREATE TABLE "kysely_migration_lock" ("id" varchar(255) not null primary key, "is_locked" integer default 0 not null); +CREATE TABLE "kysely_migration" ("name" varchar(255) not null primary key, "timestamp" varchar(255) not null); diff --git a/tests/fixtures/schema/sequencer.sql b/tests/fixtures/schema/sequencer.sql new file mode 100644 index 0000000..3d608b9 --- /dev/null +++ b/tests/fixtures/schema/sequencer.sql @@ -0,0 +1,6 @@ +CREATE INDEX "repo_seq_did_idx" on "repo_seq" ("did"); +CREATE INDEX "repo_seq_event_type_idx" on "repo_seq" ("eventType"); +CREATE INDEX "repo_seq_sequenced_at_index" on "repo_seq" ("sequencedAt"); +CREATE TABLE "kysely_migration_lock" ("id" varchar(255) not null primary key, "is_locked" integer default 0 not null); +CREATE TABLE "kysely_migration" ("name" varchar(255) not null primary key, "timestamp" varchar(255) not null); +CREATE TABLE "repo_seq" ("seq" integer primary key autoincrement, "did" varchar not null, "eventType" varchar not null, "event" blob not null, "invalidated" int2 default 0 not null, "sequencedAt" varchar not null); diff --git a/tests/store.rs b/tests/store.rs index 9aedd86..8110b66 100644 --- a/tests/store.rs +++ b/tests/store.rs @@ -490,3 +490,86 @@ fn a_signing_key_survives_the_file_it_is_kept_in() { assert_eq!(mode & 0o777, 0o600, "nobody else reads a signing key"); } } + +/// The schema a database ends up with, in the form the fixtures hold. +fn schema(path: &std::path::Path) -> Vec { + let db = Connection::open(path).expect("opens"); + let mut objects: Vec = db + .prepare(r#"select "sql" from "sqlite_master" where "sql" is not null"#) + .expect("prepares") + .query_map([], |row| row.get::<_, String>(0)) + .expect("queries") + .collect::>() + .expect("reads"); + objects.retain(|sql| !sql.starts_with("CREATE TABLE sqlite_sequence")); + objects.sort(); + objects +} + +fn reference_schema(name: &str) -> Vec { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/schema") + .join(format!("{name}.sql")); + let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())); + // `.schema` ends each statement with a semicolon; `sqlite_master` does not. + let mut objects: Vec = text + .lines() + .map(|line| line.trim_end_matches(';').to_owned()) + .collect(); + objects.sort(); + objects +} + +#[test] +fn every_schema_is_the_one_the_reference_builds() { + let home = tempfile::tempdir().expect("a temporary directory"); + let at = |name: &str| home.path().join(format!("{name}.sqlite")); + + Actor::open(&at("actor"), account()).expect("opens"); + Sequencer::open(&at("sequencer")).expect("opens"); + DidCache::open(&at("did_cache")).expect("opens"); + for name in ["actor", "sequencer", "did_cache"] { + assert_eq!(schema(&at(name)), reference_schema(name), "{name}"); + } + + // The account database stops at 003; 004 to 007 are the OAuth tables. + // Everything it does build has to be what the reference built. + Accounts::open(&at("account")).expect("opens"); + let ours = schema(&at("account")); + let theirs = reference_schema("account"); + for object in &ours { + assert!(theirs.contains(object), "not the reference's: {object}"); + } + let mut missing: Vec<&str> = theirs + .iter() + .filter(|object| !ours.contains(object)) + .map(|object| { + object + .split('"') + .nth(1) + .expect("a quoted name in every statement") + }) + .collect(); + missing.sort_unstable(); + assert_eq!( + missing, + [ + "account_device", + "account_device_did_idx", + "authorization_request", + "authorization_request_code_idx", + "authorization_request_expires_at_idx", + "authorized_client", + "device", + "device_account", + "lexicon", + "lexicon_failures_idx", + "token", + "token_code_idx", + "token_did_idx", + "used_refresh_token", + "used_refresh_token_id_idx", + ], + "what migrations 004 to 007 would add" + ); +} -- 2.54.0