Skip to content

Commit 57c49e2

Browse files
committed
feat(migrations): make command create_initial_db idempotent
1 parent 552e09b commit 57c49e2

File tree

9 files changed

+546
-516
lines changed

9 files changed

+546
-516
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<?php namespace App\Console\Commands;
2+
/*
3+
* Copyright 2024 OpenStack Foundation
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
**/
14+
15+
use Illuminate\Console\Command;
16+
use Illuminate\Support\Facades\Validator;
17+
use Illuminate\Validation\ValidationException;
18+
19+
/**
20+
* Class CreateTestDBCommand
21+
* @package App\Console\Commands
22+
*/
23+
final class CreateInitialDBCommand extends Command {
24+
/**
25+
* The console command name.
26+
*
27+
* @var string
28+
*/
29+
protected $name = "create_initial_db";
30+
31+
/**
32+
* The name and signature of the console command.
33+
*
34+
* @var string
35+
*/
36+
protected $signature = "db:create_initial_db {--schema=}";
37+
38+
/**
39+
* The console command description.
40+
*
41+
* @var string
42+
*/
43+
protected $description = "Create Initial DB";
44+
45+
const SchemaConfig = "config";
46+
47+
const SchemaModel = "model";
48+
49+
const AllowedSchemas = [self::SchemaConfig, self::SchemaModel];
50+
51+
/**
52+
* MySQL error codes for objects that already exist.
53+
*/
54+
const ER_TABLE_EXISTS = '42S01';
55+
const ER_DUP_KEYNAME = '42000';
56+
const ER_DUP_ENTRY = '23000';
57+
58+
/**
59+
* Execute the console command.
60+
*
61+
* @return void
62+
*/
63+
public function handle(): void {
64+
$schema_name = $this->option("schema");
65+
$this->validateOptions($schema_name);
66+
67+
$this->info(sprintf("Creating Initial DB for schema %s", $schema_name));
68+
69+
$db_host = env("SS_DB_HOST");
70+
$db_port = env("SS_DB_PORT");
71+
$db_user_name = env("SS_DB_USERNAME");
72+
$db_password = env("SS_DB_PASSWORD");
73+
$db_name = env("SS_DATABASE");
74+
75+
if ($schema_name == self::SchemaConfig) {
76+
$db_host = env("DB_HOST");
77+
$db_port = env("DB_PORT");
78+
$db_user_name = env("DB_USERNAME");
79+
$db_password = env("DB_PASSWORD");
80+
$db_name = env("DB_DATABASE");
81+
}
82+
83+
$pdo = new \PDO(
84+
sprintf("mysql:host=%s;port=%s", $db_host, $db_port),
85+
$db_user_name,
86+
$db_password,
87+
);
88+
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
89+
90+
try {
91+
$this->info("creating schema {$db_name} at host {$db_host} (if not exists)...");
92+
$pdo->exec("CREATE SCHEMA IF NOT EXISTS " . $db_name . ";");
93+
$pdo->exec("USE " . $db_name . ";");
94+
} catch (\Exception $e) {
95+
$this->error($e->getMessage());
96+
return;
97+
}
98+
99+
$current_dir = dirname(__FILE__);
100+
101+
// Temporarily disable sql_require_primary_key for this session.
102+
// The schema dump contains tables without explicit PKs (e.g. join tables).
103+
// This setting is session-scoped and does not affect other connections.
104+
try {
105+
$pdo->exec("SET SESSION sql_require_primary_key = 0;");
106+
} catch (\PDOException $e) {
107+
// Variable may not exist on older MySQL versions — safe to ignore
108+
$this->warn("Could not disable sql_require_primary_key: " . $e->getMessage());
109+
}
110+
111+
$this->info("creating initial schema...");
112+
$schema = file_get_contents(
113+
"{$current_dir}/../../../database/migrations/{$schema_name}/initial_schema.sql",
114+
true,
115+
);
116+
$schema = explode(";", $schema);
117+
foreach ($schema as $ddl) {
118+
$ddl = trim($ddl);
119+
if (empty($ddl)) {
120+
continue;
121+
}
122+
// Make CREATE TABLE idempotent
123+
$ddl = preg_replace('/^create\s+table\b/i', 'CREATE TABLE IF NOT EXISTS', $ddl);
124+
try {
125+
$pdo->exec($ddl . ";");
126+
} catch (\PDOException $e) {
127+
// Skip duplicate index/key errors (already exists)
128+
if (in_array($e->getCode(), [self::ER_DUP_KEYNAME, self::ER_TABLE_EXISTS])) {
129+
$this->warn("Skipped (already exists): " . substr($ddl, 0, 80) . "...");
130+
continue;
131+
}
132+
throw $e;
133+
}
134+
}
135+
136+
$this->info("adding already ran migrations...");
137+
$migrations = file_get_contents(
138+
"{$current_dir}/../../../database/migrations/{$schema_name}/initial_migrations.sql",
139+
true,
140+
);
141+
142+
$migrations = explode(";", $migrations);
143+
144+
foreach ($migrations as $idx => $statement) {
145+
$statement = trim($statement);
146+
if (empty($statement)) {
147+
continue;
148+
}
149+
// Make INSERT idempotent by using INSERT IGNORE
150+
$statement = preg_replace('/^INSERT\s+INTO\b/i', 'INSERT IGNORE INTO', $statement);
151+
try {
152+
$affected = $pdo->exec($statement . ";");
153+
if ($affected > 0) {
154+
$this->info("added migration {$idx}.");
155+
} else {
156+
$this->warn("skipped migration {$idx} (already exists).");
157+
}
158+
} catch (\PDOException $e) {
159+
if ($e->getCode() === self::ER_DUP_ENTRY) {
160+
$this->warn("Skipped duplicate migration entry {$idx}.");
161+
continue;
162+
}
163+
throw $e;
164+
}
165+
}
166+
167+
$this->info(sprintf("Initial DB for schema %s created successfully!", $schema_name));
168+
}
169+
170+
protected function validateOptions($schema): void {
171+
$validator = Validator::make(
172+
[
173+
"schema" => $schema,
174+
],
175+
[
176+
"schema" => "required|string|in:" . implode(",", self::AllowedSchemas),
177+
],
178+
);
179+
180+
try {
181+
$validator->validate();
182+
} catch (ValidationException $e) {
183+
$this->error("Validation error: " . $e->getMessage());
184+
exit(1);
185+
}
186+
}
187+
}

