diff --git a/.github/workflows/contentauthor.yaml b/.github/workflows/contentauthor.yaml
index 393e433ce5..ab8b3f69fd 100644
--- a/.github/workflows/contentauthor.yaml
+++ b/.github/workflows/contentauthor.yaml
@@ -77,6 +77,31 @@ jobs:
if: "!inputs.deploy"
runs-on: ubuntu-22.04
+ services:
+ mariadb:
+ image: mariadb:11
+ env:
+ MARIADB_ROOT_PASSWORD: mysqlpassword
+ MARIADB_DATABASE: edlibcontentauthor_test
+ ports:
+ - 3306:3306
+ options: >-
+ --health-cmd="healthcheck.sh --connect --innodb_initialized"
+ --health-interval=5s
+ --health-timeout=5s
+ --health-retries=20
+ postgres:
+ image: postgres:16-alpine
+ env:
+ POSTGRES_PASSWORD: thepgpassword
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd=pg_isready
+ --health-interval=5s
+ --health-timeout=5s
+ --health-retries=20
+
steps:
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
@@ -100,6 +125,9 @@ jobs:
- name: Test
run: |
docker run -v "/tmp:/reports" --rm \
+ --network host \
+ -e DB_HOST=127.0.0.1 \
+ -e DB_PGSQL_HOST=127.0.0.1 \
test-image \
vendor/bin/phpunit --coverage-clover /reports/coverage.xml
diff --git a/docker-compose.yml b/docker-compose.yml
index c515ced4da..eca6e25303 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -69,6 +69,7 @@ services:
depends_on:
contentauthor-startup: { condition: service_completed_successfully }
mysql: { condition: service_healthy }
+ postgres: { condition: service_healthy }
redis: { condition: service_healthy }
healthcheck:
test: [CMD, nc, -z, localhost, "9000"]
@@ -86,6 +87,7 @@ services:
depends_on:
contentauthor-startup: { condition: service_completed_successfully }
mysql: { condition: service_healthy }
+ postgres: { condition: service_healthy }
redis: { condition: service_healthy }
volumes:
- ./data/nginx/ca:/usr/local/share/ca-certificates:ro
@@ -101,6 +103,7 @@ services:
depends_on:
contentauthor-startup: { condition: service_completed_successfully }
mysql: { condition: service_healthy }
+ postgres: { condition: service_healthy }
redis: { condition: service_healthy }
volumes:
- ./data/nginx/ca:/usr/local/share/ca-certificates:ro
@@ -116,6 +119,7 @@ services:
depends_on:
certs: { condition: service_completed_successfully }
mysql: { condition: service_healthy }
+ postgres: { condition: service_healthy }
redis: { condition: service_healthy }
volumes:
- ./data/nginx/ca:/usr/local/share/ca-certificates:ro
diff --git a/localSetup/projects/mysql/init.sql b/localSetup/projects/mysql/init.sql
index 8522f43932..e9c956bb79 100644
--- a/localSetup/projects/mysql/init.sql
+++ b/localSetup/projects/mysql/init.sql
@@ -1,2 +1,3 @@
CREATE DATABASE IF NOT EXISTS edlibcontentauthor;
CREATE DATABASE IF NOT EXISTS moodle;
+CREATE DATABASE IF NOT EXISTS edlibcontentauthor_test;
diff --git a/localSetup/projects/postgres/postgresinit.d/hub.sql b/localSetup/projects/postgres/postgresinit.d/hub.sql
index 52e783f9e1..9f56f981fc 100644
--- a/localSetup/projects/postgres/postgresinit.d/hub.sql
+++ b/localSetup/projects/postgres/postgresinit.d/hub.sql
@@ -1,2 +1,3 @@
CREATE DATABASE hub;
CREATE DATABASE hub_test;
+CREATE DATABASE contentauthor;
diff --git a/sourcecode/apis/contentauthor/.env.example b/sourcecode/apis/contentauthor/.env.example
index 2e1142919f..eff4f9236a 100644
--- a/sourcecode/apis/contentauthor/.env.example
+++ b/sourcecode/apis/contentauthor/.env.example
@@ -12,6 +12,7 @@ LOG_STACK_CHANNELS=stderr,single
LOG_LEVEL=debug
#LOG_STDERR_FORMATTER='App\Logging\JsonFormatter'
+# To run contentauthor with postgresql switch the comments for these blocks.
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
@@ -19,6 +20,13 @@ DB_DATABASE=edlibcontentauthor
DB_USERNAME=root
DB_PASSWORD=mysqlpassword
+# DB_CONNECTION=pgsql
+# DB_HOST=postgres
+# DB_PORT=5432
+# DB_DATABASE=contentauthor
+# DB_USERNAME=postgres
+# DB_PASSWORD=thepgpassword
+
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
diff --git a/sourcecode/apis/contentauthor/Dockerfile b/sourcecode/apis/contentauthor/Dockerfile
index 8b4be7ffb2..75bc2ddecd 100644
--- a/sourcecode/apis/contentauthor/Dockerfile
+++ b/sourcecode/apis/contentauthor/Dockerfile
@@ -15,6 +15,7 @@ RUN install-php-extensions \
pcntl \
pcov \
pdo_mysql \
+ pdo_pgsql \
redis \
zip
diff --git a/sourcecode/apis/contentauthor/app/H5PLibrary.php b/sourcecode/apis/contentauthor/app/H5PLibrary.php
index a62783c332..34837ad9d6 100644
--- a/sourcecode/apis/contentauthor/app/H5PLibrary.php
+++ b/sourcecode/apis/contentauthor/app/H5PLibrary.php
@@ -323,15 +323,15 @@ public function getAddons()
});
})
->select([
- 'l1.id as libraryId',
- 'l1.add_to as addTo',
- 'l1.name as machineName',
- 'l1.major_version as majorVersion',
- 'l1.minor_version as minorVersion',
- 'l1.patch_version as patchVersion',
- 'l1.preloaded_js as preloadedJs',
- 'l1.preloaded_css as preloadedCss',
- 'l1.patch_version_in_folder_name as patchVersionInFolderName',
+ 'l1.id as \"libraryId\"',
+ 'l1.add_to as \"addTo\"',
+ 'l1.name as \"machineName\"',
+ 'l1.major_version as \"majorVersion\"',
+ 'l1.minor_version as \"minorVersion\"',
+ 'l1.patch_version as \"patchVersion\"',
+ 'l1.preloaded_js as \"preloadedJs\"',
+ 'l1.preloaded_css as \"preloadedCss\"',
+ 'l1.patch_version_in_folder_name as \"patchVersionInFolderName\"',
])
->whereNull('l2.name')
->whereNotNull('l1.add_to')
diff --git a/sourcecode/apis/contentauthor/app/Http/Controllers/Admin/AdminController.php b/sourcecode/apis/contentauthor/app/Http/Controllers/Admin/AdminController.php
index b315f2820b..ba8f00ec36 100644
--- a/sourcecode/apis/contentauthor/app/Http/Controllers/Admin/AdminController.php
+++ b/sourcecode/apis/contentauthor/app/Http/Controllers/Admin/AdminController.php
@@ -40,7 +40,6 @@ public function viewMaxScoreOverview()
},
])
->groupBy('id')
- ->having('contents_count', ">", 0)
->orderBy('name')
->get()
->filter(fn(H5PLibrary $library) => $library->supportsMaxScore());
diff --git a/sourcecode/apis/contentauthor/app/Http/Controllers/Admin/LibraryUpgradeController.php b/sourcecode/apis/contentauthor/app/Http/Controllers/Admin/LibraryUpgradeController.php
index c1ddb3681d..9c9378573b 100644
--- a/sourcecode/apis/contentauthor/app/Http/Controllers/Admin/LibraryUpgradeController.php
+++ b/sourcecode/apis/contentauthor/app/Http/Controllers/Admin/LibraryUpgradeController.php
@@ -20,7 +20,6 @@
use Illuminate\Http\Response;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
-use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class LibraryUpgradeController extends Controller
@@ -46,12 +45,11 @@ public function index(Request $request): View
'patch_version',
'runnable',
'restricted',
- DB::raw('UNIX_TIMESTAMP(created_at) as created_ts'),
- DB::raw('UNIX_TIMESTAMP(updated_at) as updated_ts'),
+ 'created_at',
+ 'updated_at',
])
->orderBy('major_version')
->orderBy('minor_version')
- ->getQuery()
->get()
->mapToGroups(function ($item) {
return [$item->name => $item];
@@ -105,13 +103,13 @@ public function index(Request $request): View
'hubUpgradeMessage' => '',
'hubAvailable' => false,
'canRebuild' => !empty($library->id),
- 'createdTs' => $library->created_ts,
- 'updatedTs' => $library->updated_ts,
+ 'createdTs' => $library->created_at->timestamp,
+ 'updatedTs' => $library->updated_at->timestamp,
];
if ($library->runnable) {
if (config('h5p.isHubEnabled')) {
- $upgrades = $this->core->getUpgrades($library, $versions->toArray());
+ $upgrades = $this->core->getUpgrades($library, $versions->all());
$item['upgradeUrl'] = empty($upgrades) || empty($usage['content']) ? false : route('admin.library', [
'task' => 'upgrade',
diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php
index 59a5fae31d..27bee9861e 100644
--- a/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php
+++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php
@@ -951,15 +951,15 @@ public function loadContentDependencies($id, $type = null)
}
$sql =
"SELECT hl.id
- , hl.name AS machineName
- , hl.major_version AS majorVersion
- , hl.minor_version AS minorVersion
- , hl.patch_version AS patchVersion
- , hl.preloaded_css AS preloadedCss
- , hl.preloaded_js AS preloadedJs
- , hcl.drop_css AS dropCss
- , hcl.dependency_type AS dependencyType
- , hl.patch_version_in_folder_name AS patchVersionInFolderName
+ , hl.name AS \"machineName\"
+ , hl.major_version AS \"majorVersion\"
+ , hl.minor_version AS \"minorVersion\"
+ , hl.patch_version AS \"patchVersion\"
+ , hl.preloaded_css AS \"preloadedCss\"
+ , hl.preloaded_js AS \"preloadedJs\"
+ , hcl.drop_css AS \"dropCss\"
+ , hcl.dependency_type AS \"dependencyType\"
+ , hl.patch_version_in_folder_name AS \"patchVersionInFolderName\"
FROM h5p_contents_libraries hcl
JOIN h5p_libraries hl ON hcl.library_id = hl.id
WHERE hcl.content_id = ?";
@@ -1208,8 +1208,8 @@ public function replaceContentTypeCache($contentTypeCache)
'major_version' => $type->version->major,
'minor_version' => $type->version->minor,
'patch_version' => $type->version->patch,
- 'h5P_major_version' => $type->coreApiVersionNeeded->major,
- 'h5P_minor_version' => $type->coreApiVersionNeeded->minor,
+ 'h5p_major_version' => $type->coreApiVersionNeeded->major,
+ 'h5p_minor_version' => $type->coreApiVersionNeeded->minor,
'title' => $type->title,
'summary' => $type->summary,
'description' => $type->description,
diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PProgress.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PProgress.php
index 44a0dcfa68..ab411c76a9 100644
--- a/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PProgress.php
+++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PProgress.php
@@ -152,7 +152,7 @@ public function getProgress(Request $request)
public function getState($contentId, $context)
{
- $sql = "select data, sub_content_id, data_id from $this->tableName where content_id=:contentId and user_id=:userId and preload=1";
+ $sql = "select data, sub_content_id, data_id from $this->tableName where content_id=:contentId and user_id=:userId and preload=true";
$params = [
':contentId' => $contentId,
':userId' => $this->currentUserId,
diff --git a/sourcecode/apis/contentauthor/config/queue.php b/sourcecode/apis/contentauthor/config/queue.php
index 3e15ddffdc..a8a963f273 100644
--- a/sourcecode/apis/contentauthor/config/queue.php
+++ b/sourcecode/apis/contentauthor/config/queue.php
@@ -94,7 +94,7 @@
*/
'failed' => [
- 'database' => 'mysql', 'table' => 'failed_jobs',
+ 'database' => env('DB_CONNECTION', 'mysql'), 'table' => 'failed_jobs',
],
];
diff --git a/sourcecode/apis/contentauthor/database/migrations/2015_09_25_092958_create_h5p_contents_user_data_table.php b/sourcecode/apis/contentauthor/database/migrations/2015_09_25_092958_create_h5p_contents_user_data_table.php
index 8bdc420b57..0894f832dc 100644
--- a/sourcecode/apis/contentauthor/database/migrations/2015_09_25_092958_create_h5p_contents_user_data_table.php
+++ b/sourcecode/apis/contentauthor/database/migrations/2015_09_25_092958_create_h5p_contents_user_data_table.php
@@ -20,7 +20,7 @@ public function up()
$table->text('data');
$table->boolean('preload')->default(0);
$table->boolean('invalidate')->default(0);
- $table->dateTime('updated_at')->default('0000-00-00 00:00:00');
+ $table->dateTime('updated_at')->useCurrent();
$table->primary(['content_id','user_id','sub_content_id','data_id'], 'cud_pk1');
});
}
diff --git a/sourcecode/apis/contentauthor/database/migrations/2015_09_25_092958_create_h5p_libraries_languages_table.php b/sourcecode/apis/contentauthor/database/migrations/2015_09_25_092958_create_h5p_libraries_languages_table.php
index 23e3c3408b..4c4ee8b132 100644
--- a/sourcecode/apis/contentauthor/database/migrations/2015_09_25_092958_create_h5p_libraries_languages_table.php
+++ b/sourcecode/apis/contentauthor/database/migrations/2015_09_25_092958_create_h5p_libraries_languages_table.php
@@ -13,10 +13,11 @@ class CreateH5pLibrariesLanguagesTable extends Migration
public function up()
{
Schema::create('h5p_libraries_languages', function (Blueprint $table) {
+ $table->increments('id');
$table->integer('library_id')->unsigned();
$table->string('language_code', 31);
$table->text('translation', 65535);
- $table->primary(['library_id','language_code']);
+ $table->unique(['library_id','language_code']);
});
}
diff --git a/sourcecode/apis/contentauthor/database/migrations/2015_09_25_092958_create_h5p_libraries_libraries_table.php b/sourcecode/apis/contentauthor/database/migrations/2015_09_25_092958_create_h5p_libraries_libraries_table.php
index 582d191b5f..69a6be5dfe 100644
--- a/sourcecode/apis/contentauthor/database/migrations/2015_09_25_092958_create_h5p_libraries_libraries_table.php
+++ b/sourcecode/apis/contentauthor/database/migrations/2015_09_25_092958_create_h5p_libraries_libraries_table.php
@@ -13,10 +13,11 @@ class CreateH5pLibrariesLibrariesTable extends Migration
public function up()
{
Schema::create('h5p_libraries_libraries', function (Blueprint $table) {
+ $table->increments('id');
$table->integer('library_id')->unsigned();
$table->integer('required_library_id')->unsigned();
$table->string('dependency_type', 31);
- $table->primary(['library_id','required_library_id']);
+ $table->unique(['library_id','required_library_id']);
});
}
diff --git a/sourcecode/apis/contentauthor/database/migrations/2015_10_02_083208_update-user_id.php b/sourcecode/apis/contentauthor/database/migrations/2015_10_02_083208_update-user_id.php
index df5947c6da..03b5ee576b 100644
--- a/sourcecode/apis/contentauthor/database/migrations/2015_10_02_083208_update-user_id.php
+++ b/sourcecode/apis/contentauthor/database/migrations/2015_10_02_083208_update-user_id.php
@@ -29,6 +29,15 @@ public function up()
*/
public function down()
{
+ // Postgres needs an explicit cast to turn a string column into an integer
+ if (DB::connection()->getDriverName() === 'pgsql') {
+ foreach (['h5p_contents', 'h5p_contents_user_data', 'h5p_results'] as $tableName) {
+ DB::statement("ALTER TABLE $tableName ALTER COLUMN user_id TYPE integer USING user_id::integer");
+ }
+
+ return;
+ }
+
Schema::table('h5p_contents', function ($table) {
$table->integer('user_id')->unsigned()->change();
});
diff --git a/sourcecode/apis/contentauthor/database/migrations/2015_12_15_081807_create_content_sharing_table.php b/sourcecode/apis/contentauthor/database/migrations/2015_12_15_081807_create_content_sharing_table.php
index 8fb0264b8e..ec0afb74f7 100644
--- a/sourcecode/apis/contentauthor/database/migrations/2015_12_15_081807_create_content_sharing_table.php
+++ b/sourcecode/apis/contentauthor/database/migrations/2015_12_15_081807_create_content_sharing_table.php
@@ -15,7 +15,7 @@ public function up()
Schema::create('cerpus_contents_shares', function (Blueprint $table) {
$table->integer('h5p_id')->unsigned();
$table->text('email', 65535);
- $table->dateTime('created_at')->default('0000-00-00 00:00:00');
+ $table->dateTime('created_at')->useCurrent();
});
}
diff --git a/sourcecode/apis/contentauthor/database/migrations/2016_04_27_123915_create_h5p_options_table.php b/sourcecode/apis/contentauthor/database/migrations/2016_04_27_123915_create_h5p_options_table.php
index f7ea15802c..9695dbb2f0 100644
--- a/sourcecode/apis/contentauthor/database/migrations/2016_04_27_123915_create_h5p_options_table.php
+++ b/sourcecode/apis/contentauthor/database/migrations/2016_04_27_123915_create_h5p_options_table.php
@@ -17,7 +17,7 @@ public function up()
$table->bigIncrements('option_id')->unsigned();
$table->string('option_name', 191)->nullable()->default(null);
$table->longText('option_value');
- $table->string('autoload', 20);
+ $table->string('autoload', 20)->default(0);
});
}
}
diff --git a/sourcecode/apis/contentauthor/database/migrations/2016_05_25_130842_add_h5p_context.php b/sourcecode/apis/contentauthor/database/migrations/2016_05_25_130842_add_h5p_context.php
index b6c5bb29db..20e590a685 100644
--- a/sourcecode/apis/contentauthor/database/migrations/2016_05_25_130842_add_h5p_context.php
+++ b/sourcecode/apis/contentauthor/database/migrations/2016_05_25_130842_add_h5p_context.php
@@ -35,7 +35,7 @@ public function up()
$table->text('data');
$table->boolean('preload')->default(0);
$table->boolean('invalidate')->default(0);
- $table->dateTime('updated_at')->default('0000-00-00 00:00:00');
+ $table->dateTime('updated_at')->useCurrent();
$table->string('context', 40)->nullable()->default(null);
$table->unique(['content_id', 'user_id', 'sub_content_id', 'data_id', 'context'], 'cu_pid');
});
@@ -57,10 +57,17 @@ public function up()
public function down()
{
Schema::table('h5p_contents_user_data', function ($table) {
- $table->dropColumn(["id", "context"]);
+ $table->dropUnique('cu_pid');
if (!DB::connection() instanceof \Illuminate\Database\SQLiteConnection) {
- $table->dropUnique('cu_pid');
+ $table->dropIndex(['context']);
}
+ });
+
+ Schema::table('h5p_contents_user_data', function ($table) {
+ $table->dropColumn(["id", "context"]);
+ });
+
+ Schema::table('h5p_contents_user_data', function ($table) {
$table->primary(['content_id', 'user_id', 'sub_content_id', 'data_id'], 'cud_pk1');
});
diff --git a/sourcecode/apis/contentauthor/database/migrations/2016_08_01_085342_increase_h5p_contents_textfield_size.php b/sourcecode/apis/contentauthor/database/migrations/2016_08_01_085342_increase_h5p_contents_textfield_size.php
index ac8b49799a..66c8375bab 100644
--- a/sourcecode/apis/contentauthor/database/migrations/2016_08_01_085342_increase_h5p_contents_textfield_size.php
+++ b/sourcecode/apis/contentauthor/database/migrations/2016_08_01_085342_increase_h5p_contents_textfield_size.php
@@ -14,12 +14,12 @@ class IncreaseH5pContentsTextfieldSize extends Migration
public function up()
{
Schema::table('h5p_contents', function ($table) {
- $table->string('parameters', self::longTextLength)->change();
- $table->string('filtered', self::longTextLength)->change();
+ $table->longText('parameters')->change();
+ $table->longText('filtered')->change();
});
Schema::table('h5p_contents_user_data', function ($table) {
- $table->string('data', self::longTextLength)->change();
+ $table->longText('data')->change();
});
}
diff --git a/sourcecode/apis/contentauthor/database/migrations/2016_10_20_071210_set_all_content_private.php b/sourcecode/apis/contentauthor/database/migrations/2016_10_20_071210_set_all_content_private.php
index c095127e2e..0069638276 100644
--- a/sourcecode/apis/contentauthor/database/migrations/2016_10_20_071210_set_all_content_private.php
+++ b/sourcecode/apis/contentauthor/database/migrations/2016_10_20_071210_set_all_content_private.php
@@ -7,7 +7,7 @@ class SetAllContentPrivate extends Migration
public function up()
{
DB::update('update articles set is_private=1');
- DB::update('update h5p_contents set is_private=1');
+ DB::update('update h5p_contents set is_private=true');
}
public function down()
diff --git a/sourcecode/apis/contentauthor/database/migrations/2018_07_12_112426_add_millionaire_contenttype.php b/sourcecode/apis/contentauthor/database/migrations/2018_07_12_112426_add_millionaire_contenttype.php
index d14c93b76e..bfcaa0a85f 100644
--- a/sourcecode/apis/contentauthor/database/migrations/2018_07_12_112426_add_millionaire_contenttype.php
+++ b/sourcecode/apis/contentauthor/database/migrations/2018_07_12_112426_add_millionaire_contenttype.php
@@ -31,7 +31,7 @@ public function up()
if (!Schema::hasTable('games')) {
Schema::create('games', function (Blueprint $table) {
$table->uuid('id');
- $table->string('gametype');
+ $table->uuid('gametype');
$table->string('title');
$table->string('language_code');
$table->string('owner');
diff --git a/sourcecode/apis/contentauthor/database/migrations/2019_06_24_105205_change_ndla_id_field_to_string.php b/sourcecode/apis/contentauthor/database/migrations/2019_06_24_105205_change_ndla_id_field_to_string.php
index cfda5f914f..4933fea3f8 100644
--- a/sourcecode/apis/contentauthor/database/migrations/2019_06_24_105205_change_ndla_id_field_to_string.php
+++ b/sourcecode/apis/contentauthor/database/migrations/2019_06_24_105205_change_ndla_id_field_to_string.php
@@ -1,5 +1,6 @@
getDriverName() === 'pgsql') {
+ DB::statement('ALTER TABLE ndla_article_import_statuses ALTER COLUMN ndla_id TYPE integer USING ndla_id::integer');
+
+ return;
+ }
+
Schema::table('ndla_article_import_statuses', function (Blueprint $table) {
$table->unsignedInteger('ndla_id')->change();
});
diff --git a/sourcecode/apis/contentauthor/database/migrations/2026_08_19_130000_change_games_gametype_to_uuid.php b/sourcecode/apis/contentauthor/database/migrations/2026_08_19_130000_change_games_gametype_to_uuid.php
new file mode 100644
index 0000000000..5b9fce6401
--- /dev/null
+++ b/sourcecode/apis/contentauthor/database/migrations/2026_08_19_130000_change_games_gametype_to_uuid.php
@@ -0,0 +1,47 @@
+dropForeign(['gametype']);
+ });
+
+ Schema::table('games', function (Blueprint $table) {
+ $table->uuid('gametype')->change();
+ });
+
+ Schema::table('games', function (Blueprint $table) {
+ $table->foreign('gametype')->references('id')->on('gametypes');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ // Nothing to revert: "gametype" should be a uuid on every database, and
+ // changing it back to a varchar breaks the foreign key on Postgres.
+ }
+};
diff --git a/sourcecode/apis/contentauthor/database/migrations/2026_08_19_140000_fix_h5p_options_autoload_null.php b/sourcecode/apis/contentauthor/database/migrations/2026_08_19_140000_fix_h5p_options_autoload_null.php
new file mode 100644
index 0000000000..383f18031e
--- /dev/null
+++ b/sourcecode/apis/contentauthor/database/migrations/2026_08_19_140000_fix_h5p_options_autoload_null.php
@@ -0,0 +1,34 @@
+whereNull('autoload')
+ ->update(['autoload' => 0]);
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ // Not reversible: original NULL rows can't be distinguished from
+ // rows that legitimately had autoload = 0.
+ }
+};
diff --git a/sourcecode/apis/contentauthor/database/migrations/2026_08_20_100000_replace_h5p_libraries_languages_primary_key.php b/sourcecode/apis/contentauthor/database/migrations/2026_08_20_100000_replace_h5p_libraries_languages_primary_key.php
new file mode 100644
index 0000000000..e1894506b4
--- /dev/null
+++ b/sourcecode/apis/contentauthor/database/migrations/2026_08_20_100000_replace_h5p_libraries_languages_primary_key.php
@@ -0,0 +1,62 @@
+hasCompositePrimaryKey('h5p_libraries_languages')) {
+ return;
+ }
+
+ Schema::table('h5p_libraries_languages', function (Blueprint $table) {
+ $table->dropPrimary(['library_id', 'language_code']);
+ });
+
+ Schema::table('h5p_libraries_languages', function (Blueprint $table) {
+ $table->increments('id')->first();
+ $table->unique(['library_id', 'language_code']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ if ($this->hasCompositePrimaryKey('h5p_libraries_languages')) {
+ return;
+ }
+
+ Schema::table('h5p_libraries_languages', function (Blueprint $table) {
+ $table->dropUnique(['library_id', 'language_code']);
+ $table->dropColumn('id');
+ });
+
+ Schema::table('h5p_libraries_languages', function (Blueprint $table) {
+ $table->primary(['library_id', 'language_code']);
+ });
+ }
+
+ private function hasCompositePrimaryKey(string $table): bool
+ {
+ $primaryKey = collect(Schema::getIndexes($table))->firstWhere('primary', true);
+
+ return count($primaryKey['columns'] ?? []) > 1;
+ }
+};
diff --git a/sourcecode/apis/contentauthor/database/migrations/2026_08_20_110000_replace_h5p_libraries_libraries_primary_key.php b/sourcecode/apis/contentauthor/database/migrations/2026_08_20_110000_replace_h5p_libraries_libraries_primary_key.php
new file mode 100644
index 0000000000..035ae63a16
--- /dev/null
+++ b/sourcecode/apis/contentauthor/database/migrations/2026_08_20_110000_replace_h5p_libraries_libraries_primary_key.php
@@ -0,0 +1,62 @@
+hasCompositePrimaryKey('h5p_libraries_libraries')) {
+ return;
+ }
+
+ Schema::table('h5p_libraries_libraries', function (Blueprint $table) {
+ $table->dropPrimary(['library_id', 'required_library_id']);
+ });
+
+ Schema::table('h5p_libraries_libraries', function (Blueprint $table) {
+ $table->increments('id')->first();
+ $table->unique(['library_id', 'required_library_id']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ if ($this->hasCompositePrimaryKey('h5p_libraries_libraries')) {
+ return;
+ }
+
+ Schema::table('h5p_libraries_libraries', function (Blueprint $table) {
+ $table->dropUnique(['library_id', 'required_library_id']);
+ $table->dropColumn('id');
+ });
+
+ Schema::table('h5p_libraries_libraries', function (Blueprint $table) {
+ $table->primary(['library_id', 'required_library_id']);
+ });
+ }
+
+ private function hasCompositePrimaryKey(string $table): bool
+ {
+ $primaryKey = collect(Schema::getIndexes($table))->firstWhere('primary', true);
+
+ return count($primaryKey['columns'] ?? []) > 1;
+ }
+};
diff --git a/sourcecode/apis/contentauthor/phpunit.xml b/sourcecode/apis/contentauthor/phpunit.xml
index 0d0af84618..6f02041a19 100644
--- a/sourcecode/apis/contentauthor/phpunit.xml
+++ b/sourcecode/apis/contentauthor/phpunit.xml
@@ -33,8 +33,18 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleVersioningTest.php b/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleVersioningTest.php
index f257bc0365..4d827693bd 100644
--- a/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleVersioningTest.php
+++ b/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleVersioningTest.php
@@ -26,12 +26,18 @@ public function testDatabaseVersioning()
{
$authId = Str::uuid();
$article = Article::factory()->create(['owner_id' => $authId]);
+ ContentVersion::factory()->create([
+ 'id' => $article->version_id,
+ 'content_id' => $article->id,
+ 'content_type' => Content::TYPE_ARTICLE,
+ ]);
$startCount = Article::count();
$this->withSession(['authId' => $authId])
->put(route('article.update', $article->id), [
'title' => 'Title',
'content' => 'Content',
- ]);
+ ])
+ ->assertStatus(Response::HTTP_CREATED);
$this->assertDatabaseHas('articles', [ // See the new
'title' => 'Title',
'content' => 'Content',
diff --git a/sourcecode/apis/contentauthor/tests/Integration/Commands/EnsureVersionExistsTest.php b/sourcecode/apis/contentauthor/tests/Integration/Commands/EnsureVersionExistsTest.php
index 34e7498936..87110d2cd7 100644
--- a/sourcecode/apis/contentauthor/tests/Integration/Commands/EnsureVersionExistsTest.php
+++ b/sourcecode/apis/contentauthor/tests/Integration/Commands/EnsureVersionExistsTest.php
@@ -17,12 +17,18 @@ class EnsureVersionExistsTest extends TestCase
public function test_articles(): void
{
+ // The command processes articles ordered by created_at, so make it unique
+ $seconds = 0;
+ $createdAt = function () use (&$seconds) {
+ return ['created_at' => now()->addSeconds(++$seconds)];
+ };
+
// Not versioned
- $noVersion = Article::factory(2)->create([
+ $noVersion = Article::factory(2)->sequence($createdAt)->create([
'version_id' => null,
]);
// With version
- $versioned = Article::factory(2)->create()->each(function (Article $article) {
+ $versioned = Article::factory(2)->sequence($createdAt)->create()->each(function (Article $article) {
ContentVersion::factory()->create([
'id' => $article->version_id,
'content_id' => $article->id,
@@ -30,10 +36,10 @@ public function test_articles(): void
]);
});
// Version record missing
- $missing = Article::factory(2)->create();
+ $missing = Article::factory(2)->sequence($createdAt)->create();
// Unconnected version
$unconnectedVersions = collect();
- $notConnected = Article::factory(2)->create([
+ $notConnected = Article::factory(2)->sequence($createdAt)->create([
'version_id' => null,
])->each(function (Article $article) use ($unconnectedVersions) {
$unconnectedVersions->add(ContentVersion::factory()->create([
diff --git a/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/Admin/AdminH5PDetailsControllerTest.php b/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/Admin/AdminH5PDetailsControllerTest.php
index 9f654d414e..c3f467a854 100644
--- a/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/Admin/AdminH5PDetailsControllerTest.php
+++ b/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/Admin/AdminH5PDetailsControllerTest.php
@@ -244,7 +244,6 @@ public function test_contentHistory(): void
'library_id' => $library->id,
]);
$content = H5PContent::factory()->create([
- 'id' => 42,
'version_id' => $this->faker->uuid,
'library_id' => $library->id,
]);
@@ -305,7 +304,6 @@ public function test_contentHistory_noResource(): void
]);
$library = H5PLibrary::factory()->create();
$content = H5PContent::factory()->create([
- 'id' => 42,
'version_id' => $this->faker->uuid,
'library_id' => $library->id,
]);
diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/CRUTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/CRUTest.php
index 3cf4ce76b6..ecc0631fd5 100644
--- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/CRUTest.php
+++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/CRUTest.php
@@ -86,13 +86,14 @@ public function create_and_update_h5p_using_web_request(bool $useLinearVersionin
->assertStatus(Response::HTTP_CREATED); // Redirects after save
$this->assertDatabaseCount('h5p_contents', 1);
- $h5p = H5PContent::find(1);
+ $h5p = H5PContent::sole();
+ $firstId = $h5p->id;
$this->assertCount(1, $h5p->collaborators);
- $this->assertDatabaseHas('h5p_contents', ['id' => 1, 'title' => 'Tittel']);
+ $this->assertDatabaseHas('h5p_contents', ['id' => $firstId, 'title' => 'Tittel']);
$firstVersion = $h5p->version_id;
$this->assertDatabaseHas('content_versions', [
'id' => $firstVersion,
- 'content_id' => 1,
+ 'content_id' => $firstId,
'content_type' => Content::TYPE_H5P,
'version_purpose' => ContentVersion::PURPOSE_CREATE,
]);
@@ -103,7 +104,7 @@ public function create_and_update_h5p_using_web_request(bool $useLinearVersionin
'email' => $owner->email,
'verifiedEmails' => [$owner->email],
])
- ->put(route('h5p.update', 1), [
+ ->put(route('h5p.update', $firstId), [
'_token' => csrf_token(),
'title' => 'Tittel',
'action' => 'create',
@@ -121,14 +122,14 @@ public function create_and_update_h5p_using_web_request(bool $useLinearVersionin
$h5p->refresh();
$this->assertDatabaseCount('h5p_contents', 1);
$this->assertCount(2, $h5p->collaborators);
- $this->assertDatabaseHas('h5p_contents', ['id' => 1, 'title' => 'Tittel']);
+ $this->assertDatabaseHas('h5p_contents', ['id' => $firstId, 'title' => 'Tittel']);
$this->assertDatabaseCount('content_versions', 2);
$secondVersion = $h5p->version_id;
$this->assertDatabaseHas('content_versions', [
'id' => $secondVersion,
'parent_id' => $firstVersion,
- 'content_id' => 1,
+ 'content_id' => $firstId,
'content_type' => Content::TYPE_H5P,
'version_purpose' => ContentVersion::PURPOSE_UPDATE,
]);
@@ -139,7 +140,7 @@ public function create_and_update_h5p_using_web_request(bool $useLinearVersionin
'email' => $owner->email,
'verifiedEmails' => [$owner->email],
])
- ->put(route('h5p.update', 1), [
+ ->put(route('h5p.update', $firstId), [
'_token' => csrf_token(),
'title' => 'Tittel 2', // Will trigger a new version
'action' => 'create',
@@ -154,16 +155,17 @@ public function create_and_update_h5p_using_web_request(bool $useLinearVersionin
]);
$this->assertDatabaseCount('h5p_contents', 2);
- $this->assertCount(2, H5PContent::find(1)->collaborators); // Original still has two collaborators
- $this->assertCount(3, H5PContent::find(2)->collaborators); // New has three collaborators
- $this->assertDatabaseHas('h5p_contents', ['id' => 1, 'title' => 'Tittel']);
- $this->assertDatabaseHas('h5p_contents', ['id' => 2, 'title' => 'Tittel 2']);
+ $secondId = H5PContent::max('id');
+ $this->assertCount(2, H5PContent::find($firstId)->collaborators); // Original still has two collaborators
+ $this->assertCount(3, H5PContent::find($secondId)->collaborators); // New has three collaborators
+ $this->assertDatabaseHas('h5p_contents', ['id' => $firstId, 'title' => 'Tittel']);
+ $this->assertDatabaseHas('h5p_contents', ['id' => $secondId, 'title' => 'Tittel 2']);
$this->assertDatabaseCount('content_versions', 3);
- $thirdVersion = H5PContent::find(2)->version_id;
+ $thirdVersion = H5PContent::find($secondId)->version_id;
$this->assertDatabaseHas('content_versions', [
'id' => $thirdVersion,
- 'content_id' => 2,
+ 'content_id' => $secondId,
'parent_id' => $secondVersion,
'version_purpose' => ContentVersion::PURPOSE_UPDATE,
]);
@@ -190,14 +192,15 @@ public function create_and_update_h5p_using_web_request(bool $useLinearVersionin
->assertStatus(Response::HTTP_OK);
$this->assertDatabaseCount('h5p_contents', 3);
- $this->assertCount(2, H5PContent::find(3)->collaborators); // Collaborators not updated
+ $thirdId = H5PContent::max('id');
+ $this->assertCount(2, H5PContent::find($thirdId)->collaborators); // Collaborators not updated
$this->assertDatabaseHas('h5p_contents', ['user_id' => $owner->auth_id, 'title' => 'Tittel 3']); // Owner has not changed, title updated
$this->assertDatabaseCount('content_versions', 4);
- $fourthVersion = H5PContent::find(3)->version_id;
+ $fourthVersion = H5PContent::find($thirdId)->version_id;
$this->assertDatabaseHas('content_versions', [
'id' => $fourthVersion,
- 'content_id' => 3,
+ 'content_id' => $thirdId,
'parent_id' => $useLinearVersioning ? $thirdVersion : $secondVersion,
'version_purpose' => ContentVersion::PURPOSE_UPDATE,
]);
@@ -227,13 +230,14 @@ public function create_and_update_h5p_using_web_request(bool $useLinearVersionin
->assertStatus(Response::HTTP_OK);
$this->assertDatabaseCount('h5p_contents', 4); // New H5P in db
+ $fourthId = H5PContent::max('id');
$this->assertDatabaseHas('h5p_contents', ['user_id' => $copyist->auth_id, 'title' => 'Tittel 4']); // Owner and title updated
- $this->assertCount(0, H5PContent::find(4)->collaborators); //No collaborators on new resource
+ $this->assertCount(0, H5PContent::find($fourthId)->collaborators); //No collaborators on new resource
$this->assertDatabaseCount('content_versions', 5);
$this->assertDatabaseHas('content_versions', [
- 'id' => H5PContent::find(4)->version_id,
- 'content_id' => 4,
+ 'id' => H5PContent::find($fourthId)->version_id,
+ 'content_id' => $fourthId,
'parent_id' => $useLinearVersioning ? $fourthVersion : $secondVersion,
'version_purpose' => ContentVersion::PURPOSE_COPY,
]);
diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/H5PViewConfigTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/H5PViewConfigTest.php
index 9c7591dba4..7f64d2cc1a 100644
--- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/H5PViewConfigTest.php
+++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/H5PViewConfigTest.php
@@ -130,8 +130,8 @@ public function test_loadContent(string $adapterMode): void
->loadContent($content->id)
->getConfig();
- $this->assertDatabaseHas('h5p_contents_libraries', ['content_id' => 1, 'library_id' => $library->id, 'dependency_type' => 'preloaded']);
- $this->assertDatabaseHas('h5p_contents_libraries', ['content_id' => 1, 'library_id' => $dependency->id, 'dependency_type' => 'preloaded']);
+ $this->assertDatabaseHas('h5p_contents_libraries', ['content_id' => $content->id, 'library_id' => $library->id, 'dependency_type' => 'preloaded']);
+ $this->assertDatabaseHas('h5p_contents_libraries', ['content_id' => $content->id, 'library_id' => $dependency->id, 'dependency_type' => 'preloaded']);
$this->assertTrue($data->postUserStatistics);
$this->assertObjectHasProperty('cid-' . $content->id, $data->contents);
diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/h5pTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/h5pTest.php
index fe8eaaf975..f3a023db7b 100644
--- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/h5pTest.php
+++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/h5pTest.php
@@ -119,7 +119,7 @@ public function createWithOutVersioningContent()
$h5p = app(h5p::class);
$content = $h5p->storeContent($request, null, "createContentUserId");
$this->assertNotFalse($content);
- $this->assertEquals(1, $content['id']);
+ $this->assertEquals(H5PContent::max('id'), $content['id']);
$this->assertEquals("My Test Title", $content['title']);
$this->assertEquals("createContentUserId", $content['user_id']);
$this->assertFileExists("{$this->getContentDirectory()}/{$content['id']}/images/image-5805bff7c5330.jpg");
@@ -130,8 +130,8 @@ public function createWithOutVersioningContent()
$this->assertEquals("Hvor er ørreten?", $contentParamsDecoded->cards[0]->text);
$this->assertEquals("Her!", $contentParamsDecoded->cards[0]->answer);
- $this->assertDatabaseHas("h5p_contents", ["id" => 1]);
- $this->assertDatabaseHas("h5p_contents_metadata", ["id" => 1, 'content_id' => 1, "license" => "U"]);
+ $this->assertDatabaseHas("h5p_contents", ["id" => $content['id']]);
+ $this->assertDatabaseHas("h5p_contents_metadata", ['content_id' => $content['id'], "license" => "U"]);
$request = new Request();
$request->replace([
@@ -145,7 +145,7 @@ public function createWithOutVersioningContent()
$storedContent = $core->loadContent($content['id']);
$updatedContent = $h5p->storeContent($request, $storedContent, "createContentUserId");
$this->assertNotFalse($updatedContent);
- $this->assertEquals(1, $updatedContent['id']);
+ $this->assertEquals($content['id'], $updatedContent['id']);
$this->assertEquals("Updated Test Title", $updatedContent['title']);
$this->assertEquals("createContentUserId", $updatedContent['user_id']);
$contentParamsDecoded = json_decode($updatedContent['params']);
@@ -155,13 +155,13 @@ public function createWithOutVersioningContent()
$this->assertEquals("Her!", $contentParamsDecoded->cards[0]->answer);
$this->assertFileExists("{$this->getContentDirectory()}/{$updatedContent['id']}/images/image-5805bff7c5330.jpg");
- $h5pContent = H5PContent::find(1);
+ $h5pContent = H5PContent::find($content['id']);
$this->assertEquals($h5pContent->id, $updatedContent['id']);
$this->assertEquals("Deltittel", $h5pContent->title);
- $this->assertDatabaseHas("h5p_contents", ["id" => 1]);
- $this->assertDatabaseMissing("h5p_contents", ["id" => 2]);
- $this->assertDatabaseHas("h5p_contents_metadata", ["id" => 1, 'content_id' => 1, 'license' => "BY"]);
+ $this->assertDatabaseCount("h5p_contents", 1);
+ $this->assertDatabaseHas("h5p_contents", ["id" => $content['id']]);
+ $this->assertDatabaseHas("h5p_contents_metadata", ['content_id' => $content['id'], 'license' => "BY"]);
}
#[Test]
@@ -179,7 +179,7 @@ public function createWithVersioningContent()
$h5p = app(h5p::class);
$content = $h5p->storeContent($request, null, "createContentUserId");
$this->assertNotFalse($content);
- $this->assertEquals(1, $content['id']);
+ $this->assertEquals(H5PContent::max('id'), $content['id']);
$this->assertEquals("My Test Title", $content['title']);
$this->assertEquals("createContentUserId", $content['user_id']);
$this->assertFileExists("{$this->getContentDirectory()}/{$content['id']}/images/image-5805bff7c5330.jpg");
@@ -190,8 +190,8 @@ public function createWithVersioningContent()
$this->assertEquals("Hvor er ørreten?", $contentParamsDecoded->cards[0]->text);
$this->assertEquals("Her!", $contentParamsDecoded->cards[0]->answer);
- $this->assertDatabaseHas("h5p_contents", ["id" => 1]);
- $this->assertDatabaseHas("h5p_contents_metadata", ["id" => 1, 'content_id' => 1, "license" => "U"]);
+ $this->assertDatabaseHas("h5p_contents", ["id" => $content['id']]);
+ $this->assertDatabaseHas("h5p_contents_metadata", ['content_id' => $content['id'], "license" => "U"]);
$request = Request::create('', parameters: [
'library' => "H5P.Flashcards 1.1",
@@ -204,7 +204,7 @@ public function createWithVersioningContent()
$storedContent['useVersioning'] = true;
$updatedContent = $h5p->storeContent($request, $storedContent, "createContentUserId");
$this->assertNotFalse($updatedContent);
- $this->assertEquals(2, $updatedContent['id']);
+ $this->assertNotEquals($content['id'], $updatedContent['id']);
$this->assertEquals("Updated Test Title", $updatedContent['title']);
$this->assertEquals("createContentUserId", $updatedContent['user_id']);
$contentParamsDecoded = json_decode($updatedContent['params']);
@@ -214,10 +214,10 @@ public function createWithVersioningContent()
$this->assertEquals("Her!", $contentParamsDecoded->cards[0]->answer);
$this->assertFileExists("{$this->getContentDirectory()}/{$updatedContent['id']}/images/image-5805bff7c5330.jpg");
- $this->assertDatabaseHas("h5p_contents", ["id" => 1]);
- $this->assertDatabaseHas("h5p_contents", ["id" => 2]);
- $this->assertDatabaseMissing("h5p_contents", ["id" => 3]);
- $this->assertDatabaseHas("h5p_contents_metadata", ["id" => 1, 'content_id' => 1, 'license' => "U"]);
- $this->assertDatabaseHas("h5p_contents_metadata", ["id" => 2, 'content_id' => 2, 'license' => "BY"]);
+ $this->assertDatabaseCount("h5p_contents", 2);
+ $this->assertDatabaseHas("h5p_contents", ["id" => $content['id']]);
+ $this->assertDatabaseHas("h5p_contents", ["id" => $updatedContent['id']]);
+ $this->assertDatabaseHas("h5p_contents_metadata", ['content_id' => $content['id'], 'license' => "U"]);
+ $this->assertDatabaseHas("h5p_contents_metadata", ['content_id' => $updatedContent['id'], 'license' => "BY"]);
}
}
diff --git a/sourcecode/apis/contentauthor/tests/Integration/MigrationsTest.php b/sourcecode/apis/contentauthor/tests/Integration/MigrationsTest.php
new file mode 100644
index 0000000000..03fecc21e2
--- /dev/null
+++ b/sourcecode/apis/contentauthor/tests/Integration/MigrationsTest.php
@@ -0,0 +1,100 @@
+setUpConnection($driver);
+
+ $this->artisan('migrate', ['--database' => $connection])
+ ->assertSuccessful();
+
+ $this->artisan('migrate:status', ['--database' => $connection, '--pending' => true])
+ ->expectsOutputToContain('No pending migrations')
+ ->assertSuccessful();
+
+ DB::purge($connection);
+ }
+
+ /**
+ * @param 'mysql'|'pgsql' $driver
+ */
+ #[DataProvider('provider_drivers')]
+ public function test_migrations_roll_back(string $driver): void
+ {
+ $connection = $this->setUpConnection($driver);
+
+ $this->artisan('migrate', ['--database' => $connection])
+ ->assertSuccessful();
+
+ $this->artisan('migrate:reset', ['--database' => $connection])
+ ->assertSuccessful();
+
+ $this->assertSame(
+ ['migrations'],
+ Schema::connection($connection)->getTableListing(),
+ 'Tables left after rolling back all migrations',
+ );
+
+ DB::purge($connection);
+ }
+
+ public static function provider_drivers(): Generator
+ {
+ yield 'mariadb' => ['mysql'];
+ yield 'postgres' => ['pgsql'];
+ }
+
+ /**
+ * @param 'mysql'|'pgsql' $driver
+ */
+ private function setUpConnection(string $driver): string
+ {
+ $config = match ($driver) {
+ 'mysql' => config('database.connections.mysql'),
+ 'pgsql' => [
+ ...config('database.connections.pgsql'),
+ 'host' => env('DB_PGSQL_HOST'),
+ 'port' => env('DB_PGSQL_PORT'),
+ 'username' => env('DB_PGSQL_USERNAME'),
+ 'password' => env('DB_PGSQL_PASSWORD'),
+ ],
+ };
+
+ // Connect to a database that always exists, and create the one to migrate
+ $admin = "migrations_admin_$driver";
+ config(["database.connections.$admin" => [
+ ...$config,
+ 'database' => $driver === 'pgsql' ? 'postgres' : null,
+ ]]);
+
+ DB::connection($admin)->statement('DROP DATABASE IF EXISTS ' . self::DATABASE . ($driver === 'pgsql' ? ' WITH (FORCE)' : ''));
+ DB::connection($admin)->statement('CREATE DATABASE ' . self::DATABASE);
+ DB::purge($admin);
+
+ $connection = "migrations_test_$driver";
+ config(["database.connections.$connection" => [...$config, 'database' => self::DATABASE]]);
+
+ return $connection;
+ }
+}