checkDBschema.pl 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. #!/usr/bin/perl
  2. #
  3. # Copyright (C) Roman Dmitriev, rnd@rajven.ru
  4. #
  5. use utf8;
  6. use open ":encoding(utf8)";
  7. use open ':std', ':encoding(UTF-8)';
  8. use Encode;
  9. no warnings 'utf8';
  10. use English;
  11. use FindBin '$Bin';
  12. use lib "/opt/Eye/scripts";
  13. use Getopt::Long qw(GetOptions);
  14. use Data::Dumper;
  15. use eyelib::config;
  16. use eyelib::main;
  17. use eyelib::database;
  18. use eyelib::common;
  19. use eyelib::net_utils;
  20. use strict;
  21. use warnings;
  22. # === Получаем имя чистой БД ===
  23. my $clear_db_name = $DBNAME . "_test";
  24. # === Подключаемся к рабочей БД ===
  25. my $work_db = init_db();
  26. # === Подключаемся к чистой БД ===
  27. my $clear_db;
  28. if ($config_ref{DBTYPE} eq 'mysql') {
  29. my $dsn = "dbi:mysql:database=$clear_db_name;host=$DBHOST;port=3306;mysql_local_infile=1";
  30. $clear_db = DBI->connect($dsn, $DBUSER, $DBPASS, {
  31. RaiseError => 0,
  32. AutoCommit => 1,
  33. mysql_enable_utf8 => 1
  34. });
  35. if (!defined $clear_db) {
  36. die "Cannot connect to MySQL database '$clear_db_name': $DBI::errstr\n";
  37. }
  38. $clear_db->do('SET NAMES utf8mb4');
  39. } else {
  40. my $dsn = "dbi:Pg:dbname=$clear_db_name;host=$DBHOST;port=5432;";
  41. $clear_db = DBI->connect($dsn, $DBUSER, $DBPASS, {
  42. RaiseError => 0,
  43. AutoCommit => 1,
  44. pg_enable_utf8 => 1,
  45. pg_server_prepare => 0
  46. });
  47. if (!defined $clear_db) {
  48. die "Cannot connect to PostgreSQL database '$clear_db_name': $DBI::errstr\n";
  49. }
  50. }
  51. # === Функция нормализации значения по умолчанию ===
  52. sub normalize_default {
  53. my ($default, $db_type) = @_;
  54. return undef unless defined $default;
  55. if ($db_type eq 'mysql') {
  56. # Убираем кавычки, если строка
  57. $default =~ s/^'(.*)'$/$1/;
  58. # NULL → undef
  59. return undef if lc($default) eq 'null';
  60. } else {
  61. # PostgreSQL: уже в нормальном виде
  62. return undef if lc($default) eq 'null';
  63. }
  64. return $default;
  65. }
  66. # === Сбор схемы для БД ===
  67. sub get_schema {
  68. my ($db, $db_type, $db_name) = @_;
  69. my %schema;
  70. my @tables;
  71. if ($db_type eq 'mysql') {
  72. my @rows = get_records_sql($db, 'SHOW TABLES');
  73. my $idx = 'Tables_in_' . $db_name;
  74. @tables = map { $_->{$idx} } grep { $_ && exists $_->{$idx} } @rows;
  75. } else {
  76. my @rows = get_records_sql($db, "SELECT tablename FROM pg_tables WHERE schemaname = 'public'");
  77. @tables = map { $_->{tablename} } @rows;
  78. }
  79. for my $table (@tables) {
  80. my %cols = get_table_columns($db, $table);
  81. # get_table_columns уже возвращает lowercase имена
  82. for my $col (keys %cols) {
  83. my $info = $cols{$col};
  84. $schema{$table}{$col} = {
  85. name => $info->{name} // '',
  86. type => $info->{type} // '',
  87. nullable => $info->{nullable} // 1,
  88. default => normalize_default($info->{default}, $db_type),
  89. };
  90. }
  91. }
  92. return %schema;
  93. }
  94. # === Сбор схем ===
  95. print "Fetching schema from working database '$DBNAME'...\n";
  96. my %work_schema = get_schema($work_db, $config_ref{DBTYPE}, $DBNAME);
  97. print "Fetching schema from clean database '$clear_db_name'...\n";
  98. my %clear_schema = get_schema($clear_db, $config_ref{DBTYPE}, $clear_db_name);
  99. print "\n=== Comparing DB schemas ===\n\n";
  100. my $has_critical_error = 0;
  101. # === 1. Проверка: всё ли из чистой БД есть в рабочей? ===
  102. for my $table (keys %clear_schema) {
  103. if (!exists $work_schema{$table}) {
  104. print "❗ ERROR: Table '$table' exists in clean DB but not in working DB!\n";
  105. $has_critical_error = 1;
  106. next;
  107. }
  108. for my $col (keys %{ $clear_schema{$table} }) {
  109. if (!exists $work_schema{$table}{$col}) {
  110. print "❗ ERROR: Column '$col' in table '$table' exists in clean DB but not in working DB!\n";
  111. $has_critical_error = 1;
  112. next;
  113. }
  114. my $clean_name = $clear_schema{$table}{$col}{name} // '';
  115. my $work_name = $work_schema{$table}{$col}{name} // '';
  116. if ($clean_name ne $work_name) {
  117. print "❗ ERROR: Column '$col' in table '$table' has different name case:\n";
  118. print " Clean: '$clean_name', Working: '$work_name'\n";
  119. $has_critical_error = 1;
  120. }
  121. # === Сравнение типов ===
  122. my $clean_type = $clear_schema{$table}{$col}{type} // '';
  123. my $work_type = $work_schema{$table}{$col}{type} // '';
  124. # Нормализуем типы для сравнения (MySQL vs PG)
  125. # if ($config_ref{DBTYPE} eq 'mysql') {
  126. # Пример: TINYINT(1) → boolean-like, но у нас SMALLINT
  127. # Для простоты сравниваем как строки
  128. # }
  129. if ($clean_type ne $work_type) {
  130. print "❗ ERROR: Column '$col' in table '$table' has different type:\n";
  131. print " Clean: '$clean_type', Working: '$work_type'\n";
  132. $has_critical_error = 1;
  133. }
  134. # === Сравнение NULL ===
  135. my $clean_null = $clear_schema{$table}{$col}{nullable} // 1;
  136. my $work_null = $work_schema{$table}{$col}{nullable} // 1;
  137. if ($clean_null != $work_null) {
  138. my $clean_str = $clean_null ? "NULL" : "NOT NULL";
  139. my $work_str = $work_null ? "NULL" : "NOT NULL";
  140. print "❗ ERROR: Column '$col' in table '$table' has different NULL setting:\n";
  141. print " Clean: $clean_str, Working: $work_str\n";
  142. $has_critical_error = 1;
  143. }
  144. # === Сравнение DEFAULT ===
  145. my $clean_def = $clear_schema{$table}{$col}{default};
  146. my $work_def = $work_schema{$table}{$col}{default};
  147. if (!defined $clean_def && !defined $work_def) {
  148. # ok
  149. } elsif (!defined $clean_def || !defined $work_def) {
  150. print "❗ ERROR: Column '$col' in table '$table' has different DEFAULT (one is NULL):\n";
  151. print " Clean: ", defined $clean_def ? "'$clean_def'" : "NULL", "\n";
  152. print " Working: ", defined $work_def ? "'$work_def'" : "NULL", "\n";
  153. $has_critical_error = 1;
  154. } elsif ($clean_def ne $work_def) {
  155. print "❗ ERROR: Column '$col' in table '$table' has different DEFAULT:\n";
  156. print " Clean: '$clean_def', Working: '$work_def'\n";
  157. $has_critical_error = 1;
  158. }
  159. }
  160. }
  161. # === 2. Проверка: есть ли лишнее в рабочей БД? ===
  162. for my $table (keys %work_schema) {
  163. if (!exists $clear_schema{$table}) {
  164. print "⚠ WARNING: Table '$table' exists in working DB but not in clean DB — will be skipped.\n";
  165. next;
  166. }
  167. for my $col (keys %{ $work_schema{$table} }) {
  168. if (!exists $clear_schema{$table}{$col}) {
  169. print "⚠ WARNING: Column '$col' in table '$table' exists in working DB but not in clean DB — will be ignored.\n";
  170. }
  171. }
  172. }
  173. if ($has_critical_error) {
  174. print "\nSchema validation failed: structural differences found.\n";
  175. exit 103;
  176. }
  177. print "✅ Schema validation passed.\n\n";
  178. exit 0;