diff --git a/php/inc_fx_static_sync.php b/php/inc_fx_static_sync.php index 3187920..9beaccc 100644 --- a/php/inc_fx_static_sync.php +++ b/php/inc_fx_static_sync.php @@ -130,17 +130,79 @@ function fxStaticSyncRowBusinessKey(array $row, array $tabKeyCols) { return implode("\0", $tabParts); } -function fxStaticSyncRowContentHash(array $row, array $tabIgnoreCols) { - $tabData = $row; - foreach ($tabIgnoreCols as $strCol) { - unset($tabData[$strCol]); +/** + * 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 fxStaticSyncRowContentHash(array $row, array $tabCompareCols) { + $tabData = array(); + foreach ($tabCompareCols as $strCol) { + $tabData[$strCol] = array_key_exists($strCol, $row) ? $row[$strCol] : null; } ksort($tabData); return md5(json_encode($tabData, JSON_UNESCAPED_UNICODE)); } -function fxStaticSyncLoadKeyedRows($objDb, array $tabDef) { +function fxStaticSyncLoadKeyedRows($objDb, array $tabDef, array $tabCompareCols = null) { $strTable = $tabDef['table']; $tabKeys = $tabDef['keys']; $tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array(); @@ -157,13 +219,24 @@ function fxStaticSyncLoadKeyedRows($objDb, array $tabDef) { return array('error' => null, 'rows' => array(), 'count' => 0); } + if ($tabCompareCols === null) { + $tabCompareCols = array(); + if (count($tabResults) > 0) { + foreach (array_keys($tabResults[1]) as $strCol) { + if (!in_array($strCol, $tabIgnore, true)) { + $tabCompareCols[] = $strCol; + } + } + } + } + foreach ($tabResults as $row) { if (!is_array($row)) { continue; } $strKey = fxStaticSyncRowBusinessKey($row, $tabKeys); $tabKeyed[$strKey] = array( - 'hash' => fxStaticSyncRowContentHash($row, $tabIgnore), + 'hash' => fxStaticSyncRowContentHash($row, $tabCompareCols), 'row' => $row, ); } @@ -171,10 +244,15 @@ function fxStaticSyncLoadKeyedRows($objDb, array $tabDef) { return array('error' => null, 'rows' => $tabKeyed, 'count' => count($tabKeyed)); } +/** Sync d'écriture interdite vers client prod (comparaison seule). */ +function fxStaticSyncWriteAllowed($strEnvId) { + return $strEnvId !== 'client_prod'; +} + /** - * @return array status: ok|content_diff|missing_rows|extra_rows|missing_table|no_connection|error + * @return array status: ok|content_diff|missing_rows|extra_rows|missing_table|schema_mismatch|no_connection|error */ -function fxStaticSyncCompareTable(array $tabSourceData, array $tabTargetData) { +function fxStaticSyncCompareTable(array $tabSourceData, array $tabTargetData, array $tabSchemaInfo = array()) { if (!empty($tabTargetData['error']) && $tabTargetData['error'] === 'missing_table') { return array( 'status' => 'missing_table', @@ -225,6 +303,8 @@ function fxStaticSyncCompareTable(array $tabSourceData, array $tabTargetData) { '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(), ); } @@ -289,7 +369,7 @@ function fxStaticSyncGetTableEngine($objDb, $strTable) { return !empty($row['ENGINE']) ? strtoupper($row['ENGINE']) : ''; } -function fxStaticSyncCopyTable($objSource, $objTarget, $strTable) { +function fxStaticSyncCopyTable($objSource, $objTarget, $strTable, array $tabDef = null) { if (!$objSource || !$objTarget) { return array('ok' => false, 'message' => 'Connexion BD invalide.'); } @@ -302,27 +382,23 @@ function fxStaticSyncCopyTable($objSource, $objTarget, $strTable) { return array('ok' => false, 'message' => 'Table absente dans la cible — exécuter le DDL d\'abord.'); } + $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.', + ); + } + $tabRows = $objSource->fxGetResults('SELECT * FROM `' . $strTable . '`'); - $tabTargetCols = fxStaticSyncGetTableColumnNames($objTarget, $strTable); + $tabCopyCols = $tabSchema['copy_cols']; + $tabSkippedCols = array(); - if (count($tabTargetCols) === 0) { - return array('ok' => false, 'message' => 'Impossible de lire les colonnes de la table cible.'); - } - - $tabSourceCols = array(); - if (is_array($tabRows) && count($tabRows) > 0) { - $tabSourceCols = array_keys($tabRows[1]); - } - - $tabCopyCols = array(); - foreach ($tabSourceCols as $strCol) { - if (in_array($strCol, $tabTargetCols, true)) { - $tabCopyCols[] = $strCol; - } - } - - $tabSkippedCols = array_diff($tabSourceCols, $tabCopyCols); - if (count($tabSourceCols) > 0 && count($tabCopyCols) === 0) { + if (count($tabCopyCols) === 0 && is_array($tabRows) && count($tabRows) > 0) { return array( 'ok' => false, 'message' => 'Aucune colonne commune entre source et cible — exécuter les scripts DDL manquants.', @@ -430,22 +506,18 @@ function fxStaticSyncCopyTable($objSource, $objTarget, $strTable) { } $strMessage = $intCopied . ' ligne(s) copiée(s).'; - if (count($tabSkippedCols) > 0) { - $strMessage .= ' Colonnes ignorées (absentes en cible) : ' . implode(', ', $tabSkippedCols) . '.'; - } return array( 'ok' => true, 'message' => $strMessage, 'rows' => $intCopied, - 'skipped_cols' => array_values($tabSkippedCols), ); } function fxStaticSyncStatusLabel($strStatus) { switch ($strStatus) { case 'ok': - return 'Aligné'; + return 'Données alignées'; case 'content_diff': return 'Contenu différent'; case 'missing_rows': @@ -454,6 +526,8 @@ function fxStaticSyncStatusLabel($strStatus) { return 'Lignes en trop'; case 'missing_table': return 'Table absente'; + case 'schema_mismatch': + return 'Structures non alignées'; case 'no_connection': return 'Connexion impossible'; default: @@ -473,6 +547,8 @@ function fxStaticSyncStatusClass($strStatus) { case 'missing_table': case 'no_connection': return 'ss-muted'; + case 'schema_mismatch': + return 'ss-schema'; default: return 'ss-bad'; } @@ -515,6 +591,46 @@ function fxStaticSyncFindEnvDef($strEnvId) { 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']); + + return fxStaticSyncCompareTable($tabSourceData, $tabTargetData, $tabSchema); +} + function fxStaticSyncBuildReport($tabConnections) { $tabDefs = fxStaticSyncTableDefinitions(); $tabEnvs = fxStaticSyncEnvironmentDefinitions(); @@ -543,12 +659,13 @@ function fxStaticSyncBuildReport($tabConnections) { 'missing_keys' => array(), 'extra_keys' => array(), 'diff_keys' => array(), + 'schema_missing_cols' => array(), + 'schema_extra_cols' => array(), ); continue; } - $tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef); - $tabRow['envs'][$strEnvId] = fxStaticSyncCompareTable($tabSourceData, $tabTargetData); + $tabRow['envs'][$strEnvId] = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef); } $tabReport[] = $tabRow; @@ -572,9 +689,11 @@ function fxStaticSyncGetDiffDetail($strTable, $strEnvId, $tabConnections) { return null; } - $tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef); - $tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef); - $tabCompare = fxStaticSyncCompareTable($tabSourceData, $tabTargetData); + $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, diff --git a/php/sync_static_db.php b/php/sync_static_db.php index 54f7d97..75174ce 100644 --- a/php/sync_static_db.php +++ b/php/sync_static_db.php @@ -39,6 +39,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST[' if (!$tabDef || !$tabEnv || !empty($tabEnv['is_source'])) { $strFlash = 'Paramètres invalides.'; $strFlashType = 'bad'; + } elseif (!fxStaticSyncWriteAllowed($strEnvId)) { + $strFlash = 'Sync désactivée vers client prod — comparaison seule.'; + $strFlashType = 'bad'; } else { $tabConnections = fxStaticSyncOpenConnections(); $objSource = $tabConnections['dev_preprod']; @@ -48,10 +51,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST[' $strFlash = 'Connexion BD impossible.'; $strFlashType = 'bad'; } else { - $tabResult = fxStaticSyncCopyTable($objSource, $objTarget, $strTable); + $tabResult = fxStaticSyncCopyTable($objSource, $objTarget, $strTable, $tabDef); if ($tabResult['ok']) { + $tabVerify = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef); $strFlash = $tabDef['label'] . ' → ' . $tabEnv['label'] . ' : ' . $tabResult['message']; - $strFlashType = !empty($tabResult['skipped_cols']) ? 'info' : 'ok'; + if ($tabVerify['status'] === 'ok') { + $strFlash .= ' Vérification : aligné.'; + $strFlashType = 'ok'; + } else { + $strFlash .= ' ATTENTION — après sync, statut : ' + . fxStaticSyncStatusLabel($tabVerify['status']) . '.'; + $strFlashType = 'bad'; + } } else { $strFlash = $tabDef['label'] . ' → ' . $tabEnv['label'] . ' — échec : ' . $tabResult['message']; $strFlashType = 'bad'; @@ -87,6 +98,8 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') { .flash-ok { background: #d4edda; border: 1px solid #b7dfc3; } .flash-bad { background: #f8d7da; border: 1px solid #f1b8be; } .flash-info { background: #e8f0fe; border: 1px solid #c5d7fb; } + .warn-box { background: #fff3cd; border: 1px solid #ffecb5; padding: 12px 14px; border-radius: 6px; margin-bottom: 16px; font-size: 0.9rem; } + .warn-box strong { color: #856404; } table { width: 100%; border-collapse: collapse; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.08); } th, td { border: 1px solid #ddd; padding: 8px 10px; vertical-align: top; font-size: 0.9rem; } th { background: #eef2f7; text-align: left; } @@ -94,6 +107,7 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') { .ss-warn { background: #fff3cd; color: #856404; } .ss-bad { background: #f8d7da; color: #721c24; } .ss-muted { background: #e9ecef; color: #495057; } + .ss-schema { background: #fde2e2; color: #7f1d1d; border: 1px solid #f5c2c2; } .badge { display: inline-block; padding: 3px 8px; border-radius: 4px; font-size: 0.82rem; font-weight: 600; } .count { font-size: 0.82rem; color: #444; margin-top: 4px; } .btn { display: inline-block; margin-top: 6px; padding: 4px 10px; font-size: 0.82rem; border: 1px solid #888; background: #fff; border-radius: 4px; cursor: pointer; text-decoration: none; color: #222; } @@ -111,10 +125,15 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {

Sync tables statiques

+
+ Attention — opération destructive. + Sync = DELETE puis recopie complète depuis dev préprod. + Ne pas utiliser vers client prod (bouton désactivé). + Si le statut indique Structures non alignées, aligner les structures via Navicat — aucune sync possible tant que ce n'est pas réglé. +

Source de vérité : dev préprod (). - Comparaison vers dev prod, client préprod et client prod. - Le bouton Sync écrase la table cible avec la source. + Comparaison vers dev prod, client préprod et client prod (lecture seule pour prod).

@@ -122,9 +141,10 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
- Aligné + Données alignées Manque ou contenu différent Lignes en trop + Structures non alignées Table absente / connexion
@@ -168,11 +188,20 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
/ ligne(s)
+ isset($tabCmp['schema_missing_cols']) ? $tabCmp['schema_missing_cols'] : array(), + 'extra_in_target' => isset($tabCmp['schema_extra_cols']) ? $tabCmp['schema_extra_cols'] : array(), + ); + $strSchemaDiff = fxStaticSyncFormatSchemaDiff($tabSchemaDiff); + if ($strSchemaDiff !== '') { ?> +
+ Détails - -
+ + @@ -198,6 +227,15 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') { Manquantes : — Différences : — En trop : + isset($tabCmp['schema_missing_cols']) ? $tabCmp['schema_missing_cols'] : array(), + 'extra_in_target' => isset($tabCmp['schema_extra_cols']) ? $tabCmp['schema_extra_cols'] : array(), + ); + $strSchemaDiff = fxStaticSyncFormatSchemaDiff($tabSchemaDiff); + if ($strSchemaDiff !== '') { ?> +
+ Fermer