From 82c6b364af42282e98d14d543e1d21f7edd5134f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 21:41:04 +0100 Subject: [PATCH] fix(mysql): fix domain_expiry migration for MySQL 8.0 compatibility (#6612) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: CommanderStorm <26258709+CommanderStorm@users.noreply.github.com> --- .../2025-09-02-0000-add-domain-expiry.js | 4 +- ...1-06-0000-fix-domain-expiry-column-type.js | 14 ++++ package-lock.json | 58 +++++++++++++++++ package.json | 1 + test/backend-test/test-migration.js | 65 +++++++++++++++++++ 5 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 db/knex_migrations/2026-01-06-0000-fix-domain-expiry-column-type.js diff --git a/db/knex_migrations/2025-09-02-0000-add-domain-expiry.js b/db/knex_migrations/2025-09-02-0000-add-domain-expiry.js index ede2e1889..b204a44d3 100644 --- a/db/knex_migrations/2025-09-02-0000-add-domain-expiry.js +++ b/db/knex_migrations/2025-09-02-0000-add-domain-expiry.js @@ -6,7 +6,9 @@ exports.up = function (knex) { .createTable("domain_expiry", (table) => { table.increments("id"); table.datetime("last_check"); - table.text("domain").unique().notNullable(); + // Use VARCHAR(255) for MySQL/MariaDB compatibility with unique constraint + // Maximum domain name length is 253 characters (255 octets on the wire) + table.string("domain", 255).unique().notNullable(); table.datetime("expiry"); table.integer("last_expiry_notification_sent").defaultTo(null); }); diff --git a/db/knex_migrations/2026-01-06-0000-fix-domain-expiry-column-type.js b/db/knex_migrations/2026-01-06-0000-fix-domain-expiry-column-type.js new file mode 100644 index 000000000..6728c5fb8 --- /dev/null +++ b/db/knex_migrations/2026-01-06-0000-fix-domain-expiry-column-type.js @@ -0,0 +1,14 @@ +// Ensure domain column is VARCHAR(255) across all database types. +// This migration ensures MySQL, SQLite, and MariaDB have consistent column type, +// even if a user installed 2.1.0-beta.0 or 2.1.0-beta.1 which had TEXT type for this column. +// Maximum domain name length is 253 characters (255 octets on the wire). +// Note: The unique constraint is already present from the original migration. +exports.up = function (knex) { + return knex.schema.alterTable("domain_expiry", function (table) { + table.string("domain", 255).notNullable().alter(); + }); +}; + +exports.down = function (knex) { + // No rollback needed - keeping VARCHAR(255) is the correct state +}; diff --git a/package-lock.json b/package-lock.json index 3c7d62865..90c9a3969 100644 --- a/package-lock.json +++ b/package-lock.json @@ -103,6 +103,7 @@ "@testcontainers/hivemq": "^10.13.1", "@testcontainers/mariadb": "^10.13.0", "@testcontainers/mssqlserver": "^10.28.0", + "@testcontainers/mysql": "^11.11.0", "@testcontainers/postgresql": "^11.9.0", "@testcontainers/rabbitmq": "^10.13.2", "@types/bootstrap": "~5.1.9", @@ -5665,6 +5666,63 @@ "testcontainers": "^10.28.0" } }, + "node_modules/@testcontainers/mysql": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@testcontainers/mysql/-/mysql-11.11.0.tgz", + "integrity": "sha512-2EfFhUDEvEdwBwez+F/NhqP+h2rFzLzHYbRX0N/9/Lgdlq8TbsYWZ9SaWL9V0f1FWX89XnyZrT3i/j7m8MIESg==", + "dev": true, + "license": "MIT", + "dependencies": { + "testcontainers": "^11.11.0" + } + }, + "node_modules/@testcontainers/mysql/node_modules/docker-compose": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-1.3.0.tgz", + "integrity": "sha512-7Gevk/5eGD50+eMD+XDnFnOrruFkL0kSd7jEG4cjmqweDSUhB7i0g8is/nBdVpl+Bx338SqIB2GLKm32M+Vs6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@testcontainers/mysql/node_modules/testcontainers": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-11.11.0.tgz", + "integrity": "sha512-nKTJn3n/gkyGg/3SVkOwX+isPOGSHlfI+CWMobSmvQrsj7YW01aWvl2pYIfV4LMd+C8or783yYrzKSK2JlP+Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@types/dockerode": "^3.3.47", + "archiver": "^7.0.1", + "async-lock": "^1.4.1", + "byline": "^5.0.0", + "debug": "^4.4.3", + "docker-compose": "^1.3.0", + "dockerode": "^4.0.9", + "get-port": "^7.1.0", + "proper-lockfile": "^4.1.2", + "properties-reader": "^2.3.0", + "ssh-remote-port-forward": "^1.0.4", + "tar-fs": "^3.1.1", + "tmp": "^0.2.5", + "undici": "^7.16.0" + } + }, + "node_modules/@testcontainers/mysql/node_modules/undici": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", + "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/@testcontainers/postgresql": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-11.11.0.tgz", diff --git a/package.json b/package.json index e463b19c7..0686aa316 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,7 @@ "@testcontainers/hivemq": "^10.13.1", "@testcontainers/mariadb": "^10.13.0", "@testcontainers/mssqlserver": "^10.28.0", + "@testcontainers/mysql": "^11.11.0", "@testcontainers/postgresql": "^11.9.0", "@testcontainers/rabbitmq": "^10.13.2", "@types/bootstrap": "~5.1.9", diff --git a/test/backend-test/test-migration.js b/test/backend-test/test-migration.js index 0ed494fae..e2218d4e5 100644 --- a/test/backend-test/test-migration.js +++ b/test/backend-test/test-migration.js @@ -2,6 +2,7 @@ const { describe, test } = require("node:test"); const fs = require("fs"); const path = require("path"); const { GenericContainer, Wait } = require("testcontainers"); +const { MySqlContainer } = require("@testcontainers/mysql"); describe("Database Migration", () => { test("SQLite migrations run successfully from fresh database", async () => { @@ -130,4 +131,68 @@ describe("Database Migration", () => { } } ); + + test( + "MySQL migrations run successfully from fresh database", + { + skip: + !!process.env.CI && + (process.platform !== "linux" || process.arch !== "x64"), + }, + async () => { + // Start MySQL 8.0 container (the version mentioned in the issue) + const mysqlContainer = await new MySqlContainer("mysql:8.0") + .withStartupTimeout(120000) + .start(); + + const knex = require("knex"); + const knexInstance = knex({ + client: "mysql2", + connection: { + host: mysqlContainer.getHost(), + port: mysqlContainer.getPort(), + user: mysqlContainer.getUsername(), + password: mysqlContainer.getUserPassword(), + database: mysqlContainer.getDatabase(), + connectTimeout: 60000, + }, + pool: { + min: 0, + max: 10, + acquireTimeoutMillis: 60000, + idleTimeoutMillis: 60000, + }, + }); + + // Setup R (redbean) with knex instance like production code does + const { R } = require("redbean-node"); + R.setup(knexInstance); + + try { + // Use production code to initialize MySQL tables + const { createTables } = require("../../db/knex_init_db.js"); + await createTables(); + + // Run all migrations like production code does + await R.knex.migrate.latest({ + directory: path.join(__dirname, "../../db/knex_migrations") + }); + + // Test passes if migrations complete successfully without errors + + } finally { + // Clean up + try { + await R.knex.destroy(); + } catch (e) { + // Ignore cleanup errors + } + try { + await mysqlContainer.stop(); + } catch (e) { + // Ignore cleanup errors + } + } + } + ); });