db-schema.unit.test.ts
| 1 | import { Database } from "bun:sqlite"; |
| 2 | import { describe, expect, test } from "bun:test"; |
| 3 | import { readFileSync } from "node:fs"; |
| 4 | import path from "node:path"; |
| 5 | |
| 6 | const SCHEMA = readFileSync( |
| 7 | path.resolve(import.meta.dir, "../src/db/schema.sql"), |
| 8 | "utf-8", |
| 9 | ); |
| 10 | |
| 11 | function open() { |
| 12 | const db = new Database(":memory:"); |
| 13 | db.run("PRAGMA foreign_keys=ON"); |
| 14 | return db; |
| 15 | } |
| 16 | |
| 17 | function tables(db: Database): string[] { |
| 18 | return db |
| 19 | .query<{ name: string }, []>( |
| 20 | "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name", |
| 21 | ) |
| 22 | .all() |
| 23 | .map((r) => r.name); |
| 24 | } |
| 25 | |
| 26 | function columns(db: Database, table: string): string[] { |
| 27 | return db |
| 28 | .query<{ name: string }, []>(`PRAGMA table_info(${table})`) |
| 29 | .all() |
| 30 | .map((r) => r.name) |
| 31 | .sort(); |
| 32 | } |
| 33 | |
| 34 | describe("schema.sql", () => { |
| 35 | test("creates every table the Kysely Database interface declares", () => { |
| 36 | const db = open(); |
| 37 | db.run(SCHEMA); |
| 38 | expect(tables(db)).toEqual([ |
| 39 | "ci_artifacts", |
| 40 | "ci_run_counters", |
| 41 | "ci_runs", |
| 42 | "ci_secrets", |
| 43 | "ci_steps", |
| 44 | "issue_comments", |
| 45 | "issue_labels", |
| 46 | "issue_reactions", |
| 47 | "issues", |
| 48 | "labels", |
| 49 | "passkeys", |
| 50 | "patch_comments", |
| 51 | "patch_labels", |
| 52 | "patch_reactions", |
| 53 | "patches", |
| 54 | "release_assets", |
| 55 | "releases", |
| 56 | "repositories", |
| 57 | "sessions", |
| 58 | "ssh_keys", |
| 59 | "users", |
| 60 | ]); |
| 61 | db.close(); |
| 62 | }); |
| 63 | |
| 64 | test("includes the columns that used to be added by ALTER migrations", () => { |
| 65 | const db = open(); |
| 66 | db.run(SCHEMA); |
| 67 | expect(columns(db, "users")).toContain("is_pending"); |
| 68 | expect(columns(db, "users")).toContain("register_application"); |
| 69 | expect(columns(db, "patches")).toContain("version"); |
| 70 | expect(columns(db, "repositories")).toContain("allow_user_labels"); |
| 71 | expect(columns(db, "ci_runs")).toContain("repo_run_id"); |
| 72 | db.close(); |
| 73 | }); |
| 74 | |
| 75 | test("creates the query indexes", () => { |
| 76 | const db = open(); |
| 77 | db.run(SCHEMA); |
| 78 | const idx = db |
| 79 | .query<{ name: string }, []>( |
| 80 | "SELECT name FROM sqlite_master WHERE type='index' AND name LIKE 'idx_%'", |
| 81 | ) |
| 82 | .all() |
| 83 | .map((r) => r.name); |
| 84 | expect(idx).toContain("idx_issues_repo_status"); |
| 85 | expect(idx).toContain("idx_patches_repo_status"); |
| 86 | expect(idx).toContain("idx_sessions_expires_at"); |
| 87 | expect(idx).toContain("idx_ci_runs_repo_id"); |
| 88 | db.close(); |
| 89 | }); |
| 90 | |
| 91 | test("is idempotent — re-running changes nothing", () => { |
| 92 | const db = open(); |
| 93 | db.run(SCHEMA); |
| 94 | const before = tables(db); |
| 95 | const userCols = columns(db, "users"); |
| 96 | db.run(SCHEMA); |
| 97 | db.run(SCHEMA); |
| 98 | expect(tables(db)).toEqual(before); |
| 99 | expect(columns(db, "users")).toEqual(userCols); |
| 100 | db.close(); |
| 101 | }); |
| 102 | |
| 103 | test("applies cleanly over a pre-CI database, as an upgrade would", () => { |
| 104 | const db = open(); |
| 105 | // Only the oldest core tables, mimicking a database created before |
| 106 | // the CI and label features existed. |
| 107 | db.run(`CREATE TABLE users ( |
| 108 | id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 109 | username TEXT NOT NULL UNIQUE, |
| 110 | password_hash TEXT, |
| 111 | created_at TEXT NOT NULL, |
| 112 | avatar_version INTEGER NOT NULL DEFAULT 0 |
| 113 | )`); |
| 114 | db.run(SCHEMA); |
| 115 | expect(tables(db)).toContain("ci_runs"); |
| 116 | expect(tables(db)).toContain("labels"); |
| 117 | // The pre-existing table keeps its old shape; index.ts ALTERs the |
| 118 | // missing columns in. |
| 119 | expect(columns(db, "users")).not.toContain("is_pending"); |
| 120 | db.close(); |
| 121 | }); |
| 122 | }); |
| 123 |