Update besar apk finance
This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
<?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(getenv('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' => getenv($prefix . '_HOST'),
|
||||
'port' => (int) getenv($prefix . '_PORT'),
|
||||
'user' => getenv($prefix . '_USERNAME'),
|
||||
'pass' => getenv($prefix . '_PASSWORD'),
|
||||
'db' => getenv($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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user