Files
accounting_dev_v2/application/controllers/Dbcompare.php
T
2026-09-11 18:06:17 +07:00

339 lines
12 KiB
PHP

<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Dbcompare extends MY_Admin_Controller
{
private $sourceConfig;
private $targetConfig;
public function __construct()
{
parent::__construct();
if (!filter_var(app_env('DBCOMPARE_ENABLED', 'false'), FILTER_VALIDATE_BOOLEAN)) {
show_404();
}
$this->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);
}
}