aboutsummaryrefslogtreecommitdiffhomepage
path: root/services/app/src/Migrations/MigrationManager.php
diff options
context:
space:
mode:
Diffstat (limited to 'services/app/src/Migrations/MigrationManager.php')
-rw-r--r--services/app/src/Migrations/MigrationManager.php248
1 files changed, 248 insertions, 0 deletions
diff --git a/services/app/src/Migrations/MigrationManager.php b/services/app/src/Migrations/MigrationManager.php
new file mode 100644
index 0000000..88073db
--- /dev/null
+++ b/services/app/src/Migrations/MigrationManager.php
@@ -0,0 +1,248 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Albatross\Migrations;
+
+use Nsfisis\Albatross\Database\Connection;
+
+final class MigrationManager
+{
+ public function __construct(
+ private readonly Connection $conn,
+ ) {
+ }
+
+ public function execute(): void
+ {
+ $this->conn->transaction(function () {
+ $version = $this->fetchSchemaVersion();
+ while (method_exists($this, "execute$version")) {
+ $method = "execute$version";
+ // @phpstan-ignore-next-line
+ $this->$method();
+ $this->conn
+ ->query()
+ ->insert('migrations')
+ ->values([])
+ ->execute();
+ $version++;
+ }
+ });
+ }
+
+ private function fetchSchemaVersion(): int
+ {
+ $this->conn->query()->schema(<<<EOSQL
+ CREATE TABLE IF NOT EXISTS migrations (
+ migration_id SERIAL PRIMARY KEY
+ );
+ EOSQL);
+ $result = $this->conn
+ ->query()
+ ->select('migrations')
+ ->fields(['COALESCE(MAX(migration_id), 0) + 1 AS schema_version'])
+ ->first()
+ ->execute();
+ assert(isset($result['schema_version']));
+ return (int) $result['schema_version'];
+ }
+
+ /**
+ * Create the initial schema.
+ */
+ private function execute1(): void
+ {
+ $this->conn->query()->schema(<<<EOSQL
+ CREATE TABLE IF NOT EXISTS quizzes (
+ quiz_id SERIAL PRIMARY KEY,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ title TEXT NOT NULL,
+ summary TEXT NOT NULL,
+ input_description TEXT NOT NULL,
+ output_description TEXT NOT NULL,
+ expected_result TEXT NOT NULL,
+ example_code TEXT NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS answers (
+ answer_id SERIAL PRIMARY KEY,
+ quiz_id INTEGER NOT NULL,
+ answer_number INTEGER NOT NULL,
+ submitted_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ author_name TEXT NOT NULL,
+ code TEXT NOT NULL,
+ code_size INTEGER NOT NULL,
+ execution_status INTEGER NOT NULL,
+ execution_stdout TEXT,
+ execution_stderr TEXT,
+ UNIQUE (quiz_id, answer_number),
+ FOREIGN KEY (quiz_id) REFERENCES quizzes (quiz_id)
+ );
+ EOSQL);
+ }
+
+ /**
+ * Add "slug" column to quizzes table.
+ */
+ private function execute2(): void
+ {
+ $this->conn->query()->schema(<<<EOSQL
+ ALTER TABLE quizzes ADD COLUMN slug VARCHAR(32);
+ UPDATE quizzes SET slug = title;
+ ALTER TABLE quizzes ALTER COLUMN slug SET NOT NULL;
+ ALTER TABLE quizzes ADD CONSTRAINT uniq_slug UNIQUE (slug);
+ EOSQL);
+ }
+
+ /**
+ * Add "birdie_code_size" column to quizzes table.
+ */
+ private function execute3(): void
+ {
+ $this->conn->query()->schema(<<<EOSQL
+ ALTER TABLE quizzes ADD COLUMN birdie_code_size INTEGER;
+ EOSQL);
+ }
+
+ /**
+ * Remove "input_description" and "summary" columns from quizzes table. Rename "output_description" to "description".
+ */
+ private function execute4(): void
+ {
+ $this->conn->query()->schema(<<<EOSQL
+ ALTER TABLE quizzes DROP COLUMN input_description;
+ ALTER TABLE quizzes DROP COLUMN summary;
+ ALTER TABLE quizzes RENAME COLUMN output_description TO description;
+ EOSQL);
+ }
+
+ /**
+ * Add "started_at", "answers_hidden_at" and "finished_at" columns to quizzes table.
+ */
+ private function execute5(): void
+ {
+ $this->conn->query()->schema(<<<EOSQL
+ ALTER TABLE quizzes ADD COLUMN started_at TIMESTAMP;
+ ALTER TABLE quizzes ADD COLUMN answers_hidden_at TIMESTAMP;
+ ALTER TABLE quizzes ADD COLUMN finished_at TIMESTAMP;
+ EOSQL);
+ }
+
+ /**
+ * Create users table.
+ */
+ private function execute6(): void
+ {
+ $this->conn->query()->schema(<<<EOSQL
+ CREATE TABLE IF NOT EXISTS users (
+ user_id SERIAL PRIMARY KEY,
+ username VARCHAR(64) NOT NULL,
+ password_hash VARCHAR(256) NOT NULL,
+ is_admin BOOLEAN NOT NULL DEFAULT FALSE,
+ UNIQUE (username)
+ );
+ EOSQL);
+ }
+
+ /**
+ * Migrate from author_name to author_id in answers table.
+ */
+ private function execute7(): void
+ {
+ $this->conn->query()->schema(<<<EOSQL
+ ALTER TABLE answers DROP COLUMN author_name;
+ TRUNCATE TABLE answers;
+ ALTER TABLE answers ADD COLUMN author_id INTEGER NOT NULL;
+ ALTER TABLE answers ADD CONSTRAINT fk_author_id FOREIGN KEY (author_id) REFERENCES users (user_id);
+ EOSQL);
+ }
+
+ /**
+ * Rename answers_hidden_at to ranking_hidden_at in quizzes table.
+ */
+ private function execute8(): void
+ {
+ $this->conn->query()->schema(<<<EOSQL
+ ALTER TABLE quizzes RENAME COLUMN answers_hidden_at TO ranking_hidden_at;
+ EOSQL);
+ }
+
+ /**
+ * Make "started_at", "ranking_hidden_at" and "finished_at" columns not null.
+ */
+ private function execute9(): void
+ {
+ $this->conn->query()->schema(<<<EOSQL
+ UPDATE quizzes SET started_at = CURRENT_TIMESTAMP;
+ UPDATE quizzes SET ranking_hidden_at = CURRENT_TIMESTAMP;
+ UPDATE quizzes SET finished_at = CURRENT_TIMESTAMP;
+ ALTER TABLE quizzes ALTER COLUMN started_at SET NOT NULL;
+ ALTER TABLE quizzes ALTER COLUMN ranking_hidden_at SET NOT NULL;
+ ALTER TABLE quizzes ALTER COLUMN finished_at SET NOT NULL;
+ EOSQL);
+ }
+
+ /**
+ * Remove "password_hash" column from "users" table.
+ */
+ private function execute10(): void
+ {
+ $this->conn->query()->schema(<<<EOSQL
+ ALTER TABLE users DROP COLUMN password_hash;
+ EOSQL);
+ }
+
+ /**
+ * Implement multi-testcases.
+ *
+ * - Create "testcases" table and "testcase_executions" table.
+ * - Remove "expected_result" column from "quizzes" table.
+ * - Remove "execution_stdout" and "execution_stderr" columns from "answers" table.
+ */
+ private function execute11(): void
+ {
+ $this->conn->query()->schema(<<<EOSQL
+ CREATE TABLE IF NOT EXISTS testcases (
+ testcase_id SERIAL PRIMARY KEY,
+ quiz_id INTEGER NOT NULL,
+ input TEXT NOT NULL,
+ expected_result TEXT NOT NULL,
+ FOREIGN KEY (quiz_id) REFERENCES quizzes (quiz_id)
+ );
+ CREATE TABLE IF NOT EXISTS testcase_executions (
+ testcase_execution_id SERIAL PRIMARY KEY,
+ testcase_id INTEGER NOT NULL,
+ answer_id INTEGER NOT NULL,
+ status INTEGER NOT NULL,
+ stdout TEXT,
+ stderr TEXT,
+ FOREIGN KEY (testcase_id) REFERENCES testcases (testcase_id),
+ FOREIGN KEY (answer_id) REFERENCES answers (answer_id)
+ );
+
+ INSERT INTO testcases (quiz_id, input, expected_result)
+ SELECT quiz_id, '', expected_result FROM quizzes;
+ ALTER TABLE quizzes DROP COLUMN expected_result;
+
+ INSERT INTO testcase_executions (testcase_id, answer_id, status, stdout, stderr)
+ SELECT testcases.testcase_id, answers.answer_id, answers.execution_status, answers.execution_stdout, answers.execution_stderr
+ FROM answers
+ INNER JOIN testcases ON testcases.quiz_id = answers.quiz_id;
+ ALTER TABLE answers DROP COLUMN execution_stdout;
+ ALTER TABLE answers DROP COLUMN execution_stderr;
+ EOSQL);
+ }
+
+ /**
+ * Migrate value of "execution_status" in "answers" table.
+ */
+ private function execute12(): void
+ {
+ $this->conn->query()->schema(<<<EOSQL
+ UPDATE answers SET execution_status = 1 WHERE execution_status = 0 OR execution_status = 1;
+ UPDATE answers SET execution_status = 2 WHERE execution_status BETWEEN 2 AND 5;
+ UPDATE answers SET execution_status = 3 WHERE execution_status = 6;
+ EOSQL);
+ }
+}