sourceConfig = $this->readConnectionConfig('DBCOMPARE_SOURCE'); $this->targetConfig = $this->readConnectionConfig('DBCOMPARE_TARGET'); } private function readConnectionConfig($prefix) { $config = array( 'host' => app_env($prefix . '_HOST', ''), 'port' => (int) app_env($prefix . '_PORT', 3306), 'user' => app_env($prefix . '_USERNAME', ''), 'pass' => app_env($prefix . '_PASSWORD', ''), 'db' => app_env($prefix . '_DATABASE', '') ); foreach (array('host', 'port', 'user', 'pass', 'db') as $key) { if (empty($config[$key])) { show_error('Konfigurasi Dbcompare belum lengkap.', 503); } } return $config; } public function sql() { $result = $this->buildSqlDiff(); header('Content-Type: text/plain; charset=utf-8'); echo $result['sql']; } public function sql_download() { $result = $this->buildSqlDiff(); $filename = 'sync_structure_' . date('Ymd_His') . '.sql'; header('Content-Type: application/sql; charset=utf-8'); header('Content-Disposition: attachment; filename="' . $filename . '"'); echo $result['sql']; } private function buildSqlDiff() { $source = $this->connectMysqli($this->sourceConfig); $target = $this->connectMysqli($this->targetConfig); $sqlLines = array(); $sqlLines[] = '-- =================================================='; $sqlLines[] = '-- AUTO GENERATED SQL - STRUCTURE SYNC'; $sqlLines[] = '-- Source DB : ' . $this->sourceConfig['db'] . ' (' . $this->sourceConfig['host'] . ':' . $this->sourceConfig['port'] . ')'; $sqlLines[] = '-- Target DB : ' . $this->targetConfig['db'] . ' (' . $this->targetConfig['host'] . ':' . $this->targetConfig['port'] . ')'; $sqlLines[] = '-- Generated : ' . date('Y-m-d H:i:s'); $sqlLines[] = '-- Notes : Hanya struktur (table/column/primary key/index), tanpa data'; $sqlLines[] = '-- =================================================='; $sqlLines[] = ''; $sourceTables = $this->getTables($source, $this->sourceConfig['db']); $targetTables = $this->getTables($target, $this->targetConfig['db']); foreach ($sourceTables as $tableName) { $safeTable = $this->escapeIdentifier($tableName); if (!in_array($tableName, $targetTables, true)) { $createSql = $this->getCreateTableSql($source, $tableName); $sqlLines[] = '-- --------------------------------------------------'; $sqlLines[] = '-- CREATE TABLE: ' . $tableName; $sqlLines[] = '-- --------------------------------------------------'; $sqlLines[] = $createSql . ';'; $sqlLines[] = ''; continue; } $sqlLines[] = '-- --------------------------------------------------'; $sqlLines[] = '-- ALTER TABLE: ' . $tableName; $sqlLines[] = '-- --------------------------------------------------'; $sourceColumns = $this->getColumns($source, $this->sourceConfig['db'], $tableName); $targetColumns = $this->getColumns($target, $this->targetConfig['db'], $tableName); // Column ADD / MODIFY foreach ($sourceColumns as $columnName => $sourceCol) { if (!isset($targetColumns[$columnName])) { $sqlLines[] = 'ALTER TABLE ' . $safeTable . ' ADD COLUMN ' . $this->buildColumnDefinition($sourceCol) . ';'; continue; } $targetCol = $targetColumns[$columnName]; if (!$this->isSameColumn($sourceCol, $targetCol)) { $sqlLines[] = 'ALTER TABLE ' . $safeTable . ' MODIFY COLUMN ' . $this->buildColumnDefinition($sourceCol) . ';'; } } // Primary key sync $sourcePk = $this->getPrimaryKeyColumns($source, $this->sourceConfig['db'], $tableName); $targetPk = $this->getPrimaryKeyColumns($target, $this->targetConfig['db'], $tableName); if ($sourcePk !== $targetPk) { if (!empty($targetPk)) { $sqlLines[] = 'ALTER TABLE ' . $safeTable . ' DROP PRIMARY KEY;'; } if (!empty($sourcePk)) { $pkCols = array(); foreach ($sourcePk as $pkCol) { $pkCols[] = $this->escapeIdentifier($pkCol); } $sqlLines[] = 'ALTER TABLE ' . $safeTable . ' ADD PRIMARY KEY (' . implode(', ', $pkCols) . ');'; } } // Secondary indexes sync (add missing / different composition) $sourceIndexes = $this->getIndexes($source, $this->sourceConfig['db'], $tableName); $targetIndexes = $this->getIndexes($target, $this->targetConfig['db'], $tableName); foreach ($sourceIndexes as $idxName => $sourceIdx) { if ($idxName === 'PRIMARY') { continue; } $needCreate = false; if (!isset($targetIndexes[$idxName])) { $needCreate = true; } else { $targetIdx = $targetIndexes[$idxName]; if (!$this->isSameIndex($sourceIdx, $targetIdx)) { $sqlLines[] = 'ALTER TABLE ' . $safeTable . ' DROP INDEX ' . $this->escapeIdentifier($idxName) . ';'; $needCreate = true; } } if ($needCreate) { $idxCols = array(); foreach ($sourceIdx['columns'] as $colName) { $idxCols[] = $this->escapeIdentifier($colName); } $idxType = $sourceIdx['non_unique'] == 0 ? 'ADD UNIQUE INDEX ' : 'ADD INDEX '; $sqlLines[] = 'ALTER TABLE ' . $safeTable . ' ' . $idxType . $this->escapeIdentifier($idxName) . ' (' . implode(', ', $idxCols) . ');'; } } $sqlLines[] = ''; } $source->close(); $target->close(); return array('sql' => implode("\n", $sqlLines)); } private function connectMysqli($config) { $conn = @new mysqli($config['host'], $config['user'], $config['pass'], $config['db'], (int)$config['port']); if ($conn->connect_errno) { show_error('Gagal konek DB ' . $config['db'] . ': ' . $conn->connect_error, 500); } $conn->set_charset('utf8'); return $conn; } private function getTables($conn, $schema) { $sql = "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME"; $stmt = $conn->prepare($sql); $stmt->bind_param('s', $schema); $stmt->execute(); $res = $stmt->get_result(); $tables = array(); while ($row = $res->fetch_assoc()) { $tables[] = $row['TABLE_NAME']; } $stmt->close(); return $tables; } private function getCreateTableSql($conn, $tableName) { $sql = 'SHOW CREATE TABLE ' . $this->escapeIdentifier($tableName); $res = $conn->query($sql); if (!$res) { return '-- ERROR: gagal ambil CREATE TABLE untuk ' . $tableName; } $row = $res->fetch_assoc(); return isset($row['Create Table']) ? $row['Create Table'] : '-- ERROR: format SHOW CREATE TABLE tidak valid'; } private function getColumns($conn, $schema, $tableName) { $sql = "SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA, COLUMN_COMMENT, CHARACTER_SET_NAME, COLLATION_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION"; $stmt = $conn->prepare($sql); $stmt->bind_param('ss', $schema, $tableName); $stmt->execute(); $res = $stmt->get_result(); $columns = array(); while ($row = $res->fetch_assoc()) { $columns[$row['COLUMN_NAME']] = $row; } $stmt->close(); return $columns; } private function getPrimaryKeyColumns($conn, $schema, $tableName) { $sql = "SELECT kcu.COLUMN_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA AND tc.TABLE_NAME = kcu.TABLE_NAME WHERE tc.TABLE_SCHEMA = ? AND tc.TABLE_NAME = ? AND tc.CONSTRAINT_TYPE = 'PRIMARY KEY' ORDER BY kcu.ORDINAL_POSITION"; $stmt = $conn->prepare($sql); $stmt->bind_param('ss', $schema, $tableName); $stmt->execute(); $res = $stmt->get_result(); $pk = array(); while ($row = $res->fetch_assoc()) { $pk[] = $row['COLUMN_NAME']; } $stmt->close(); return $pk; } private function getIndexes($conn, $schema, $tableName) { $sql = "SELECT INDEX_NAME, NON_UNIQUE, SEQ_IN_INDEX, COLUMN_NAME FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ORDER BY INDEX_NAME, SEQ_IN_INDEX"; $stmt = $conn->prepare($sql); $stmt->bind_param('ss', $schema, $tableName); $stmt->execute(); $res = $stmt->get_result(); $indexes = array(); while ($row = $res->fetch_assoc()) { $idxName = $row['INDEX_NAME']; if (!isset($indexes[$idxName])) { $indexes[$idxName] = array( 'non_unique' => (int)$row['NON_UNIQUE'], 'columns' => array() ); } $indexes[$idxName]['columns'][] = $row['COLUMN_NAME']; } $stmt->close(); return $indexes; } private function buildColumnDefinition($col) { $def = $this->escapeIdentifier($col['COLUMN_NAME']) . ' ' . $col['COLUMN_TYPE']; $def .= ($col['IS_NULLABLE'] === 'NO') ? ' NOT NULL' : ' NULL'; if ($col['COLUMN_DEFAULT'] !== null) { if (strtoupper($col['COLUMN_DEFAULT']) === 'CURRENT_TIMESTAMP') { $def .= ' DEFAULT CURRENT_TIMESTAMP'; } else { $def .= " DEFAULT '" . $this->escapeStringValue($col['COLUMN_DEFAULT']) . "'"; } } elseif ($col['IS_NULLABLE'] === 'YES') { $def .= ' DEFAULT NULL'; } if (!empty($col['EXTRA'])) { $def .= ' ' . strtoupper($col['EXTRA']); } if (!empty($col['COLUMN_COMMENT'])) { $def .= " COMMENT '" . $this->escapeStringValue($col['COLUMN_COMMENT']) . "'"; } return $def; } private function isSameColumn($a, $b) { return ( strtolower((string)$a['COLUMN_TYPE']) === strtolower((string)$b['COLUMN_TYPE']) && (string)$a['IS_NULLABLE'] === (string)$b['IS_NULLABLE'] && (string)$a['COLUMN_DEFAULT'] === (string)$b['COLUMN_DEFAULT'] && strtolower((string)$a['EXTRA']) === strtolower((string)$b['EXTRA']) && (string)$a['COLUMN_COMMENT'] === (string)$b['COLUMN_COMMENT'] ); } private function isSameIndex($a, $b) { return ( (int)$a['non_unique'] === (int)$b['non_unique'] && implode(',', $a['columns']) === implode(',', $b['columns']) ); } private function escapeIdentifier($name) { return '`' . str_replace('`', '``', $name) . '`'; } private function escapeStringValue($str) { return str_replace("'", "''", (string)$str); } }