From d46e9a3faf0ef8cbbceca97091fedc7c3a717367 Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Sat, 29 Mar 2025 20:46:35 +0100 Subject: [PATCH] Prepare for DB migartions --- packages/backend/src/database.ts | 229 ++++++++++++++++++------------- 1 file changed, 134 insertions(+), 95 deletions(-) diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index 553e210..4fe1cfa 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -1,5 +1,6 @@ import { Database as BunSqliteDatabase } from "bun:sqlite"; import { AttachmentId, PieceId, RepertoireId, RequestId, SessionId, Sha256_Bin, UserId } from "common"; +import { HashSet } from "effect"; import { ColumnType, CompiledQuery, CreateTableBuilder, Kysely, Selectable } from "kysely"; import { BunSqliteDialect } from "kysely-bun-sqlite"; @@ -15,9 +16,11 @@ export interface Database { Attachment: AttachmentTable; File: FileTable; Piece: PieceTable; + Option: OptionTable; Repertoire: RepertoireTable; RepertoireEntry: RepertoireEntryTable; Session: SessionTable; + sqlite_schema: SqliteSchemaTable; } export interface SystemInformation { @@ -52,6 +55,11 @@ export interface FileTable { data: ColumnType; } +export interface OptionTable { + key: ColumnType; + value: string; +} + export interface PieceData { name: string; composer: string | null; @@ -91,13 +99,23 @@ export interface SessionTable extends SessionData { expiresAt: string; } +export interface SqliteSchemaTable { + type: ColumnType; + name: ColumnType; + tbl_name: ColumnType; + rootpage: ColumnType; + sql: ColumnType; +} + export type AccessLog = Selectable; export type Attachment = Selectable; export type File = Selectable; +export type Option = Selectable; export type Piece = Selectable; export type Repertoire = Selectable; export type RepertoireEntry = Selectable; export type Session = Selectable; +export type SqliteSchema = Selectable; function systemInformation(schema: CreateTableBuilder) { return schema @@ -115,112 +133,133 @@ export async function initDatabase(filename: string = "db.sqlite3"): Promise c.notNull().primaryKey()) - .addColumn("timestamp", "text", (c) => c.notNull()) - .addColumn("method", "text", (c) => c.notNull()) - .addColumn("pathname", "text", (c) => c.notNull()) - .addColumn("query", "text", (c) => c.notNull()) - .addColumn("ip", "text") - .execute(); + const tables = await db + .selectFrom("sqlite_schema") + .select("name") + .where("type", "=", "table") + .execute() + .then((tables) => HashSet.make(...tables.map(({ name }) => name))); - await db.schema - .createIndex("AccessLog_timestamp") - .ifNotExists() - .on("AccessLog") - .column("timestamp") - .execute(); + if (!HashSet.has(tables, "Option")) { + await db.schema + .createTable("AccessLog") + .ifNotExists() + .addColumn("requestId", "text", (c) => c.notNull().primaryKey()) + .addColumn("timestamp", "text", (c) => c.notNull()) + .addColumn("method", "text", (c) => c.notNull()) + .addColumn("pathname", "text", (c) => c.notNull()) + .addColumn("query", "text", (c) => c.notNull()) + .addColumn("ip", "text") + .execute(); - await db.schema - .createTable("File") - .ifNotExists() - .addColumn("sha256", "blob", (c) => c.notNull().primaryKey()) - .addColumn("data", "blob", (c) => c.notNull()) - .execute(); + await db.schema + .createIndex("AccessLog_timestamp") + .ifNotExists() + .on("AccessLog") + .column("timestamp") + .execute(); - await db.schema - .createTable("User") - .ifNotExists() - .addColumn("userId", "text", (c) => c.notNull().primaryKey()) - .addColumn("username", "text", (c) => c.notNull().unique()) - .addColumn("password", "text", (c) => c.notNull()) - .addColumn("admin", "boolean", (c) => c.notNull()) - .execute(); + await db.schema + .createTable("File") + .ifNotExists() + .addColumn("sha256", "blob", (c) => c.notNull().primaryKey()) + .addColumn("data", "blob", (c) => c.notNull()) + .execute(); - await db.schema - .createTable("Piece") - .ifNotExists() - .addColumn("pieceId", "text", (c) => c.notNull().primaryKey()) - .addColumn("name", "text", (c) => c.notNull()) - .addColumn("composer", "text") - .addColumn("lyricist", "text") - .addColumn("arranger", "text") - .$call(systemInformation) - .execute(); + await db.schema + .createTable("User") + .ifNotExists() + .addColumn("userId", "text", (c) => c.notNull().primaryKey()) + .addColumn("username", "text", (c) => c.notNull().unique()) + .addColumn("password", "text", (c) => c.notNull()) + .addColumn("admin", "boolean", (c) => c.notNull()) + .execute(); - await db.schema - .createIndex("Piece_name_composer_arranger") - .ifNotExists() - .on("Piece") - .columns(["name", "composer", "arranger"]) - .execute(); + await db.schema + .createTable("Piece") + .ifNotExists() + .addColumn("pieceId", "text", (c) => c.notNull().primaryKey()) + .addColumn("name", "text", (c) => c.notNull()) + .addColumn("composer", "text") + .addColumn("lyricist", "text") + .addColumn("arranger", "text") + .$call(systemInformation) + .execute(); - await db.schema - .createTable("Repertoire") - .ifNotExists() - .addColumn("repertoireId", "text", (c) => c.notNull().primaryKey()) - .addColumn("name", "text", (c) => c.notNull()) - .$call(systemInformation) - .execute(); + await db.schema + .createIndex("Piece_name_composer_arranger") + .ifNotExists() + .on("Piece") + .columns(["name", "composer", "arranger"]) + .execute(); - await db.schema - .createIndex("Repertoire_name") - .ifNotExists() - .on("Repertoire") - .column("name") - .execute(); + await db.schema + .createTable("Repertoire") + .ifNotExists() + .addColumn("repertoireId", "text", (c) => c.notNull().primaryKey()) + .addColumn("name", "text", (c) => c.notNull()) + .$call(systemInformation) + .execute(); - await db.schema - .createTable("RepertoireEntry") - .ifNotExists() - .addColumn("repertoireId", "text", (c) => c.notNull().references("Repertoire.repertoireId").onDelete("cascade").onUpdate("cascade")) - .addColumn("order", "integer", (c) => c.notNull()) - .addColumn("pieceId", "text", (c) => c.notNull().references("Piece.pieceId").onDelete("restrict").onUpdate("restrict")) - .addPrimaryKeyConstraint("pk_RepertoireEntry", ["repertoireId", "order"]) - .execute(); + await db.schema + .createIndex("Repertoire_name") + .ifNotExists() + .on("Repertoire") + .column("name") + .execute(); - await db.schema - .createTable("Session") - .ifNotExists() - .addColumn("sessionId", "text", (c) => c.notNull().primaryKey()) - .addColumn("state", "text") - .addColumn("codeVerifier", "text") - .addColumn("accessToken", "text") - .addColumn("idToken", "text") - .addColumn("refreshToken", "text") - .addColumn("external", "boolean") - .addColumn("expiresAt", "text", (c) => c.notNull()) - .execute(); + await db.schema + .createTable("RepertoireEntry") + .ifNotExists() + .addColumn("repertoireId", "text", (c) => c.notNull().references("Repertoire.repertoireId").onDelete("cascade").onUpdate("cascade")) + .addColumn("order", "integer", (c) => c.notNull()) + .addColumn("pieceId", "text", (c) => c.notNull().references("Piece.pieceId").onDelete("restrict").onUpdate("restrict")) + .addPrimaryKeyConstraint("pk_RepertoireEntry", ["repertoireId", "order"]) + .execute(); - await db.schema - .createTable("Attachment") - .ifNotExists() - .addColumn("attachmentId", "text", (c) => c.notNull().primaryKey()) - .addColumn("pieceId", "text", (c) => c.notNull().references("Piece.pieceId").onDelete("cascade").onUpdate("cascade")) - .addColumn("sha256", "blob", (c) => c.notNull().references("File.sha256").onDelete("restrict").onUpdate("restrict")) - .addColumn("filename", "text", (c) => c.notNull()) - .addColumn("mediaType", "text", (c) => c.notNull()) - .$call(systemInformation) - .execute(); + await db.schema + .createTable("Session") + .ifNotExists() + .addColumn("sessionId", "text", (c) => c.notNull().primaryKey()) + .addColumn("state", "text") + .addColumn("codeVerifier", "text") + .addColumn("accessToken", "text") + .addColumn("idToken", "text") + .addColumn("refreshToken", "text") + .addColumn("external", "boolean") + .addColumn("expiresAt", "text", (c) => c.notNull()) + .execute(); - await db.schema - .createIndex("Attachment_pieceId_filename") - .ifNotExists() - .on("Attachment") - .columns(["pieceId", "filename"]) - .execute(); + await db.schema + .createTable("Attachment") + .ifNotExists() + .addColumn("attachmentId", "text", (c) => c.notNull().primaryKey()) + .addColumn("pieceId", "text", (c) => c.notNull().references("Piece.pieceId").onDelete("cascade").onUpdate("cascade")) + .addColumn("sha256", "blob", (c) => c.notNull().references("File.sha256").onDelete("restrict").onUpdate("restrict")) + .addColumn("filename", "text", (c) => c.notNull()) + .addColumn("mediaType", "text", (c) => c.notNull()) + .$call(systemInformation) + .execute(); + + await db.schema + .createIndex("Attachment_pieceId_filename") + .ifNotExists() + .on("Attachment") + .columns(["pieceId", "filename"]) + .execute(); + + await db.schema + .createTable("Option") + .ifNotExists() + .addColumn("key", "text", (c) => c.notNull().primaryKey()) + .addColumn("value", "text", (c) => c.notNull()) + .execute(); + + await db + .insertInto("Option") + .values({ key: "database_version", value: "0" }) + .execute(); + } return db; }