app/Console/Commands/CreateTestDBCommand.php

Lines changed: 0 additions & 158 deletions
This file was deleted.

app/Console/Kernel.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class Kernel extends ConsoleKernel
4747
\App\Console\Commands\PresentationMediaUploadsRegenerateTemporalLinks::class,
4848
\App\Console\Commands\PurgeAuditLogCommand::class,
4949
\App\Console\Commands\SummitBadgesQREncryptor::class,
50-
\App\Console\Commands\CreateTestDBCommand::class,
50+
\App\Console\Commands\CreateInitialDBCommand::class,
5151
\App\Console\Commands\SeedTestDataCommand::class,
5252
\App\Console\Commands\PublishStreamUpdatesCommand::class,
5353
\App\Console\Commands\PurgeSummitsMarkAsDeletedCommand::class,
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
INSERT INTO DoctrineMigration (version, executed_at) VALUES ('20190422160409', '2019-04-22 16:04:09');
2-
INSERT INTO DoctrineMigration (version, executed_at) VALUES ('20190828142430', '2019-08-28 14:24:30');
3-
INSERT INTO DoctrineMigration (version, executed_at) VALUES ('20190828143005', '2019-08-28 14:30:05');
4-
INSERT INTO DoctrineMigration (version, executed_at) VALUES ('20200123174717', '2020-01-23 17:47:17');
1+
INSERT INTO DoctrineMigration (version, executed_at) VALUES ('Database\\Migrations\\Config\\Version20190422160409', '2019-04-22 16:04:09');
2+
INSERT INTO DoctrineMigration (version, executed_at) VALUES ('Database\\Migrations\\Config\\Version20190828142430', '2019-08-28 14:24:30');
3+
INSERT INTO DoctrineMigration (version, executed_at) VALUES ('Database\\Migrations\\Config\\Version20190828143005', '2019-08-28 14:30:05');
4+
INSERT INTO DoctrineMigration (version, executed_at) VALUES ('Database\\Migrations\\Config\\Version20200123174717', '2020-01-23 17:47:17');

database/migrations/config/initial_schema.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
create table DoctrineMigration
22
(
3-
version varchar(14) not null
3+
version varchar(191) not null
44
primary key,
5-
executed_at datetime not null comment '(DC2Type:datetime_immutable)'
5+
executed_at datetime not null comment '(DC2Type:datetime_immutable)'
66
)
77
collate = utf8mb3_unicode_ci;
88

0 commit comments

Comments
 (0)