|
| 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 | +} |
0 commit comments