|null */ private ?array $lastSyncResult = null; private function lockFilePath(): string { return WRITEPATH . 'setup_db_complete.lock'; } private function masterSchemaFilePath(): string { $fromEnv = trim((string) env('setup.sync.master.schema_file', '')); if ($fromEnv !== '') { if (str_starts_with($fromEnv, '/') || preg_match('/^[A-Za-z]:[\\\/]/', $fromEnv) === 1) { return $fromEnv; } return ROOTPATH . ltrim(str_replace(['\\'], '/', $fromEnv), '/'); } return APPPATH . 'Database/master_schema.json'; } private function isSetupLocked(): bool { return is_file($this->lockFilePath()); } private function ensureSetupAccess() { if ($this->isSetupLocked()) { return $this->response->setStatusCode(404)->setBody('Setup is already completed and locked.'); } $keyFromEnv = trim((string) env('setup.dbToolsKey', '')); if ($keyFromEnv !== '') { $keyFromRequest = trim((string) ($this->request->getGet('key') ?? $this->request->getPost('key') ?? '')); if ($keyFromRequest !== '' && hash_equals($keyFromEnv, $keyFromRequest)) { return null; } } if (!function_exists('auth')) { return null; } $user = auth()->user(); if (!$user) { return $this->response->setStatusCode(403)->setBody('Please login to access setup tools.'); } $allowed = false; if (method_exists($user, 'can') && $user->can('billing.opd.edit')) { $allowed = true; } if (!$allowed && method_exists($user, 'inGroup') && $user->inGroup('admin', 'superadmin', 'developer')) { $allowed = true; } if (!$allowed) { return $this->response->setStatusCode(403)->setBody('Access denied.'); } return null; } public function dbTools() { if ($deny = $this->ensureSetupAccess()) { return $deny; } return view('setup/db_setup', $this->buildDbToolsViewData()); } public function schemaSync() { if ($deny = $this->ensureSetupAccess()) { return $deny; } @ini_set('memory_limit', (string) env('setup.sync.memory_limit', '1024M')); @ini_set('max_execution_time', '0'); @set_time_limit(0); @ignore_user_abort(true); $action = strtolower(trim((string) $this->request->getPost('sync_action'))); if (!in_array($action, ['analyze', 'apply'], true)) { $action = 'analyze'; } $syncResult = $this->buildSchemaSyncPlan(); if ($action === 'apply' && empty($syncResult['errors']) && !empty($syncResult['sql'])) { $applyResult = $this->applySchemaSyncSql($syncResult['sql']); $syncResult['applied'] = $applyResult['applied']; $syncResult['apply_errors'] = $applyResult['errors']; } $this->lastSyncResult = $syncResult; return view('setup/db_setup', $this->buildDbToolsViewData()); } public function exportMasterSchema() { if ($deny = $this->ensureSetupAccess()) { return $deny; } $schema = $this->readSchemaFromConnection($this->db); $file = $this->masterSchemaFilePath(); $dir = dirname($file); if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { session()->setFlashdata('setup_msg', 'Unable to create schema file directory.'); return redirect()->to($this->dbToolsUrlWithKey()); } $payload = [ 'generated_at' => date('Y-m-d H:i:s'), 'source' => 'local-current-db', 'tables' => $schema, ]; $json = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); if (!is_string($json) || @file_put_contents($file, $json) === false) { session()->setFlashdata('setup_msg', 'Unable to write master schema file.'); return redirect()->to($this->dbToolsUrlWithKey()); } session()->setFlashdata('setup_msg', 'Master schema exported: ' . $file); return redirect()->to($this->dbToolsUrlWithKey()); } public function generateMigrations() { if ($deny = $this->ensureSetupAccess()) { return $deny; } $selected = $this->request->getPost('tables'); if (!is_array($selected)) { $selected = []; } $selected = array_values(array_unique(array_map(static function ($value): string { $name = strtolower(trim((string) $value)); return preg_replace('/[^a-z0-9_]/', '', $name) ?? ''; }, $selected))); $selected = array_values(array_filter($selected)); if (empty($selected)) { session()->setFlashdata('setup_msg', 'Please select at least one table.'); return redirect()->to($this->dbToolsUrlWithKey()); } $migrationDir = APPPATH . 'Database/Migrations'; if (!is_dir($migrationDir) && !mkdir($migrationDir, 0777, true) && !is_dir($migrationDir)) { session()->setFlashdata('setup_msg', 'Unable to create migration directory.'); return redirect()->to($this->dbToolsUrlWithKey()); } $createdFiles = []; $ts = date('Y-m-d-His'); $counter = 0; foreach ($selected as $tableName) { $showQuery = $this->db->query('SHOW CREATE TABLE `' . $tableName . '`'); $showRow = $showQuery->getRowArray(); if (empty($showRow)) { continue; } $createSql = ''; foreach ($showRow as $key => $value) { if (is_string($key) && stripos($key, 'Create Table') !== false) { $createSql = (string) $value; break; } } if ($createSql === '') { $vals = array_values($showRow); $createSql = (string) ($vals[1] ?? ''); } if ($createSql === '') { continue; } $createSql = $this->ensureCreateTableIfNotExists((string) $createSql); if (!str_ends_with(trim($createSql), ';')) { $createSql .= ';'; } $className = 'CreateExistingTable' . $this->studly($tableName) . date('His') . str_pad((string) $counter, 2, '0', STR_PAD_LEFT); $fileStamp = $ts . str_pad((string) $counter, 2, '0', STR_PAD_LEFT); $fileName = $fileStamp . '_' . $className . '.php'; $counter++; $php = "db->query(<<<'SQL'\n{$createSql}\nSQL\n );\n }\n\n public function down()\n {\n \$this->forge->dropTable('{$tableName}', true);\n }\n}\n"; $path = $migrationDir . DIRECTORY_SEPARATOR . $fileName; if (@file_put_contents($path, $php) !== false) { $createdFiles[] = $fileName; } } if (empty($createdFiles)) { session()->setFlashdata('setup_msg', 'No migration files generated.'); } else { session()->setFlashdata('setup_msg', 'Generated ' . count($createdFiles) . ' migration file(s).'); } return redirect()->to($this->dbToolsUrlWithKey()); } public function complete() { if ($deny = $this->ensureSetupAccess()) { return $deny; } $lockFile = $this->lockFilePath(); $userName = 'unknown'; if (function_exists('auth') && auth()->user()) { $user = auth()->user(); $userName = (string) ($user->username ?? $user->email ?? 'unknown'); } $content = "Setup completed at: " . date('Y-m-d H:i:s') . "\nBy: " . $userName . "\n"; @file_put_contents($lockFile, $content); return $this->response->setBody('Setup locked successfully. You can now remove setup routes/page if required.'); } public function ensureAdminLogin() { if ($deny = $this->ensureSetupAccess()) { return $deny; } $username = trim((string) env('setup.admin.username', 'admin')); $email = trim((string) env('setup.admin.email', 'admin@example.com')); $password = (string) env('setup.admin.password', 'Admin@12345'); $group = strtolower(trim((string) env('setup.admin.group', 'admin'))); if ($username === '') { $username = 'admin'; } if ($email === '' || filter_var($email, FILTER_VALIDATE_EMAIL) === false) { $email = 'admin@example.com'; } if (mb_strlen($password) < 8) { $password = 'Admin@12345'; } try { $userModel = model(UserModel::class); $user = $userModel->where('username', $username)->first(); if ($user === null) { $tables = config('Auth')->tables; $identitiesTable = (string) ($tables['identities'] ?? 'auth_identities'); $idRow = $this->db->table($identitiesTable) ->select('user_id') ->where('type', 'email_password') ->where('secret', $email) ->get(1) ->getRowArray(); if (!empty($idRow['user_id'])) { $user = $userModel->find((int) $idRow['user_id']); } } $action = 'updated'; if ($user === null) { $newUser = new User(); $newUser->username = $username; $newUser->email = $email; $newUser->password = $password; $newUser->active = 1; $userModel->save($newUser); $insertId = (int) $userModel->getInsertID(); $user = $insertId > 0 ? $userModel->find($insertId) : null; $action = 'created'; } else { $user->username = $username; $user->email = $email; $user->password = $password; $user->active = 1; $userModel->save($user); $user = $userModel->find((int) ($user->id ?? 0)); } if ($user !== null && method_exists($user, 'addGroup')) { $groupConfig = setting('AuthGroups.groups'); if (!is_array($groupConfig) || !isset($groupConfig[$group])) { $group = 'admin'; } $user->addGroup($group); } session()->setFlashdata('setup_msg', 'Admin login ' . $action . ': username=' . $username . ', email=' . $email . ', group=' . $group . '.'); } catch (\Throwable $e) { session()->setFlashdata('setup_msg', 'Admin login setup failed: ' . $e->getMessage()); } return redirect()->to($this->dbToolsUrlWithKey()); } /** * @return array */ private function buildDbToolsViewData(): array { $allTables = $this->db->listTables(); $scripted = $this->detectScriptedTables(); $tableRows = []; foreach ($allTables as $tableName) { $isScripted = isset($scripted[strtolower((string) $tableName)]); $tableRows[] = [ 'name' => (string) $tableName, 'scripted' => $isScripted, ]; } usort($tableRows, static function (array $a, array $b): int { if ($a['scripted'] === $b['scripted']) { return strcmp($a['name'], $b['name']); } return $a['scripted'] ? 1 : -1; }); return [ 'tables' => $tableRows, 'msg' => session('setup_msg'), 'lock_file' => $this->lockFilePath(), 'setup_key' => trim((string) env('setup.dbToolsKey', '')), 'sync_result' => $this->lastSyncResult, 'sync_master_database' => (string) env('setup.sync.master.database', ''), 'sync_client_database' => (string) env('setup.sync.client.database', ''), 'master_schema_file' => $this->masterSchemaFilePath(), 'master_schema_exists' => is_file($this->masterSchemaFilePath()), ]; } /** * @return array */ private function buildSchemaSyncPlan(): array { $errors = []; $sql = []; $masterSchema = $this->loadMasterSchemaDefinition(); if (!empty($masterSchema['errors'])) { return [ 'errors' => $masterSchema['errors'], 'sql' => [], 'summary' => [], 'source' => $masterSchema['source'] ?? 'unknown', ]; } try { $client = $this->resolveClientConnection(); } catch (\Throwable $e) { return [ 'errors' => [$e->getMessage()], 'sql' => [], 'summary' => [], 'source' => (string) ($masterSchema['source'] ?? 'unknown'), ]; } $masterTables = $masterSchema['tables'] ?? []; $clientTables = array_flip($client->listTables()); $maxTables = (int) env('setup.sync.max_tables_per_run', 150); if ($maxTables <= 0) { $maxTables = 150; } $processedTables = 0; $truncated = false; $createCount = 0; $alterCount = 0; foreach ($masterTables as $table => $masterTableDef) { if ($processedTables >= $maxTables) { $truncated = true; break; } $table = (string) $table; if (!isset($clientTables[$table])) { $createSql = (string) ($masterTableDef['create_sql'] ?? ''); if ($createSql !== '') { $createSql = $this->ensureCreateTableIfNotExists((string) $createSql); if (!str_ends_with($createSql, ';')) { $createSql .= ';'; } } if ($createSql !== '') { $sql[] = $createSql; $createCount++; } continue; } $tableAlterSql = $this->buildAlterSqlForTableFromSchema((array) $masterTableDef, $client, $table); if (!empty($tableAlterSql)) { foreach ($tableAlterSql as $stmt) { $sql[] = $stmt; } $alterCount += count($tableAlterSql); } $processedTables++; } return [ 'errors' => $errors, 'sql' => $sql, 'summary' => [ 'master_tables' => count((array) $masterTables), 'processed_tables' => $processedTables, 'max_tables_per_run' => $maxTables, 'create_tables' => $createCount, 'alter_statements' => $alterCount, ], 'source' => (string) ($masterSchema['source'] ?? 'master-schema-file'), 'truncated' => $truncated, ]; } /** * @param array $sqlList * @return array{applied:int,errors:array} */ private function applySchemaSyncSql(array $sqlList): array { $client = $this->resolveClientConnection(); $applied = 0; $errors = []; foreach ($sqlList as $stmt) { $stmt = $this->normalizeSyncSql((string) $stmt); if ($stmt === '') { continue; } try { $client->query($stmt); $applied++; } catch (\Throwable $e) { $message = (string) $e->getMessage(); if ($this->isSafeToSkipSyncStatement($message, $stmt)) { continue; } if ($this->isCreateTableStatement($stmt) && stripos($message, 'Cannot add foreign key constraint') !== false) { $retry = $this->stripForeignKeysFromCreateTable($stmt); if ($retry !== $stmt && $retry !== '') { try { $client->query($retry); $applied++; continue; } catch (\Throwable $retryError) { $message = (string) $retryError->getMessage(); } } } $errors[] = $message . ' | SQL: ' . mb_substr($stmt, 0, 180); } } return ['applied' => $applied, 'errors' => $errors]; } /** * @return array */ private function loadMasterSchemaDefinition(): array { if ($this->hasSyncRoleConfig('master')) { try { $masterDb = $this->connectSyncDb('master'); return [ 'errors' => [], 'tables' => $this->readSchemaFromConnection($masterDb), 'source' => 'master-db-connection', ]; } catch (\Throwable $e) { return [ 'errors' => ['Master DB connection failed: ' . $e->getMessage()], 'tables' => [], 'source' => 'master-db-connection', ]; } } $file = $this->masterSchemaFilePath(); if (!is_file($file)) { return [ 'errors' => ['Master schema file not found: ' . $file . '. Use "Export Master Schema File" first on master system.'], 'tables' => [], 'source' => 'master-schema-file', ]; } $json = @file_get_contents($file); $decoded = is_string($json) ? json_decode($json, true) : null; if (!is_array($decoded) || !is_array($decoded['tables'] ?? null)) { return [ 'errors' => ['Invalid master schema file format: ' . $file], 'tables' => [], 'source' => 'master-schema-file', ]; } return [ 'errors' => [], 'tables' => $decoded['tables'], 'source' => 'master-schema-file', ]; } private function hasSyncRoleConfig(string $role): bool { $role = strtolower(trim($role)); $host = trim((string) env('setup.sync.' . $role . '.hostname', '')); $user = trim((string) env('setup.sync.' . $role . '.username', '')); $db = trim((string) env('setup.sync.' . $role . '.database', '')); return $host !== '' && $user !== '' && $db !== ''; } private function resolveClientConnection() { if ($this->hasSyncRoleConfig('client')) { return $this->connectSyncDb('client'); } return $this->db; } /** * @return array> */ private function readSchemaFromConnection($db): array { $tables = $db->listTables(); $out = []; foreach ($tables as $tableName) { $table = (string) $tableName; $out[$table] = [ 'create_sql' => $this->getCreateTableSql($db, $table), 'columns' => $this->readColumns($db, $table), 'indexes' => $this->readIndexes($db, $table), ]; } return $out; } /** * @return array */ private function buildAlterSqlForTableFromSchema(array $masterTableDef, $client, string $table): array { $sql = []; $masterCols = is_array($masterTableDef['columns'] ?? null) ? $masterTableDef['columns'] : []; $clientCols = $this->readColumns($client, $table); $prevCol = ''; foreach ($masterCols as $colName => $masterCol) { if (!isset($clientCols[$colName])) { $position = $prevCol === '' ? ' FIRST' : (' AFTER `' . $prevCol . '`'); $sql[] = 'ALTER TABLE `' . $table . '` ADD COLUMN ' . $this->columnDefinition($masterCol) . $position . ';'; $prevCol = $colName; continue; } if ($this->columnSignature($masterCol) !== $this->columnSignature($clientCols[$colName])) { $sql[] = 'ALTER TABLE `' . $table . '` MODIFY COLUMN ' . $this->columnDefinition($masterCol) . ';'; } $prevCol = $colName; } $masterIdx = is_array($masterTableDef['indexes'] ?? null) ? $masterTableDef['indexes'] : []; $clientIdx = $this->readIndexes($client, $table); foreach ($masterIdx as $keyName => $masterDef) { if (!isset($clientIdx[$keyName])) { $sql[] = 'ALTER TABLE `' . $table . '` ADD ' . $this->indexDefinitionSql($masterDef) . ';'; continue; } if ($this->indexSignature($masterDef) !== $this->indexSignature($clientIdx[$keyName])) { if ($keyName === 'PRIMARY') { $sql[] = 'ALTER TABLE `' . $table . '` DROP PRIMARY KEY;'; } else { $sql[] = 'ALTER TABLE `' . $table . '` DROP INDEX `' . $keyName . '`;'; } $sql[] = 'ALTER TABLE `' . $table . '` ADD ' . $this->indexDefinitionSql($masterDef) . ';'; } } return $sql; } private function connectSyncDb(string $role) { $role = strtolower(trim($role)); $cfg = [ 'DSN' => '', 'hostname' => (string) env('setup.sync.' . $role . '.hostname', ''), 'username' => (string) env('setup.sync.' . $role . '.username', ''), 'password' => (string) env('setup.sync.' . $role . '.password', ''), 'database' => (string) env('setup.sync.' . $role . '.database', ''), 'DBDriver' => (string) env('setup.sync.' . $role . '.dbdriver', 'MySQLi'), 'DBPrefix' => '', 'pConnect' => false, 'DBDebug' => true, 'charset' => (string) env('setup.sync.' . $role . '.charset', 'utf8mb4'), 'DBCollat' => (string) env('setup.sync.' . $role . '.dbcollat', 'utf8mb4_general_ci'), 'swapPre' => '', 'encrypt' => false, 'compress' => false, 'strictOn' => false, 'failover' => [], 'port' => (int) env('setup.sync.' . $role . '.port', 3306), ]; if ($cfg['hostname'] === '' || $cfg['username'] === '' || $cfg['database'] === '') { throw new \RuntimeException('Missing setup.sync.' . $role . ' database config in .env'); } return \Config\Database::connect($cfg, false); } private function dbToolsUrlWithKey(): string { $url = base_url('setup/db-tools'); $key = trim((string) env('setup.dbToolsKey', '')); if ($key !== '') { $url .= '?key=' . urlencode($key); } return $url; } private function getCreateTableSql($db, string $table): string { $row = $db->query('SHOW CREATE TABLE `' . $table . '`')->getRowArray(); if (empty($row)) { return ''; } $create = ''; foreach ($row as $key => $val) { if (is_string($key) && stripos($key, 'Create Table') !== false) { $create = (string) $val; break; } } if ($create === '') { $values = array_values($row); $create = (string) ($values[1] ?? ''); } if ($create === '') { return ''; } $create = $this->ensureCreateTableIfNotExists((string) $create); if (!str_ends_with($create, ';')) { $create .= ';'; } return $create; } /** * @return array> */ private function readColumns($db, string $table): array { $rows = $db->query('SHOW FULL COLUMNS FROM `' . $table . '`')->getResultArray(); $out = []; foreach ($rows as $row) { $name = (string) ($row['Field'] ?? ''); if ($name !== '') { $out[$name] = $row; } } return $out; } /** * @return array> */ private function readIndexes($db, string $table): array { $rows = $db->query('SHOW INDEX FROM `' . $table . '`')->getResultArray(); $out = []; foreach ($rows as $row) { $key = (string) ($row['Key_name'] ?? ''); if ($key === '') { continue; } if (!isset($out[$key])) { $out[$key] = [ 'name' => $key, 'unique' => ((int) ($row['Non_unique'] ?? 1)) === 0, 'type' => strtoupper((string) ($row['Index_type'] ?? 'BTREE')), 'columns' => [], ]; } $seq = (int) ($row['Seq_in_index'] ?? 0); $out[$key]['columns'][$seq] = (string) ($row['Column_name'] ?? ''); } foreach ($out as $key => $def) { ksort($def['columns']); $def['columns'] = array_values(array_filter($def['columns'])); $out[$key] = $def; } return $out; } private function columnDefinition(array $col): string { $name = (string) ($col['Field'] ?? ''); $type = (string) ($col['Type'] ?? 'text'); $null = strtoupper((string) ($col['Null'] ?? 'YES')) === 'NO' ? ' NOT NULL' : ' NULL'; $extra = trim((string) ($col['Extra'] ?? '')); $default = $this->columnDefaultSql($col); $collation = trim((string) ($col['Collation'] ?? '')); $comment = trim((string) ($col['Comment'] ?? '')); $applyCollation = strtolower(trim((string) env('setup.sync.apply_column_collation', 'false'))); $canApplyCollation = in_array($applyCollation, ['1', 'true', 'yes', 'on'], true); $sql = '`' . $name . '` ' . $type; if ($collation !== '' && $canApplyCollation) { $sql .= ' COLLATE ' . $this->normalizeCollationName($collation); } $sql .= $null . $default; if ($extra !== '') { $extra = preg_replace('/\bDEFAULT_GENERATED\b/i', '', $extra) ?? $extra; $extra = trim(preg_replace('/\s+/', ' ', $extra) ?? $extra); if ($extra !== '') { $sql .= ' ' . strtoupper($extra); } } if ($comment !== '') { $sql .= " COMMENT '" . str_replace("'", "''", $comment) . "'"; } return $sql; } private function columnDefaultSql(array $col): string { if (!array_key_exists('Default', $col)) { return ''; } $default = $col['Default']; if ($default === null) { $isNotNull = strtoupper((string) ($col['Null'] ?? 'YES')) === 'NO'; return $isNotNull ? '' : ' DEFAULT NULL'; } $defaultStr = (string) $default; $upper = strtoupper($defaultStr); if ($upper === 'CURRENT_TIMESTAMP' || str_starts_with($upper, 'CURRENT_TIMESTAMP(')) { return ' DEFAULT ' . $defaultStr; } return " DEFAULT '" . str_replace("'", "''", $defaultStr) . "'"; } private function columnSignature(array $col): string { return json_encode([ strtolower((string) ($col['Type'] ?? '')), strtoupper((string) ($col['Null'] ?? 'YES')), (string) ($col['Default'] ?? ''), strtolower((string) ($col['Extra'] ?? '')), (string) ($col['Comment'] ?? ''), ]) ?: ''; } private function ensureCreateTableIfNotExists(string $sql): string { $sql = trim($sql); if ($sql === '') { return ''; } $sql = preg_replace('/^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?/i', 'CREATE TABLE IF NOT EXISTS ', $sql, 1) ?? $sql; return $this->normalizeSyncSql($sql); } private function normalizeCollationName(string $collation): string { $collation = trim($collation); if ($collation === '') { return ''; } if (preg_match('/^utf8mb4_0900_/i', $collation) === 1) { return (string) env('setup.sync.collation_fallback', 'utf8mb4_general_ci'); } return $collation; } private function normalizeSyncSql(string $sql): string { $sql = trim($sql); if ($sql === '') { return ''; } $fallbackCollation = $this->normalizeCollationName('utf8mb4_0900_ai_ci'); $sql = preg_replace('/\bDEFAULT_GENERATED\b/i', '', $sql) ?? $sql; $sql = preg_replace('/\butf8mb4_0900_[a-z0-9_]+\b/i', $fallbackCollation, $sql) ?? $sql; $sql = preg_replace('/^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)+/i', 'CREATE TABLE IF NOT EXISTS ', $sql, 1) ?? $sql; $sql = preg_replace('/\s+/', ' ', $sql) ?? $sql; $sql = preg_replace('/\s+;$/', ';', $sql) ?? $sql; return trim($sql); } private function isCreateTableStatement(string $stmt): bool { return preg_match('/^CREATE\s+TABLE\s+/i', ltrim($stmt)) === 1; } private function isSafeToSkipSyncStatement(string $message, string $stmt): bool { $messageLower = strtolower($message); $stmtLower = strtolower($stmt); if (str_contains($messageLower, 'used in a foreign key constraint') && str_contains($stmtLower, 'alter table')) { return true; } if (str_contains($messageLower, 'blob/text column') && str_contains($messageLower, 'key specification without a key length')) { return true; } return false; } private function stripForeignKeysFromCreateTable(string $stmt): string { $out = $stmt; $patterns = [ '/\s*,\s*CONSTRAINT\s+`[^`]+`\s+FOREIGN\s+KEY\s*\([^\)]*\)\s*REFERENCES\s*`[^`]+`\s*\([^\)]*\)(?:\s+ON\s+DELETE\s+[A-Z_ ]+)?(?:\s+ON\s+UPDATE\s+[A-Z_ ]+)?/i', '/\s*,\s*FOREIGN\s+KEY\s*\([^\)]*\)\s*REFERENCES\s*`[^`]+`\s*\([^\)]*\)(?:\s+ON\s+DELETE\s+[A-Z_ ]+)?(?:\s+ON\s+UPDATE\s+[A-Z_ ]+)?/i', ]; foreach ($patterns as $pattern) { $out = preg_replace($pattern, '', $out) ?? $out; } $out = preg_replace('/,\s*\)/', ')', $out) ?? $out; $out = preg_replace('/\s+/', ' ', $out) ?? $out; $out = trim($out); if ($out !== '' && !str_ends_with($out, ';')) { $out .= ';'; } return $this->normalizeSyncSql($out); } private function indexDefinitionSql(array $idx): string { $name = (string) ($idx['name'] ?? ''); $columns = array_values((array) ($idx['columns'] ?? [])); $columnSql = implode(',', array_map(static fn ($c) => '`' . $c . '`', $columns)); if ($name === 'PRIMARY') { return 'PRIMARY KEY (' . $columnSql . ')'; } $type = strtoupper((string) ($idx['type'] ?? 'BTREE')); $unique = (bool) ($idx['unique'] ?? false); if ($type === 'FULLTEXT') { return 'FULLTEXT KEY `' . $name . '` (' . $columnSql . ')'; } if ($type === 'SPATIAL') { return 'SPATIAL KEY `' . $name . '` (' . $columnSql . ')'; } if ($unique) { return 'UNIQUE KEY `' . $name . '` (' . $columnSql . ')'; } return 'KEY `' . $name . '` (' . $columnSql . ')'; } private function indexSignature(array $idx): string { return json_encode([ (string) ($idx['name'] ?? ''), (bool) ($idx['unique'] ?? false), strtoupper((string) ($idx['type'] ?? 'BTREE')), array_values((array) ($idx['columns'] ?? [])), ]) ?: ''; } /** * @return array */ private function detectScriptedTables(): array { $result = []; $files = glob(APPPATH . 'Database/Migrations/*.php') ?: []; foreach ($files as $filePath) { $content = @file_get_contents($filePath); if (!is_string($content) || $content === '') { continue; } if (preg_match_all('/createTable\(\s*[\'\"]([a-zA-Z0-9_]+)[\'\"]/i', $content, $m1) === 1 || !empty($m1[1])) { foreach (($m1[1] ?? []) as $table) { $result[strtolower((string) $table)] = true; } } if (preg_match_all('/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?([a-zA-Z0-9_]+)`?/i', $content, $m2) === 1 || !empty($m2[1])) { foreach (($m2[1] ?? []) as $table) { $result[strtolower((string) $table)] = true; } } } return $result; } private function studly(string $value): string { $value = preg_replace('/[^a-zA-Z0-9]+/', ' ', $value) ?? $value; $value = ucwords(strtolower(trim($value))); return str_replace(' ', '', $value); } }