znt/misc/difftable.php

535 lines
19 KiB
PHP
Raw Normal View History

<?php
/*
* 这个脚本用来比较两个数据库表的结构差异,并生成相应的SQL语句来同步它们。
* 从两个文件中读取表结构,比较差异,并输出同步SQL语句。
* 比较的表结构文件格式为MySQL的CREATE TABLE语句。
* 比较差异时遵循如下规则:
* 1. 如果发现源表有而目标表没有的列,则不作处理。
* 2. 如果发现目标表有而源表没有的列,则生成ADD COLUMN语句。
* 3. 如果发现列定义不同,则生成ALTER COLUMN语句。
* - 不考虑列顺序的不同。
* - 如果数据类型不同、长度不同、是否允许NULL不同或默认值不同,则视为不同。
* - 如果源表的字段取值范围更大,则视为数据类型和长度相同,仅比较是否允许NULL和默认值。生成ALTER COLUMN语句时,仅修改允许NULL和默认值。
* - 如果源表字段是 ENUM,目标表不是 ENUM:
* - 若目标是 VARCHAR 且长度 >= 最长枚举值 → 视为兼容,仅比较 NULL/DEFAULT;
* - 否则 → 视为不兼容,生成 MODIFY 为 VARCHAR(N) 的语句;
* - 忽略索引和约束的差异。
* 使用方法:php diffTable.php source_db_file target_db_file [--debug]
* 其中 source_db_file 和 target_db_file 是包含 CREATE TABLE 语句的文本文件。--debug 可选参数,用于输出调试信息。
* 例如:
* php diffTable.php db/standard/zentao21.7.7.sql db/standard/zentao21.7.8.sql --debug
*/
$debug = in_array('--debug', $argv);
function logDebug($msg)
{
global $debug;
if($debug)
{
echo "[DEBUG] " . $msg . "\n";
}
}
function parseCreateTable($createTableSql)
{
$tables = [];
$sqls = explode(";", $createTableSql);
foreach($sqls as $sql)
{
$sql = trim($sql);
if($sql === '') continue;
if(stripos($sql, 'CREATE TABLE') === 0)
{
if(preg_match('/CREATE TABLE `([^`]+)` \((.*)\) ENGINE=InnoDB\s*/s', $sql, $matches))
{
$tableName = $matches[1];
$columnsDef = $matches[2];
logDebug("Found table: $tableName");
$columns = parseColumns($columnsDef);
$tables[$tableName] = $columns;
}
else
{
logDebug("Failed to parse CREATE TABLE: " . substr($sql, 0, 100) . "...");
}
}
}
logDebug("Parsed " . count($tables) . " tables from SQL");
return $tables;
}
function parseColumns($columnsDef)
{
$columns = [];
$lines = explode("\n", trim($columnsDef));
foreach($lines as $line)
{
$line = trim($line, " ,");
if(preg_match('/^`([^`]+)` (.+)$/', $line, $matches))
{
$columnName = $matches[1];
$columnDef = $matches[2];
$columns[$columnName] = parseColumnDefinition($columnDef);
}
}
return $columns;
}
function parseColumnDefinition($columnDef)
{
$definition = [];
// 解析 ENUM
if(preg_match("/^ENUM\s*\((.+)\)/i", $columnDef, $enumMatches))
{
$definition['type'] = 'ENUM';
$definition['length'] = null;
preg_match_all("/'([^']*)'|\"([^\"]*)\"/", $enumMatches[1], $values);
$definition['enumValues'] = array_unique(array_filter(array_merge($values[1], $values[2]), function($v)
{
return $v !== '';
}));
$definition['unsigned'] = false;
logDebug("Parsed ENUM: " . json_encode($definition['enumValues']));
}
elseif(preg_match('/^([a-zA-Z]+)(\(([^)]+)\))?/', $columnDef, $matches))
{
$definition['type'] = strtoupper($matches[1]);
$definition['length'] = isset($matches[3]) ? $matches[3] : null;
$definition['enumValues'] = null;
$definition['unsigned'] = stripos($columnDef, 'unsigned') !== false;
}
else
{
$definition['type'] = 'UNKNOWN';
$definition['length'] = null;
$definition['enumValues'] = null;
$definition['unsigned'] = false;
logDebug("Unknown column definition: $columnDef");
}
$definition['nullable'] = stripos($columnDef, 'NOT NULL') === false;
if(preg_match('/DEFAULT\s+(NULL|\'[^\']*\'|"[^"]*"|[\w\.\-]+)/i', $columnDef, $matches))
{
$rawDefault = $matches[1];
if(strtoupper(trim($rawDefault)) === 'NULL')
{
$definition['default'] = null;
}
else
{
$definition['default'] = trim($rawDefault, " '\"");
}
}
else
{
$definition['default'] = null;
}
return $definition;
}
// 辅助:获取 ENUM 最长值长度
function getLongestEnumLength($enumValues)
{
if(empty($enumValues)) return 1;
return max(array_map('strlen', $enumValues));
}
function isIntegerType($type)
{
$type = strtoupper($type);
return in_array($type, ['TINYINT', 'SMALLINT', 'MEDIUMINT', 'INT', 'INTEGER', 'BIGINT']);
}
function isFloatType($type)
{
$type = strtoupper($type);
return in_array($type, ['FLOAT', 'DOUBLE', 'DECIMAL', 'NUMERIC']);
}
function isNumericType($type)
{
$type = strtoupper($type);
return in_array($type, [
'TINYINT', 'SMALLINT', 'MEDIUMINT', 'INT', 'INTEGER',
'BIGINT', 'FLOAT', 'DOUBLE', 'DECIMAL', 'NUMERIC'
]);
}
// 判断是否兼容(源 >= 目标)
function isCompatibleColumn($sourceDef, $targetDef)
{
$sourceType = strtoupper($sourceDef['type']);
$targetType = strtoupper($targetDef['type']);
// 情况1:源是 ENUM
if($sourceType === 'ENUM')
{
if($targetType === 'ENUM')
{
// 目标也是 ENUM:检查是否包含所有源值
if(!$targetDef['enumValues']) return false;
$missing = array_diff($sourceDef['enumValues'], $targetDef['enumValues']);
if(!empty($missing))
{
logDebug("ENUM missing values in target: " . json_encode($missing));
return false;
}
return true;
}
else
{
// 目标不是 ENUM
if($targetType === 'VARCHAR')
{
$targetLen = (int)($targetDef['length'] ?? 0);
$requiredLen = getLongestEnumLength($sourceDef['enumValues']);
$compatible = $targetLen >= $requiredLen;
logDebug("ENUM -> VARCHAR: required=$requiredLen, target=$targetLen, compatible=$compatible");
return $compatible;
}
logDebug("ENUM -> non-VARCHAR ($targetType): not compatible");
// 其他类型(如 TINYINT)视为不兼容
return false;
}
}
// 情况2:源不是 ENUM,走原兼容逻辑
$numericHierarchy = [
'TINYINT' => 1, 'SMALLINT' => 2, 'MEDIUMINT' => 3,
'INT' => 4, 'INTEGER' => 4, 'BIGINT' => 5,
'FLOAT' => 6, 'DOUBLE' => 7, 'DECIMAL' => 8
];
$stringHierarchy = [
'CHAR' => 1, 'VARCHAR' => 2, 'TINYTEXT' => 3,
'TEXT' => 4, 'MEDIUMTEXT' => 5, 'LONGTEXT' => 6
];
if(isset($numericHierarchy[$sourceType]) && isset($numericHierarchy[$targetType]))
{
if($numericHierarchy[$sourceType] > $numericHierarchy[$targetType]) return true;
if($numericHierarchy[$sourceType] === $numericHierarchy[$targetType])
{
if(isIntegerType($sourceType))
{
return $sourceDef['unsigned'] === $targetDef['unsigned'];
}
if(isFloatType($sourceType))
{
$sourceLen = explode(',', $sourceDef['length'] ?? '');
$sourceInteger = (int)($sourceLen[0] ?? 0);
$sourceDecimal = (int)($sourceLen[1] ?? 0);
$targetLen = explode(',', $targetDef['length'] ?? '');
$targetInteger = (int)($targetLen[0] ?? 0);
$targetDecimal = (int)($targetLen[1] ?? 0);
return $sourceInteger >= $targetInteger && $sourceDecimal >= $targetDecimal;
}
}
}
if(in_array($sourceType, ['CHAR','VARCHAR']) && in_array($targetType, ['CHAR','VARCHAR']))
{
return (int)($sourceDef['length'] ?? 0) > (int)($targetDef['length'] ?? 0);
}
if(isset($stringHierarchy[$sourceType]) && isset($stringHierarchy[$targetType]))
{
return $stringHierarchy[$sourceType] >= $stringHierarchy[$targetType];
}
return false;
}
function compareColumnDefinitions($sourceDef, $targetDef)
{
$sourceType = strtoupper($sourceDef['type']);
$targetType = strtoupper($targetDef['type']);
// 特殊处理 ENUM
if($sourceType === 'ENUM')
{
if($sourceType !== $targetType)
{
logDebug("ENUM type mismatch: source=$sourceType, target=$targetType");
return false; // 类型不同,直接不等
}
// 同为 ENUM,检查兼容性
if(isCompatibleColumn($sourceDef, $targetDef))
{
$eq = ($sourceDef['nullable'] === $targetDef['nullable'] && $sourceDef['default'] === $targetDef['default']);
logDebug("ENUM compatible, NULL/DEFAULT equal: " . ($eq ? 'YES' : 'NO'));
return $eq;
}
return false; // 不兼容
}
// 非 ENUM:使用通用兼容逻辑
if(isCompatibleColumn($sourceDef, $targetDef))
{
$eq = ($sourceDef['nullable'] === $targetDef['nullable'] && $sourceDef['default'] === $targetDef['default']);
logDebug("Compatible type, NULL/DEFAULT equal: " . ($eq ? 'YES' : 'NO'));
return $eq;
}
$strictEq = ($sourceDef['type'] === $targetDef['type'] &&
$sourceDef['unsigned'] === $targetDef['unsigned'] &&
$sourceDef['length'] === $targetDef['length'] &&
$sourceDef['nullable'] === $targetDef['nullable'] &&
$sourceDef['default'] === $targetDef['default']);
logDebug("Strict compare: " . ($strictEq ? 'EQUAL' : 'DIFFERENT'));
return $strictEq;
}
function buildColumnDefinition($columnDef)
{
if($columnDef['type'] === 'ENUM' && !empty($columnDef['enumValues']))
{
$quoted = array_map(function($v)
{
return "'" . str_replace("'", "''", $v) . "'";
}, $columnDef['enumValues']);
$definition = "enum(" . implode(", ", $quoted) . ")"; // 小写 enum
}
else
{
$definition = strtolower($columnDef['type']); // 小写类型
if($columnDef['length'])
{
$definition .= "({$columnDef['length']})";
}
if(isNumericType($columnDef['type']) && !empty($columnDef['unsigned']))
{
$definition .= " unsigned";
}
}
$definition .= $columnDef['nullable'] ? " NULL" : " NOT NULL";
if($columnDef['default'] === null)
{
if($columnDef['nullable'])
{
$definition .= " DEFAULT NULL";
}
}
else
{
if(isNumericType($columnDef['type']))
{
$definition .= " DEFAULT " . $columnDef['default'];
}
else
{
$escaped = str_replace("'", "''", $columnDef['default']);
$definition .= " DEFAULT '{$escaped}'";
}
}
return $definition;
}
function buildColumnDefinitionForModify($sourceDef, $targetDef)
{
// 如果源是 ENUM 且目标不兼容,生成 VARCHAR(N)
if(strtoupper($sourceDef['type']) === 'ENUM')
{
$requiredLen = getLongestEnumLength($sourceDef['enumValues']);
if($requiredLen < $targetDef['length'])
{
$requiredLen = $targetDef['length'];
}
$definition = "varchar({$requiredLen})";
}
else
{
// 否则保留源类型和长度
$definition = strtolower($sourceDef['type']);
if($sourceDef['length'])
{
$definition .= "({$sourceDef['length']})";
}
if(isNumericType($sourceDef['type']) && !empty($sourceDef['unsigned']))
{
$definition .= " unsigned";
}
}
$definition .= $targetDef['nullable'] ? " NULL" : " NOT NULL";
if($targetDef['default'] === null)
{
if($targetDef['nullable'])
{
$definition .= " DEFAULT NULL";
}
}
else
{
if(isNumericType($targetDef['type']))
{
$definition .= " DEFAULT " . $targetDef['default'];
}
else
{
$escaped = str_replace("'", "''", $targetDef['default']);
$definition .= " DEFAULT '{$escaped}'";
}
}
return $definition;
}
function compareTables($sourceTables, $targetTables)
{
$sqlStatements = [];
logDebug("Comparing " . count($targetTables) . " target tables");
foreach($targetTables as $tableName => $targetColumns)
{
logDebug("Processing table: $tableName");
if(!isset($sourceTables[$tableName]))
{
logDebug("Source table $tableName not found, skipping");
continue;
}
$alterStatements = [];
$enumStatements = [];
$sourceColumns = $sourceTables[$tableName];
foreach($targetColumns as $columnName => $targetDef)
{
if(!isset($sourceColumns[$columnName]))
{
logDebug("Column $columnName exists in target but not source → ADD");
$sqlStatements[] = "ALTER TABLE `$tableName` ADD COLUMN `$columnName` " . buildColumnDefinition($targetDef) . ($columnName === 'id' ? ' AUTO_INCREMENT PRIMARY KEY' : '') . ";";
}
else
{
$enum2int = false;
$sourceDef = $sourceColumns[$columnName];
if(!compareColumnDefinitions($sourceDef, $targetDef))
{
// 判断是否只需修改 NULL/DEFAULT,或需要改类型
if(isCompatibleColumn($sourceDef, $targetDef))
{
// 兼容,只改 NULL/DEFAULT
$colDef = buildColumnDefinitionForModify($sourceDef, $targetDef);
logDebug("Compatible difference → MODIFY (only NULL/DEFAULT): $colDef");
}
else
{
// 不兼容:若源是 ENUM,目标非 ENUM 且不满足长度,转为 VARCHAR
if(strtoupper($sourceDef['type']) === 'ENUM')
{
$requiredLen = getLongestEnumLength($sourceDef['enumValues']);
if($requiredLen < $targetDef['length'])
{
$requiredLen = $targetDef['length'];
}
// 构造新的 VARCHAR 定义(使用目标的 nullable/default)
$colDef = "varchar({$requiredLen})" . ($targetDef['nullable'] ? " NULL" : " NOT NULL");
if($targetDef['default'] !== null)
{
$escaped = str_replace("'", "''", $targetDef['default']);
$colDef .= " DEFAULT '{$escaped}'";
}
logDebug("ENUM incompatible → MODIFY to VARCHAR($requiredLen): $colDef");
$enum2int = true;
foreach($sourceDef['enumValues'] as $val)
{
if(!is_numeric($val))
{
$enum2int = false;
break;
}
}
}
else
{
// 其他情况:直接使用目标定义
$colDef = buildColumnDefinition($targetDef);
logDebug("Incompatible → MODIFY to target definition: $colDef");
}
}
$alterStatements[] = "MODIFY COLUMN `$columnName` {$colDef}" . ($columnName === 'id' ? ' AUTO_INCREMENT' : '');
if($enum2int)
{
logDebug("Incompatible → MODIFY to INT for ENUM to INT conversion");
$enumStatements[] = "MODIFY COLUMN `$columnName` tinyint unsigned" . ($targetDef['nullable'] ? " NULL" : " NOT NULL") . ($targetDef['default'] !== null ? " DEFAULT " . $targetDef['default'] : '');
}
}
else
{
logDebug("Column $columnName identical");
}
}
}
if($alterStatements)
{
$sqlStatements[] = "ALTER TABLE `$tableName`\n " . implode(",\n ", $alterStatements) . ";";
}
if($enumStatements)
{
$sqlStatements[] = "ALTER TABLE `$tableName`\n " . implode(",\n ", $enumStatements) . ";";
}
}
logDebug("Generated " . count($sqlStatements) . " SQL statements");
return $sqlStatements;
}
if($argc !== 3 && $argc !== 4)
{
echo "[Info] Usage: php diffTable.php source_db_file target_db_file [--debug]\n";
echo "[Info] Example: php diffTable.php db/standard/zentao21.7.7.sql db/standard/zentao21.7.8.sql --debug\n";
echo "[Info] The source_db_file and target_db_file should contain CREATE TABLE statements.\n";
echo "[Info] The --debug option is optional for debug output.\n";
echo "[Info] Note: The CREATE TABLE statements must end with ENGINE=InnoDB;\n";
exit(1);
}
$sourceDbFile = $argv[1];
$targetDbFile = $argv[2];
if(!file_exists($sourceDbFile))
{
echo "[Info] Error: Source file '$sourceDbFile' does not exist.\n";
exit(1);
}
if(!file_exists($targetDbFile))
{
echo "[Info] Error: Target file '$targetDbFile' does not exist.\n";
exit(1);
}
logDebug("Reading source file: $sourceDbFile");
logDebug("Reading target file: $targetDbFile");
$sourceSql = file_get_contents($sourceDbFile);
$targetSql = file_get_contents($targetDbFile);
if(trim($sourceSql) === '')
{
echo "[Info] Warning: Source file is empty.\n";
}
if(trim($targetSql) === '')
{
echo "[Info] Warning: Target file is empty.\n";
}
$sourceTables = parseCreateTable($sourceSql);
$targetTables = parseCreateTable($targetSql);
if(empty($targetTables))
{
echo "[Info] No target tables parsed. Check CREATE TABLE syntax (must end with ENGINE=InnoDB;).\n";
exit(1);
}
$sqlStatements = compareTables($sourceTables, $targetTables);
foreach($sqlStatements as $key => $sql)
{
$sqlStatements[$key] = str_replace(" varchar(1) ", " char(1) ", $sql); // 优化:长度为1的VARCHAR改为CHAR
}
file_put_contents('difftable.sql', implode("\n", $sqlStatements) . "\n");
echo "[Info] Generated SQL statements saved to difftable.sql\n";