This commit modifies key column references in the `fxStaticSyncTableDefinitions` function to align with updated database schema identifiers. Additionally, it introduces a new utility function, `fxStaticSyncKeyColumnsValid`, to validate key columns before executing queries, improving error handling in the `fxStaticSyncFetchRowsByKey` and `fxStaticSyncLoadKeyedRows` functions. The changes aim to enhance the robustness of the synchronization process and provide clearer error feedback during data operations.
882 lines
28 KiB
PHP
882 lines
28 KiB
PHP
<?php
|
|
|
|
require_once __DIR__ . '/inc_fx_static_sync_config.php';
|
|
|
|
/**
|
|
* Environnements comparables — configs depuis inc_settings.php uniquement.
|
|
* Source : $arrConDatabasePreProd (dev préprod sur serveur dev).
|
|
*/
|
|
function fxStaticSyncEnvironmentDefinitions() {
|
|
global $arrConDatabasePreProd, $arrConDatabaseProd;
|
|
global $arrConDatabaseClientPreProd, $arrConDatabaseClientProd;
|
|
|
|
$tabEnvs = array(
|
|
'dev_preprod' => array(
|
|
'label' => 'Dev préprod',
|
|
'config' => $arrConDatabasePreProd,
|
|
'is_source' => true,
|
|
),
|
|
'dev_prod' => array(
|
|
'label' => 'Dev prod',
|
|
'config' => $arrConDatabaseProd,
|
|
'is_source' => false,
|
|
),
|
|
);
|
|
|
|
if (!empty($arrConDatabaseClientPreProd)) {
|
|
$tabEnvs['client_preprod'] = array(
|
|
'label' => 'Client préprod',
|
|
'config' => $arrConDatabaseClientPreProd,
|
|
'is_source' => false,
|
|
);
|
|
}
|
|
|
|
if (!empty($arrConDatabaseClientProd)) {
|
|
$tabEnvs['client_prod'] = array(
|
|
'label' => 'Client prod',
|
|
'config' => $arrConDatabaseClientProd,
|
|
'is_source' => false,
|
|
);
|
|
}
|
|
|
|
return $tabEnvs;
|
|
}
|
|
|
|
/** Connexion MySQL avec timeout court, sans die() — pour BD client additionnelles. */
|
|
class clsMysqlStaticSync extends clsMysql {
|
|
function __construct($arrDatabase = array()) {
|
|
$this->last_id = 0;
|
|
$this->affected_rows = 0;
|
|
$this->con = null;
|
|
|
|
if (empty($arrDatabase['host']) || empty($arrDatabase['db'])) {
|
|
return;
|
|
}
|
|
|
|
$mysqli = mysqli_init();
|
|
if (!$mysqli) {
|
|
return;
|
|
}
|
|
|
|
mysqli_options($mysqli, MYSQLI_OPT_CONNECT_TIMEOUT, 3);
|
|
|
|
if (!@mysqli_real_connect(
|
|
$mysqli,
|
|
$arrDatabase['host'],
|
|
$arrDatabase['user'],
|
|
$arrDatabase['pass'],
|
|
$arrDatabase['db'],
|
|
3306
|
|
)) {
|
|
return;
|
|
}
|
|
|
|
// Pas de mysqli_set_charset ici — charset serveur / table legacy (latin1, utf8, etc.).
|
|
$this->con = $mysqli;
|
|
}
|
|
}
|
|
|
|
function fxStaticSyncConnect($arrConfig) {
|
|
if (empty($arrConfig['host']) || empty($arrConfig['db'])) {
|
|
return null;
|
|
}
|
|
|
|
$obj = new clsMysqlStaticSync($arrConfig);
|
|
|
|
return !empty($obj->con) ? $obj : null;
|
|
}
|
|
|
|
/**
|
|
* Dev : réutilise objDatabase* de inc_fonctions.php.
|
|
* Client : $arrConDatabaseClient* de inc_settings.php (si définis pour l'environnement).
|
|
*/
|
|
function fxStaticSyncOpenConnections() {
|
|
global $objDatabasePreProd, $objDatabaseProd;
|
|
global $arrConDatabaseClientPreProd, $arrConDatabaseClientProd;
|
|
|
|
$tabConnections = array(
|
|
'dev_preprod' => !empty($objDatabasePreProd) ? $objDatabasePreProd : null,
|
|
'dev_prod' => !empty($objDatabaseProd) ? $objDatabaseProd : null,
|
|
);
|
|
|
|
if (!empty($arrConDatabaseClientPreProd)) {
|
|
$tabConnections['client_preprod'] = fxStaticSyncConnect($arrConDatabaseClientPreProd);
|
|
}
|
|
|
|
if (!empty($arrConDatabaseClientProd)) {
|
|
$tabConnections['client_prod'] = fxStaticSyncConnect($arrConDatabaseClientProd);
|
|
}
|
|
|
|
return $tabConnections;
|
|
}
|
|
|
|
function fxStaticSyncTableExists($objDb, $strTable) {
|
|
if (!$objDb) {
|
|
return false;
|
|
}
|
|
|
|
$sql = "SHOW TABLES LIKE '" . $objDb->fxEscape($strTable) . "'";
|
|
$row = $objDb->fxGetRow($sql);
|
|
|
|
return !empty($row);
|
|
}
|
|
|
|
function fxStaticSyncRowBusinessKey(array $row, array $tabKeyCols) {
|
|
$tabParts = array();
|
|
foreach ($tabKeyCols as $strCol) {
|
|
$tabParts[] = isset($row[$strCol]) ? (string)$row[$strCol] : '';
|
|
}
|
|
|
|
return implode("\0", $tabParts);
|
|
}
|
|
|
|
/**
|
|
* Colonnes communes source/cible pour compare et copy (alignés).
|
|
* missing_in_target / extra_in_target : écarts de structure (hors ignore_cols).
|
|
*/
|
|
function fxStaticSyncResolveCompareColumns($objSource, $objTarget, $strTable, array $tabIgnoreCols) {
|
|
$tabSourceCols = fxStaticSyncGetTableColumnNames($objSource, $strTable);
|
|
$tabTargetCols = fxStaticSyncGetTableColumnNames($objTarget, $strTable);
|
|
$tabCommon = array();
|
|
|
|
foreach ($tabSourceCols as $strCol) {
|
|
if (in_array($strCol, $tabTargetCols, true)) {
|
|
$tabCommon[] = $strCol;
|
|
}
|
|
}
|
|
|
|
$tabCompareCols = array();
|
|
foreach ($tabCommon as $strCol) {
|
|
if (!in_array($strCol, $tabIgnoreCols, true)) {
|
|
$tabCompareCols[] = $strCol;
|
|
}
|
|
}
|
|
|
|
$tabMissingInTarget = array();
|
|
foreach ($tabSourceCols as $strCol) {
|
|
if (!in_array($strCol, $tabTargetCols, true) && !in_array($strCol, $tabIgnoreCols, true)) {
|
|
$tabMissingInTarget[] = $strCol;
|
|
}
|
|
}
|
|
|
|
$tabExtraInTarget = array();
|
|
foreach ($tabTargetCols as $strCol) {
|
|
if (!in_array($strCol, $tabSourceCols, true) && !in_array($strCol, $tabIgnoreCols, true)) {
|
|
$tabExtraInTarget[] = $strCol;
|
|
}
|
|
}
|
|
|
|
return array(
|
|
'copy_cols' => $tabCommon,
|
|
'compare_cols' => $tabCompareCols,
|
|
'missing_in_target' => $tabMissingInTarget,
|
|
'extra_in_target' => $tabExtraInTarget,
|
|
);
|
|
}
|
|
|
|
function fxStaticSyncSchemasAligned(array $tabSchema) {
|
|
return count($tabSchema['missing_in_target']) === 0
|
|
&& count($tabSchema['extra_in_target']) === 0;
|
|
}
|
|
|
|
function fxStaticSyncFormatSchemaDiff(array $tabSchema) {
|
|
$tabParts = array();
|
|
|
|
if (!empty($tabSchema['missing_in_target'])) {
|
|
$tabParts[] = 'absentes en cible : ' . implode(', ', $tabSchema['missing_in_target']);
|
|
}
|
|
if (!empty($tabSchema['extra_in_target'])) {
|
|
$tabParts[] = 'en trop en cible : ' . implode(', ', $tabSchema['extra_in_target']);
|
|
}
|
|
|
|
return implode(' · ', $tabParts);
|
|
}
|
|
|
|
function fxStaticSyncNormalizeCellValue($val) {
|
|
if ($val === null) {
|
|
return null;
|
|
}
|
|
|
|
return (string)$val;
|
|
}
|
|
|
|
function fxStaticSyncSqlOrderByKeys(array $tabKeys) {
|
|
$tabParts = array();
|
|
foreach ($tabKeys as $strCol) {
|
|
$tabParts[] = '`' . $strCol . '`';
|
|
}
|
|
|
|
return implode(',', $tabParts);
|
|
}
|
|
|
|
function fxStaticSyncGetTableRowCount($objDb, $strTable) {
|
|
if (!$objDb || !fxStaticSyncTableExists($objDb, $strTable)) {
|
|
return 0;
|
|
}
|
|
|
|
return (int)$objDb->fxGetVar('SELECT COUNT(*) FROM `' . $strTable . '`');
|
|
}
|
|
|
|
function fxStaticSyncKeyColumnsValid($objDb, $strTable, array $tabKeys) {
|
|
if (count($tabKeys) === 0) {
|
|
return false;
|
|
}
|
|
|
|
$tabCols = fxStaticSyncGetTableColumnNames($objDb, $strTable);
|
|
foreach ($tabKeys as $strCol) {
|
|
if (!in_array($strCol, $tabCols, true)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Charge les lignes — une entrée par clé métier (dernière gagne, ordre stable si ORDER BY possible).
|
|
*/
|
|
function fxStaticSyncFetchRowsByKey($objDb, array $tabDef) {
|
|
$strTable = $tabDef['table'];
|
|
$tabKeys = $tabDef['keys'];
|
|
|
|
if (!fxStaticSyncTableExists($objDb, $strTable)) {
|
|
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => null);
|
|
}
|
|
|
|
if (!fxStaticSyncKeyColumnsValid($objDb, $strTable, $tabKeys)) {
|
|
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => 'bad_keys');
|
|
}
|
|
|
|
$sql = 'SELECT * FROM `' . $strTable . '` ORDER BY ' . fxStaticSyncSqlOrderByKeys($tabKeys);
|
|
$tabResults = $objDb->fxGetResults($sql);
|
|
$tabKeyed = array();
|
|
$intDupes = 0;
|
|
$intPhysical = 0;
|
|
|
|
if (!is_array($tabResults)) {
|
|
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => 'query_failed');
|
|
}
|
|
|
|
foreach ($tabResults as $row) {
|
|
if (!is_array($row)) {
|
|
continue;
|
|
}
|
|
$intPhysical++;
|
|
$strKey = fxStaticSyncRowBusinessKey($row, $tabKeys);
|
|
if (isset($tabKeyed[$strKey])) {
|
|
$intDupes++;
|
|
}
|
|
$tabKeyed[$strKey] = $row;
|
|
}
|
|
|
|
return array(
|
|
'rows' => $tabKeyed,
|
|
'duplicate_count' => $intDupes,
|
|
'physical_count' => $intPhysical,
|
|
'error' => null,
|
|
);
|
|
}
|
|
|
|
function fxStaticSyncRowContentHash(array $row, array $tabCompareCols) {
|
|
$tabData = array();
|
|
foreach ($tabCompareCols as $strCol) {
|
|
$tabData[$strCol] = array_key_exists($strCol, $row)
|
|
? fxStaticSyncNormalizeCellValue($row[$strCol])
|
|
: null;
|
|
}
|
|
ksort($tabData);
|
|
|
|
return md5(json_encode($tabData, JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
function fxStaticSyncLoadKeyedRows($objDb, array $tabDef, array $tabCompareCols = null) {
|
|
$tabKeys = $tabDef['keys'];
|
|
$tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array();
|
|
|
|
if (!fxStaticSyncTableExists($objDb, $tabDef['table'])) {
|
|
return array('error' => 'missing_table', 'rows' => array(), 'count' => 0, 'physical_count' => 0);
|
|
}
|
|
|
|
$tabFetch = fxStaticSyncFetchRowsByKey($objDb, $tabDef);
|
|
if ($tabFetch['error'] === 'bad_keys') {
|
|
return array('error' => 'bad_keys', 'rows' => array(), 'count' => 0, 'physical_count' => 0);
|
|
}
|
|
if ($tabFetch['error'] === 'query_failed') {
|
|
return array('error' => 'query_failed', 'rows' => array(), 'count' => 0, 'physical_count' => 0);
|
|
}
|
|
$tabKeyed = array();
|
|
|
|
if ($tabCompareCols === null) {
|
|
$tabCompareCols = array();
|
|
if (count($tabFetch['rows']) > 0) {
|
|
$row = reset($tabFetch['rows']);
|
|
foreach (array_keys($row) as $strCol) {
|
|
if (!in_array($strCol, $tabIgnore, true)) {
|
|
$tabCompareCols[] = $strCol;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($tabFetch['rows'] as $strKey => $row) {
|
|
$tabKeyed[$strKey] = array(
|
|
'hash' => fxStaticSyncRowContentHash($row, $tabCompareCols),
|
|
'row' => $row,
|
|
);
|
|
}
|
|
|
|
return array(
|
|
'error' => null,
|
|
'rows' => $tabKeyed,
|
|
'count' => count($tabKeyed),
|
|
'physical_count' => $tabFetch['physical_count'],
|
|
'duplicate_count' => $tabFetch['duplicate_count'],
|
|
);
|
|
}
|
|
|
|
/** Sync d'écriture interdite vers client prod (comparaison seule). */
|
|
function fxStaticSyncWriteAllowed($strEnvId) {
|
|
return $strEnvId !== 'client_prod';
|
|
}
|
|
|
|
function fxStaticSyncFinalizeCompareResult(array $tabCmp) {
|
|
if (in_array($tabCmp['status'], array('missing_table', 'schema_mismatch', 'no_connection'), true)) {
|
|
return $tabCmp;
|
|
}
|
|
|
|
$intMissingKeys = count($tabCmp['missing_keys']);
|
|
$intExtraKeys = count($tabCmp['extra_keys']);
|
|
$intDiffKeys = count($tabCmp['diff_keys']);
|
|
|
|
if ($intMissingKeys === 0 && $intExtraKeys === 0 && $intDiffKeys === 0) {
|
|
$tabCmp['status'] = 'ok';
|
|
return $tabCmp;
|
|
}
|
|
|
|
$intDelta = (int)$tabCmp['target_count'] - (int)$tabCmp['source_count'];
|
|
|
|
if ($intDelta < 0) {
|
|
$tabCmp['status'] = 'missing_rows';
|
|
} elseif ($intDelta > 0) {
|
|
$tabCmp['status'] = 'extra_rows';
|
|
} else {
|
|
$tabCmp['status'] = 'content_diff';
|
|
}
|
|
|
|
return $tabCmp;
|
|
}
|
|
|
|
/**
|
|
* @return array status: ok|content_diff|missing_rows|extra_rows|missing_table|schema_mismatch|no_connection|error
|
|
*/
|
|
function fxStaticSyncCompareTable(array $tabSourceData, array $tabTargetData, array $tabSchemaInfo = array()) {
|
|
if (!empty($tabTargetData['error']) && $tabTargetData['error'] === 'missing_table') {
|
|
return array(
|
|
'status' => 'missing_table',
|
|
'source_count' => $tabSourceData['count'],
|
|
'target_count' => 0,
|
|
'missing_keys' => array(),
|
|
'extra_keys' => array(),
|
|
'diff_keys' => array(),
|
|
);
|
|
}
|
|
|
|
$tabSourceRows = $tabSourceData['rows'];
|
|
$tabTargetRows = $tabTargetData['rows'];
|
|
|
|
$tabMissing = array();
|
|
$tabDiff = array();
|
|
|
|
foreach ($tabSourceRows as $strKey => $tabRow) {
|
|
if (!isset($tabTargetRows[$strKey])) {
|
|
$tabMissing[] = $strKey;
|
|
continue;
|
|
}
|
|
if ($tabTargetRows[$strKey]['hash'] !== $tabRow['hash']) {
|
|
$tabDiff[] = $strKey;
|
|
}
|
|
}
|
|
|
|
$tabExtra = array();
|
|
foreach ($tabTargetRows as $strKey => $tabRow) {
|
|
if (!isset($tabSourceRows[$strKey])) {
|
|
$tabExtra[] = $strKey;
|
|
}
|
|
}
|
|
|
|
return fxStaticSyncFinalizeCompareResult(array(
|
|
'status' => 'pending',
|
|
'source_count' => $tabSourceData['count'],
|
|
'target_count' => $tabTargetData['count'],
|
|
'source_physical' => isset($tabSourceData['physical_count']) ? $tabSourceData['physical_count'] : $tabSourceData['count'],
|
|
'target_physical' => isset($tabTargetData['physical_count']) ? $tabTargetData['physical_count'] : $tabTargetData['count'],
|
|
'missing_keys' => $tabMissing,
|
|
'extra_keys' => $tabExtra,
|
|
'diff_keys' => $tabDiff,
|
|
'schema_missing_cols' => isset($tabSchemaInfo['missing_in_target']) ? $tabSchemaInfo['missing_in_target'] : array(),
|
|
'schema_extra_cols' => isset($tabSchemaInfo['extra_in_target']) ? $tabSchemaInfo['extra_in_target'] : array(),
|
|
));
|
|
}
|
|
|
|
function fxStaticSyncDbLastError($objDb) {
|
|
if (!$objDb || empty($objDb->con)) {
|
|
return '';
|
|
}
|
|
|
|
return mysqli_error($objDb->con);
|
|
}
|
|
|
|
/**
|
|
* Exécute une requête d'écriture — erreur remontée dans le message flash (fxQuery n'echo pas).
|
|
*/
|
|
function fxStaticSyncExecQuery($objDb, $strSql) {
|
|
if (!$objDb) {
|
|
return array('ok' => false, 'error' => 'Connexion BD invalide.');
|
|
}
|
|
|
|
$blnOk = $objDb->fxQuery($strSql);
|
|
if ($blnOk === false) {
|
|
return array(
|
|
'ok' => false,
|
|
'error' => fxStaticSyncDbLastError($objDb) ?: 'Erreur SQL inconnue.',
|
|
);
|
|
}
|
|
|
|
return array('ok' => true, 'error' => null);
|
|
}
|
|
|
|
function fxStaticSyncGetTableColumnNames($objDb, $strTable) {
|
|
if (!$objDb) {
|
|
return array();
|
|
}
|
|
|
|
$tabCols = $objDb->fxGetResults('SHOW COLUMNS FROM `' . $strTable . '`');
|
|
$tabNames = array();
|
|
|
|
if (!is_array($tabCols)) {
|
|
return array();
|
|
}
|
|
|
|
foreach ($tabCols as $row) {
|
|
if (is_array($row) && !empty($row['Field'])) {
|
|
$tabNames[] = $row['Field'];
|
|
}
|
|
}
|
|
|
|
return $tabNames;
|
|
}
|
|
|
|
function fxStaticSyncGetTableEngine($objDb, $strTable) {
|
|
if (!$objDb) {
|
|
return '';
|
|
}
|
|
|
|
$row = $objDb->fxGetRow(
|
|
"SELECT ENGINE FROM information_schema.TABLES
|
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '" . $objDb->fxEscape($strTable) . "'"
|
|
);
|
|
|
|
return !empty($row['ENGINE']) ? strtoupper($row['ENGINE']) : '';
|
|
}
|
|
|
|
function fxStaticSyncCopyTable($objSource, $objTarget, $strTable, array $tabDef = null) {
|
|
if (!$objSource || !$objTarget) {
|
|
return array('ok' => false, 'message' => 'Connexion BD invalide.');
|
|
}
|
|
|
|
if (!fxStaticSyncTableExists($objSource, $strTable)) {
|
|
return array('ok' => false, 'message' => 'Table absente dans la source.');
|
|
}
|
|
|
|
if (!fxStaticSyncTableExists($objTarget, $strTable)) {
|
|
return array('ok' => false, 'message' => 'Table absente dans la cible — exécuter le DDL d\'abord.');
|
|
}
|
|
|
|
if (!$tabDef) {
|
|
return array('ok' => false, 'message' => 'Table non configurée.');
|
|
}
|
|
|
|
$tabIgnore = ($tabDef && isset($tabDef['ignore_cols'])) ? $tabDef['ignore_cols'] : array();
|
|
$tabSchema = fxStaticSyncResolveCompareColumns($objSource, $objTarget, $strTable, $tabIgnore);
|
|
|
|
if (!fxStaticSyncSchemasAligned($tabSchema)) {
|
|
return array(
|
|
'ok' => false,
|
|
'message' => 'Structures non alignées — '
|
|
. fxStaticSyncFormatSchemaDiff($tabSchema)
|
|
. '. Aligner les structures (Navicat) avant toute sync. Aucune donnée modifiée.',
|
|
);
|
|
}
|
|
|
|
$tabCopyCols = $tabSchema['copy_cols'];
|
|
|
|
if (count($tabCopyCols) === 0) {
|
|
$tabFetchCheck = fxStaticSyncFetchRowsByKey($objSource, $tabDef);
|
|
if (count($tabFetchCheck['rows']) > 0) {
|
|
return array(
|
|
'ok' => false,
|
|
'message' => 'Aucune colonne commune entre source et cible.',
|
|
);
|
|
}
|
|
}
|
|
|
|
$tabFetch = fxStaticSyncFetchRowsByKey($objSource, $tabDef);
|
|
$tabRowsToCopy = $tabFetch['rows'];
|
|
$intDupes = $tabFetch['duplicate_count'];
|
|
|
|
// Préparer les INSERT avant toute écriture destructive (évite une table vide si échec).
|
|
$tabInsertBatches = array();
|
|
$intCopied = count($tabRowsToCopy);
|
|
|
|
if (count($tabCopyCols) > 0 && count($tabRowsToCopy) > 0) {
|
|
$strColList = '`' . implode('`,`', $tabCopyCols) . '`';
|
|
$intBatchSize = 50;
|
|
$tabBatch = array();
|
|
|
|
foreach ($tabRowsToCopy as $row) {
|
|
if (!is_array($row)) {
|
|
continue;
|
|
}
|
|
|
|
$tabVals = array();
|
|
foreach ($tabCopyCols as $strCol) {
|
|
if (!array_key_exists($strCol, $row) || $row[$strCol] === null) {
|
|
$tabVals[] = 'NULL';
|
|
} else {
|
|
$tabVals[] = "'" . $objTarget->fxEscape($row[$strCol]) . "'";
|
|
}
|
|
}
|
|
$tabBatch[] = '(' . implode(',', $tabVals) . ')';
|
|
|
|
if (count($tabBatch) >= $intBatchSize) {
|
|
$tabInsertBatches[] = 'INSERT INTO `' . $strTable . '` (' . $strColList . ') VALUES ' . implode(',', $tabBatch);
|
|
$tabBatch = array();
|
|
}
|
|
}
|
|
|
|
if (count($tabBatch) > 0) {
|
|
$tabInsertBatches[] = 'INSERT INTO `' . $strTable . '` (' . $strColList . ') VALUES ' . implode(',', $tabBatch);
|
|
}
|
|
}
|
|
|
|
$blnTransactional = (fxStaticSyncGetTableEngine($objTarget, $strTable) === 'INNODB');
|
|
$blnFkDisabled = false;
|
|
|
|
$tabFkOff = fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=0');
|
|
if (!$tabFkOff['ok']) {
|
|
return array('ok' => false, 'message' => 'FK checks : ' . $tabFkOff['error']);
|
|
}
|
|
$blnFkDisabled = true;
|
|
|
|
if ($blnTransactional) {
|
|
$tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION');
|
|
if (!$tabTx['ok']) {
|
|
fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1');
|
|
return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']);
|
|
}
|
|
}
|
|
|
|
// DELETE plutôt que TRUNCATE : compatible FK (InnoDB) avec checks désactivés.
|
|
$tabDelete = fxStaticSyncExecQuery($objTarget, 'DELETE FROM `' . $strTable . '`');
|
|
if (!$tabDelete['ok']) {
|
|
if ($blnTransactional) {
|
|
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
|
|
}
|
|
if ($blnFkDisabled) {
|
|
fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1');
|
|
}
|
|
return array('ok' => false, 'message' => 'Vidage table : ' . $tabDelete['error']);
|
|
}
|
|
|
|
foreach ($tabInsertBatches as $strSql) {
|
|
$tabInsert = fxStaticSyncExecQuery($objTarget, $strSql);
|
|
if (!$tabInsert['ok']) {
|
|
if ($blnTransactional) {
|
|
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
|
|
} else {
|
|
return array(
|
|
'ok' => false,
|
|
'message' => 'Insertion : ' . $tabInsert['error']
|
|
. ' — la table cible peut être vide (MyISAM, pas de rollback).',
|
|
);
|
|
}
|
|
if ($blnFkDisabled) {
|
|
fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1');
|
|
}
|
|
return array('ok' => false, 'message' => 'Insertion : ' . $tabInsert['error'] . ' (rollback effectué).');
|
|
}
|
|
}
|
|
|
|
if ($blnTransactional) {
|
|
$tabCommit = fxStaticSyncExecQuery($objTarget, 'COMMIT');
|
|
if (!$tabCommit['ok']) {
|
|
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
|
|
if ($blnFkDisabled) {
|
|
fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1');
|
|
}
|
|
return array('ok' => false, 'message' => 'Commit : ' . $tabCommit['error']);
|
|
}
|
|
}
|
|
|
|
if ($blnFkDisabled) {
|
|
fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1');
|
|
}
|
|
|
|
$strMessage = $intCopied . ' ligne(s) copiée(s).';
|
|
if ($intDupes > 0) {
|
|
$strMessage .= ' (' . $intDupes . ' doublon(s) ignoré(s) en source — même clé métier.)';
|
|
}
|
|
|
|
return array(
|
|
'ok' => true,
|
|
'message' => $strMessage,
|
|
'rows' => $intCopied,
|
|
);
|
|
}
|
|
|
|
function fxStaticSyncStatusLabel($strStatus, array $tabCmp = null) {
|
|
switch ($strStatus) {
|
|
case 'ok':
|
|
return 'Données alignées';
|
|
case 'missing_table':
|
|
return 'Table absente';
|
|
case 'schema_mismatch':
|
|
return 'Structures non alignées';
|
|
case 'no_connection':
|
|
return 'Connexion impossible';
|
|
case 'error':
|
|
return 'Erreur lecture';
|
|
}
|
|
|
|
if (is_array($tabCmp)) {
|
|
$intDelta = (int)$tabCmp['target_count'] - (int)$tabCmp['source_count'];
|
|
if ($intDelta < 0) {
|
|
return 'Il en manque ' . abs($intDelta);
|
|
}
|
|
if ($intDelta > 0) {
|
|
return 'Il y en a ' . $intDelta . ' en trop';
|
|
}
|
|
|
|
return 'Contenu différent';
|
|
}
|
|
|
|
switch ($strStatus) {
|
|
case 'content_diff':
|
|
return 'Contenu différent';
|
|
case 'missing_rows':
|
|
return 'Il en manque';
|
|
case 'extra_rows':
|
|
return 'Il y en a en trop';
|
|
default:
|
|
return 'Erreur';
|
|
}
|
|
}
|
|
|
|
function fxStaticSyncStatusClass($strStatus) {
|
|
switch ($strStatus) {
|
|
case 'ok':
|
|
return 'ss-ok';
|
|
case 'content_diff':
|
|
case 'missing_rows':
|
|
return 'ss-warn';
|
|
case 'extra_rows':
|
|
return 'ss-bad';
|
|
case 'missing_table':
|
|
case 'no_connection':
|
|
return 'ss-muted';
|
|
case 'schema_mismatch':
|
|
return 'ss-schema';
|
|
case 'error':
|
|
return 'ss-bad';
|
|
default:
|
|
return 'ss-bad';
|
|
}
|
|
}
|
|
|
|
function fxStaticSyncFormatKeyPreview($strKey) {
|
|
$strKey = str_replace("\0", ' · ', $strKey);
|
|
if (strlen($strKey) > 120) {
|
|
return substr($strKey, 0, 117) . '…';
|
|
}
|
|
|
|
return $strKey;
|
|
}
|
|
|
|
function fxStaticSyncEnsureCsrfToken() {
|
|
if (empty($_SESSION['ms1_static_sync_csrf'])) {
|
|
$_SESSION['ms1_static_sync_csrf'] = bin2hex(random_bytes(16));
|
|
}
|
|
|
|
return $_SESSION['ms1_static_sync_csrf'];
|
|
}
|
|
|
|
function fxStaticSyncValidateCsrf($strToken) {
|
|
return !empty($_SESSION['ms1_static_sync_csrf'])
|
|
&& hash_equals($_SESSION['ms1_static_sync_csrf'], (string)$strToken);
|
|
}
|
|
|
|
function fxStaticSyncFindTableDef($strTable) {
|
|
foreach (fxStaticSyncTableDefinitions() as $tabDef) {
|
|
if ($tabDef['table'] === $strTable) {
|
|
return $tabDef;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function fxStaticSyncFindEnvDef($strEnvId) {
|
|
$tabEnvs = fxStaticSyncEnvironmentDefinitions();
|
|
return isset($tabEnvs[$strEnvId]) ? $tabEnvs[$strEnvId] : null;
|
|
}
|
|
|
|
function fxStaticSyncCompareEnvPair($objSource, $objTarget, array $tabDef) {
|
|
$tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array();
|
|
|
|
if (!fxStaticSyncTableExists($objTarget, $tabDef['table'])) {
|
|
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef);
|
|
return array(
|
|
'status' => 'missing_table',
|
|
'source_count' => $tabSourceData['count'],
|
|
'target_count' => 0,
|
|
'missing_keys' => array(),
|
|
'extra_keys' => array(),
|
|
'diff_keys' => array(),
|
|
'schema_missing_cols' => array(),
|
|
'schema_extra_cols' => array(),
|
|
);
|
|
}
|
|
|
|
$tabSchema = fxStaticSyncResolveCompareColumns($objSource, $objTarget, $tabDef['table'], $tabIgnore);
|
|
|
|
if (!fxStaticSyncSchemasAligned($tabSchema)) {
|
|
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef);
|
|
$tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef);
|
|
return array(
|
|
'status' => 'schema_mismatch',
|
|
'source_count' => $tabSourceData['count'],
|
|
'target_count' => $tabTargetData['count'],
|
|
'missing_keys' => array(),
|
|
'extra_keys' => array(),
|
|
'diff_keys' => array(),
|
|
'schema_missing_cols' => $tabSchema['missing_in_target'],
|
|
'schema_extra_cols' => $tabSchema['extra_in_target'],
|
|
);
|
|
}
|
|
|
|
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef, $tabSchema['compare_cols']);
|
|
$tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef, $tabSchema['compare_cols']);
|
|
|
|
if (!empty($tabSourceData['error']) || !empty($tabTargetData['error'])) {
|
|
return array(
|
|
'status' => 'error',
|
|
'source_count' => isset($tabSourceData['count']) ? $tabSourceData['count'] : 0,
|
|
'target_count' => isset($tabTargetData['count']) ? $tabTargetData['count'] : 0,
|
|
'missing_keys' => array(),
|
|
'extra_keys' => array(),
|
|
'diff_keys' => array(),
|
|
'schema_missing_cols' => array(),
|
|
'schema_extra_cols' => array(),
|
|
);
|
|
}
|
|
|
|
return fxStaticSyncCompareTable($tabSourceData, $tabTargetData, $tabSchema);
|
|
}
|
|
|
|
function fxStaticSyncBuildReport($tabConnections) {
|
|
$tabDefs = fxStaticSyncTableDefinitions();
|
|
$tabEnvs = fxStaticSyncEnvironmentDefinitions();
|
|
$objSource = $tabConnections['dev_preprod'];
|
|
$tabReport = array();
|
|
|
|
foreach ($tabDefs as $tabDef) {
|
|
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef);
|
|
$tabRow = array(
|
|
'def' => $tabDef,
|
|
'source_count' => $tabSourceData['count'],
|
|
'source_physical' => $tabSourceData['physical_count'],
|
|
'source_dupes' => $tabSourceData['duplicate_count'],
|
|
'envs' => array(),
|
|
);
|
|
|
|
foreach ($tabEnvs as $strEnvId => $tabEnv) {
|
|
if (!empty($tabEnv['is_source'])) {
|
|
continue;
|
|
}
|
|
|
|
$objTarget = isset($tabConnections[$strEnvId]) ? $tabConnections[$strEnvId] : null;
|
|
if (!$objTarget) {
|
|
$tabRow['envs'][$strEnvId] = array(
|
|
'status' => 'no_connection',
|
|
'source_count' => $tabSourceData['count'],
|
|
'target_count' => 0,
|
|
'missing_keys' => array(),
|
|
'extra_keys' => array(),
|
|
'diff_keys' => array(),
|
|
'schema_missing_cols' => array(),
|
|
'schema_extra_cols' => array(),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
$tabRow['envs'][$strEnvId] = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
|
|
}
|
|
|
|
$tabReport[] = $tabRow;
|
|
}
|
|
|
|
return $tabReport;
|
|
}
|
|
|
|
function fxStaticSyncGetDiffDetail($strTable, $strEnvId, $tabConnections) {
|
|
$tabDef = fxStaticSyncFindTableDef($strTable);
|
|
$tabEnv = fxStaticSyncFindEnvDef($strEnvId);
|
|
|
|
if (!$tabDef || !$tabEnv || !empty($tabEnv['is_source'])) {
|
|
return null;
|
|
}
|
|
|
|
$objSource = $tabConnections['dev_preprod'];
|
|
$objTarget = $tabConnections[$strEnvId];
|
|
|
|
if (!$objSource || !$objTarget) {
|
|
return null;
|
|
}
|
|
|
|
$tabCompare = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
|
|
$tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array();
|
|
$tabSchema = fxStaticSyncResolveCompareColumns($objSource, $objTarget, $tabDef['table'], $tabIgnore);
|
|
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef, $tabSchema['compare_cols']);
|
|
$tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef, $tabSchema['compare_cols']);
|
|
|
|
$tabDetail = array(
|
|
'compare' => $tabCompare,
|
|
'samples' => array(),
|
|
);
|
|
|
|
$tabKeysToShow = array_slice(
|
|
array_merge($tabCompare['missing_keys'], $tabCompare['diff_keys'], $tabCompare['extra_keys']),
|
|
0,
|
|
30
|
|
);
|
|
|
|
foreach ($tabKeysToShow as $strKey) {
|
|
$tabSample = array(
|
|
'key' => $strKey,
|
|
'type' => in_array($strKey, $tabCompare['extra_keys'], true) ? 'extra'
|
|
: (in_array($strKey, $tabCompare['missing_keys'], true) ? 'missing' : 'diff'),
|
|
'source' => isset($tabSourceData['rows'][$strKey]['row']) ? $tabSourceData['rows'][$strKey]['row'] : null,
|
|
'target' => isset($tabTargetData['rows'][$strKey]['row']) ? $tabTargetData['rows'][$strKey]['row'] : null,
|
|
);
|
|
|
|
if ($tabDef['table'] === 'info' && $tabSample['type'] === 'diff') {
|
|
$tabSample['source_texte'] = isset($tabSample['source']['info_texte']) ? $tabSample['source']['info_texte'] : '';
|
|
$tabSample['target_texte'] = isset($tabSample['target']['info_texte']) ? $tabSample['target']['info_texte'] : '';
|
|
}
|
|
|
|
$tabDetail['samples'][] = $tabSample;
|
|
}
|
|
|
|
return $tabDetail;
|
|
}
|