migrate2psql.pl 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  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. my $chunk_count = 1000;
  23. my %pg_schema = (
  24. 'acl' => {
  25. 'id' => 'SERIAL',
  26. 'name' => 'VARCHAR(30)',
  27. 'description_english' => 'VARCHAR(250)',
  28. 'description_russian' => 'VARCHAR(250)',
  29. },
  30. 'ad_comp_cache' => {
  31. 'id' => 'SERIAL',
  32. 'name' => 'VARCHAR(63)',
  33. 'last_found' => 'TIMESTAMP',
  34. },
  35. 'auth_rules' => {
  36. 'id' => 'SERIAL',
  37. 'user_id' => 'INTEGER',
  38. 'ou_id' => 'INTEGER',
  39. 'rule_type' => 'SMALLINT',
  40. 'rule' => 'VARCHAR(40)',
  41. 'description' => 'VARCHAR(250)',
  42. },
  43. 'building' => {
  44. 'id' => 'SERIAL',
  45. 'name' => 'VARCHAR(50)',
  46. 'description' => 'VARCHAR(250)',
  47. },
  48. 'config' => {
  49. 'id' => 'SERIAL',
  50. 'option_id' => 'INTEGER',
  51. 'value' => 'VARCHAR(250)',
  52. },
  53. 'config_options' => {
  54. 'id' => 'SERIAL',
  55. 'option_name' => 'VARCHAR(50)',
  56. 'description_russian' => 'TEXT',
  57. 'description_english' => 'TEXT',
  58. 'draft' => 'SMALLINT',
  59. 'uniq' => 'SMALLINT',
  60. 'option_type' => 'VARCHAR(100)',
  61. 'default_value' => 'VARCHAR(250)',
  62. 'min_value' => 'INTEGER',
  63. 'max_value' => 'INTEGER',
  64. },
  65. 'connections' => {
  66. 'id' => 'BIGSERIAL',
  67. 'device_id' => 'BIGINT',
  68. 'port_id' => 'BIGINT',
  69. 'auth_id' => 'BIGINT',
  70. 'last_found' => 'TIMESTAMP',
  71. },
  72. 'customers' => {
  73. 'id' => 'SERIAL',
  74. 'login' => 'VARCHAR(20)',
  75. 'description' => 'VARCHAR(100)',
  76. 'password' => 'VARCHAR(255)',
  77. 'api_key' => 'VARCHAR(255)',
  78. 'rights' => 'SMALLINT',
  79. },
  80. 'devices' => {
  81. 'id' => 'SERIAL',
  82. 'device_type' => 'INTEGER',
  83. 'device_model_id' => 'INTEGER',
  84. 'firmware' => 'VARCHAR(100)',
  85. 'vendor_id' => 'INTEGER',
  86. 'device_name' => 'VARCHAR(50)',
  87. 'building_id' => 'INTEGER',
  88. 'ip' => 'INET',
  89. 'ip_int' => 'BIGINT',
  90. 'login' => 'VARCHAR(50)',
  91. 'password' => 'VARCHAR(255)',
  92. 'protocol' => 'SMALLINT',
  93. 'control_port' => 'INTEGER',
  94. 'port_count' => 'INTEGER',
  95. 'sn' => 'VARCHAR(80)',
  96. 'description' => 'VARCHAR(255)',
  97. 'snmp_version' => 'SMALLINT',
  98. 'snmp3_auth_proto' => 'VARCHAR(10)',
  99. 'snmp3_priv_proto' => 'VARCHAR(10)',
  100. 'snmp3_user_rw' => 'VARCHAR(20)',
  101. 'snmp3_user_rw_password' => 'VARCHAR(20)',
  102. 'snmp3_user_ro' => 'VARCHAR(20)',
  103. 'snmp3_user_ro_password' => 'VARCHAR(20)',
  104. 'community' => 'VARCHAR(50)',
  105. 'rw_community' => 'VARCHAR(50)',
  106. 'fdb_snmp_index' => 'SMALLINT',
  107. 'discovery' => 'SMALLINT',
  108. 'netflow_save' => 'SMALLINT',
  109. 'user_acl' => 'SMALLINT',
  110. 'dhcp' => 'SMALLINT',
  111. 'nagios' => 'SMALLINT',
  112. 'active' => 'SMALLINT',
  113. 'nagios_status' => 'VARCHAR(10)',
  114. 'queue_enabled' => 'SMALLINT',
  115. 'connected_user_only' => 'SMALLINT',
  116. 'user_id' => 'INTEGER',
  117. 'deleted' => 'SMALLINT',
  118. 'discovery_locked' => 'SMALLINT',
  119. 'locked_timestamp' => 'TIMESTAMP',
  120. },
  121. 'device_filter_instances' => {
  122. 'id' => 'SERIAL',
  123. 'instance_id' => 'INTEGER',
  124. 'device_id' => 'INTEGER',
  125. },
  126. 'device_l3_interfaces' => {
  127. 'id' => 'SERIAL',
  128. 'device_id' => 'INTEGER',
  129. 'snmpin' => 'INTEGER',
  130. 'interface_type' => 'SMALLINT',
  131. 'name' => 'VARCHAR(100)',
  132. },
  133. 'device_models' => {
  134. 'id' => 'SERIAL',
  135. 'model_name' => 'VARCHAR(200)',
  136. 'vendor_id' => 'INTEGER',
  137. 'poe_in' => 'SMALLINT',
  138. 'poe_out' => 'SMALLINT',
  139. 'nagios_template' => 'VARCHAR(200)',
  140. },
  141. 'device_ports' => {
  142. 'id' => 'BIGSERIAL',
  143. 'device_id' => 'INTEGER',
  144. 'snmp_index' => 'INTEGER',
  145. 'port' => 'INTEGER',
  146. 'ifname' => 'VARCHAR(40)',
  147. 'port_name' => 'VARCHAR(40)',
  148. 'description' => 'VARCHAR(50)',
  149. 'target_port_id' => 'INTEGER',
  150. 'auth_id' => 'BIGINT',
  151. 'last_mac_count' => 'INTEGER',
  152. 'uplink' => 'SMALLINT',
  153. 'nagios' => 'SMALLINT',
  154. 'skip' => 'SMALLINT',
  155. 'vlan' => 'INTEGER',
  156. 'tagged_vlan' => 'VARCHAR(250)',
  157. 'untagged_vlan' => 'VARCHAR(250)',
  158. 'forbidden_vlan' => 'VARCHAR(250)',
  159. },
  160. 'device_types' => {
  161. 'id' => 'SERIAL',
  162. 'name_russian' => 'VARCHAR(50)',
  163. 'name_english' => 'VARCHAR(50)',
  164. },
  165. 'dhcp_log' => {
  166. 'id' => 'BIGSERIAL',
  167. 'mac' => 'MACADDR',
  168. 'ip_int' => 'BIGINT',
  169. 'ip' => 'INET',
  170. 'action' => 'VARCHAR(10)',
  171. 'ts' => 'TIMESTAMP',
  172. 'auth_id' => 'BIGINT',
  173. 'dhcp_hostname' => 'VARCHAR(250)',
  174. 'circuit_id' => 'VARCHAR(255)',
  175. 'remote_id' => 'VARCHAR(255)',
  176. 'client_id' => 'VARCHAR(250)',
  177. },
  178. 'dhcp_queue' => {
  179. 'id' => 'BIGSERIAL',
  180. 'mac' => 'MACADDR',
  181. 'ip' => 'INET',
  182. 'action' => 'VARCHAR(10)',
  183. 'ts' => 'TIMESTAMP',
  184. 'dhcp_hostname' => 'VARCHAR(250)',
  185. },
  186. 'dns_cache' => {
  187. 'id' => 'BIGSERIAL',
  188. 'dns' => 'VARCHAR(250)',
  189. 'ip' => 'BIGINT',
  190. 'ts' => 'TIMESTAMP',
  191. },
  192. 'dns_queue' => {
  193. 'id' => 'SERIAL',
  194. 'auth_id' => 'INTEGER',
  195. 'name_type' => 'VARCHAR(10)',
  196. 'name' => 'VARCHAR(200)',
  197. 'operation_type' => 'VARCHAR(10)',
  198. 'value' => 'VARCHAR(100)',
  199. },
  200. 'filter_instances' => {
  201. 'id' => 'SERIAL',
  202. 'name' => 'VARCHAR(50)',
  203. 'description' => 'VARCHAR(200)',
  204. },
  205. 'filter_list' => {
  206. 'id' => 'SERIAL',
  207. 'name' => 'VARCHAR(50)',
  208. 'description' => 'VARCHAR(250)',
  209. 'proto' => 'VARCHAR(10)',
  210. 'dst' => 'TEXT',
  211. 'dstport' => 'VARCHAR(20)',
  212. 'srcport' => 'VARCHAR(20)',
  213. 'filter_type' => 'SMALLINT',
  214. },
  215. 'gateway_subnets' => {
  216. 'id' => 'SERIAL',
  217. 'device_id' => 'INTEGER',
  218. 'subnet_id' => 'INTEGER',
  219. },
  220. 'group_filters' => {
  221. 'id' => 'SERIAL',
  222. 'group_id' => 'INTEGER',
  223. 'filter_id' => 'INTEGER',
  224. 'rule_order' => 'INTEGER',
  225. 'action' => 'SMALLINT',
  226. },
  227. 'group_list' => {
  228. 'id' => 'SERIAL',
  229. 'instance_id' => 'INTEGER',
  230. 'group_name' => 'VARCHAR(50)',
  231. 'description' => 'VARCHAR(250)',
  232. },
  233. 'mac_history' => {
  234. 'id' => 'BIGSERIAL',
  235. 'mac' => 'VARCHAR(12)',
  236. 'ts' => 'TIMESTAMP',
  237. 'device_id' => 'BIGINT',
  238. 'port_id' => 'BIGINT',
  239. 'ip' => 'INET',
  240. 'auth_id' => 'BIGINT',
  241. 'dhcp_hostname' => 'VARCHAR(250)',
  242. },
  243. 'mac_vendors' => {
  244. 'id' => 'SERIAL',
  245. 'oui' => 'VARCHAR(20)',
  246. 'companyname' => 'VARCHAR(255)',
  247. 'companyaddress' => 'VARCHAR(255)',
  248. },
  249. 'ou' => {
  250. 'id' => 'SERIAL',
  251. 'ou_name' => 'VARCHAR(40)',
  252. 'description' => 'VARCHAR(250)',
  253. 'default_users' => 'SMALLINT',
  254. 'default_hotspot' => 'SMALLINT',
  255. 'nagios_dir' => 'VARCHAR(255)',
  256. 'nagios_host_use' => 'VARCHAR(50)',
  257. 'nagios_ping' => 'SMALLINT',
  258. 'nagios_default_service' => 'VARCHAR(100)',
  259. 'enabled' => 'SMALLINT',
  260. 'filter_group_id' => 'INTEGER',
  261. 'queue_id' => 'INTEGER',
  262. 'dynamic' => 'SMALLINT',
  263. 'life_duration' => 'DECIMAL(10,2)',
  264. 'parent_id' => 'INTEGER',
  265. },
  266. 'queue_list' => {
  267. 'id' => 'SERIAL',
  268. 'queue_name' => 'VARCHAR(20)',
  269. 'download' => 'INTEGER',
  270. 'upload' => 'INTEGER',
  271. },
  272. 'remote_syslog' => {
  273. 'id' => 'BIGSERIAL',
  274. 'ts' => 'TIMESTAMP',
  275. 'device_id' => 'BIGINT',
  276. 'ip' => 'INET',
  277. 'message' => 'TEXT',
  278. },
  279. 'sessions' => {
  280. 'id' => 'VARCHAR(128)',
  281. 'data' => 'TEXT',
  282. 'last_accessed' => 'INTEGER',
  283. },
  284. 'subnets' => {
  285. 'id' => 'SERIAL',
  286. 'subnet' => 'VARCHAR(18)',
  287. 'vlan_tag' => 'INTEGER',
  288. 'ip_int_start' => 'BIGINT',
  289. 'ip_int_stop' => 'BIGINT',
  290. 'dhcp_start' => 'BIGINT',
  291. 'dhcp_stop' => 'BIGINT',
  292. 'dhcp_lease_time' => 'INTEGER',
  293. 'gateway' => 'BIGINT',
  294. 'office' => 'SMALLINT',
  295. 'hotspot' => 'SMALLINT',
  296. 'vpn' => 'SMALLINT',
  297. 'free' => 'SMALLINT',
  298. 'dhcp' => 'SMALLINT',
  299. 'static' => 'SMALLINT',
  300. 'dhcp_update_hostname' => 'SMALLINT',
  301. 'discovery' => 'SMALLINT',
  302. 'notify' => 'SMALLINT',
  303. 'description' => 'VARCHAR(250)',
  304. },
  305. 'traffic_detail' => {
  306. 'id' => 'BIGSERIAL',
  307. 'auth_id' => 'BIGINT',
  308. 'router_id' => 'INTEGER',
  309. 'ts' => 'TIMESTAMP',
  310. 'proto' => 'SMALLINT',
  311. 'src_ip' => 'BIGINT',
  312. 'dst_ip' => 'BIGINT',
  313. 'src_port' => 'INTEGER',
  314. 'dst_port' => 'INTEGER',
  315. 'bytes' => 'BIGINT',
  316. 'pkt' => 'BIGINT',
  317. },
  318. 'unknown_mac' => {
  319. 'id' => 'BIGSERIAL',
  320. 'mac' => 'VARCHAR(12)',
  321. 'port_id' => 'BIGINT',
  322. 'device_id' => 'INTEGER',
  323. 'ts' => 'TIMESTAMP',
  324. },
  325. 'user_auth' => {
  326. 'id' => 'SERIAL',
  327. 'user_id' => 'BIGINT',
  328. 'ou_id' => 'INTEGER',
  329. 'ip' => 'INET',
  330. 'ip_int' => 'BIGINT',
  331. 'save_traf' => 'SMALLINT',
  332. 'enabled' => 'SMALLINT',
  333. 'dhcp' => 'SMALLINT',
  334. 'filter_group_id' => 'SMALLINT',
  335. 'dynamic' => 'SMALLINT',
  336. 'end_life' => 'TIMESTAMP',
  337. 'deleted' => 'SMALLINT',
  338. 'description' => 'VARCHAR(250)',
  339. 'dns_name' => 'VARCHAR(253)',
  340. 'dns_ptr_only' => 'SMALLINT',
  341. 'wikiname' => 'VARCHAR(250)',
  342. 'dhcp_acl' => 'TEXT',
  343. 'queue_id' => 'INTEGER',
  344. 'mac' => 'VARCHAR(20)',
  345. 'dhcp_action' => 'VARCHAR(10)',
  346. 'dhcp_option_set' => 'VARCHAR(50)',
  347. 'dhcp_time' => 'TIMESTAMP',
  348. 'dhcp_hostname' => 'VARCHAR(60)',
  349. 'last_found' => 'TIMESTAMP',
  350. 'arp_found' => 'TIMESTAMP',
  351. 'mac_found' => 'TIMESTAMP',
  352. 'blocked' => 'SMALLINT',
  353. 'day_quota' => 'INTEGER',
  354. 'month_quota' => 'INTEGER',
  355. 'device_model_id' => 'INTEGER',
  356. 'firmware' => 'VARCHAR(100)',
  357. 'ts' => 'TIMESTAMP',
  358. 'client_id' => 'VARCHAR(250)',
  359. 'nagios' => 'SMALLINT',
  360. 'nagios_status' => 'VARCHAR(10)',
  361. 'nagios_handler' => 'VARCHAR(50)',
  362. 'link_check' => 'SMALLINT',
  363. 'changed' => 'SMALLINT',
  364. 'dhcp_changed' => 'SMALLINT',
  365. 'changed_time' => 'TIMESTAMP',
  366. 'created_by' => 'VARCHAR(10)',
  367. },
  368. 'user_auth_alias' => {
  369. 'id' => 'SERIAL',
  370. 'auth_id' => 'INTEGER',
  371. 'alias' => 'VARCHAR(100)',
  372. 'description' => 'VARCHAR(100)',
  373. 'ts' => 'TIMESTAMP',
  374. },
  375. 'user_list' => {
  376. 'id' => 'BIGSERIAL',
  377. 'ts' => 'TIMESTAMP',
  378. 'login' => 'VARCHAR(255)',
  379. 'description' => 'VARCHAR(255)',
  380. 'enabled' => 'SMALLINT',
  381. 'blocked' => 'SMALLINT',
  382. 'deleted' => 'SMALLINT',
  383. 'ou_id' => 'INTEGER',
  384. 'device_id' => 'INTEGER',
  385. 'filter_group_id' => 'INTEGER',
  386. 'queue_id' => 'INTEGER',
  387. 'day_quota' => 'INTEGER',
  388. 'month_quota' => 'INTEGER',
  389. 'permanent' => 'SMALLINT',
  390. },
  391. 'user_sessions' => {
  392. 'id' => 'SERIAL',
  393. 'session_id' => 'VARCHAR(128)',
  394. 'user_id' => 'INTEGER',
  395. 'ip_address' => 'VARCHAR(45)',
  396. 'user_agent' => 'TEXT',
  397. 'created_at' => 'INTEGER',
  398. 'last_activity' => 'INTEGER',
  399. 'is_active' => 'SMALLINT',
  400. },
  401. 'user_stats' => {
  402. 'id' => 'BIGSERIAL',
  403. 'router_id' => 'BIGINT',
  404. 'auth_id' => 'BIGINT',
  405. 'ts' => 'TIMESTAMP',
  406. 'byte_in' => 'BIGINT',
  407. 'byte_out' => 'BIGINT',
  408. 'pkt_in' => 'INTEGER',
  409. 'pkt_out' => 'INTEGER',
  410. 'step' => 'SMALLINT',
  411. },
  412. 'user_stats_full' => {
  413. 'id' => 'BIGSERIAL',
  414. 'router_id' => 'BIGINT',
  415. 'auth_id' => 'BIGINT',
  416. 'ts' => 'TIMESTAMP',
  417. 'byte_in' => 'BIGINT',
  418. 'byte_out' => 'BIGINT',
  419. 'pkt_in' => 'INTEGER',
  420. 'pkt_out' => 'INTEGER',
  421. 'step' => 'SMALLINT',
  422. },
  423. 'variables' => {
  424. 'id' => 'SERIAL',
  425. 'name' => 'VARCHAR(30)',
  426. 'value' => 'VARCHAR(255)',
  427. 'clear_time' => 'TIMESTAMP',
  428. 'created' => 'TIMESTAMP',
  429. },
  430. 'vendors' => {
  431. 'id' => 'SERIAL',
  432. 'name' => 'VARCHAR(40)',
  433. },
  434. 'version' => {
  435. 'id' => 'INTEGER',
  436. 'version' => 'VARCHAR(10)',
  437. },
  438. 'wan_stats' => {
  439. 'id' => 'BIGSERIAL',
  440. 'ts' => 'TIMESTAMP',
  441. 'router_id' => 'INTEGER',
  442. 'interface_id' => 'INTEGER',
  443. 'bytes_in' => 'BIGINT',
  444. 'bytes_out' => 'BIGINT',
  445. 'forward_in' => 'BIGINT',
  446. 'forward_out' => 'BIGINT',
  447. },
  448. 'worklog' => {
  449. 'id' => 'BIGSERIAL',
  450. 'ts' => 'TIMESTAMP',
  451. 'auth_id' => 'BIGINT',
  452. 'customer' => 'VARCHAR(50)',
  453. 'ip' => 'INET',
  454. 'message' => 'TEXT',
  455. 'level' => 'SMALLINT',
  456. },
  457. );
  458. sub get_table_columns {
  459. my ($db, $table) = @_;
  460. my $sth = $db->column_info(undef, undef, $table, '%');
  461. my @cols;
  462. while (my $row = $sth->fetchrow_hashref) {
  463. push @cols, $row->{COLUMN_NAME};
  464. }
  465. return @cols;
  466. }
  467. sub batch_sql_cached {
  468. my ($db, $sql, $data) = @_;
  469. eval {
  470. my $sth = $db->prepare_cached($sql) or die "Unable to prepare SQL: " . $db->errstr;
  471. for my $params (@$data) {
  472. next unless @$params;
  473. $sth->execute(@$params) or die "Unable to execute with params [" . join(',', @$params) . "]: " . $sth->errstr;
  474. }
  475. $db->commit() if (!$db->{AutoCommit});
  476. 1;
  477. } or do {
  478. my $err = $@ || 'Unknown error';
  479. eval { $db->rollback() };
  480. print "batch_db_sql_cached failed: $err";
  481. return 0;
  482. };
  483. return 1;
  484. }
  485. # debug disable force
  486. $debug = 0;
  487. # === Разбор аргументов командной строки ===
  488. my $opt_clear = 0;
  489. my $opt_batch = 0;
  490. GetOptions(
  491. 'clear' => \$opt_clear,
  492. 'batch' => \$opt_batch,
  493. ) or die "Usage: $0 [--clear] [--batch]\n";
  494. # === Явное указание портов ===
  495. my $MYSQL_PORT = 3306;
  496. my $PG_PORT = 5432;
  497. # === Подключение к MySQL (источник) ===
  498. my $mysql_dsn = "dbi:mysql:database=$DBNAME;host=$DBHOST;port=$MYSQL_PORT;mysql_local_infile=1";
  499. my $mysql_db = DBI->connect($mysql_dsn, $DBUSER, $DBPASS, {
  500. RaiseError => 0,
  501. AutoCommit => 1,
  502. mysql_enable_utf8 => 1
  503. });
  504. if (!defined $mysql_db) {
  505. die "Cannot connect to MySQL server: $DBI::errstr\n";
  506. }
  507. $mysql_db->do('SET NAMES utf8mb4');
  508. # === Подключение к PostgreSQL (цель) ===
  509. my $pg_dsn = "dbi:Pg:dbname=$DBNAME;host=$DBHOST;port=$PG_PORT;";
  510. my $pg_db = DBI->connect($pg_dsn, $DBUSER, $DBPASS, {
  511. RaiseError => 0,
  512. AutoCommit => 1,
  513. pg_enable_utf8 => 1,
  514. pg_server_prepare => 0
  515. });
  516. if (!defined $pg_db) {
  517. print "Cannot connect to PostgreSQL server: $DBI::errstr\n";
  518. print "For install/configure PostgreSQL server please run migrate2psql.sh!\n";
  519. exit 100;
  520. }
  521. # === Получение списка таблиц ===
  522. print "Fetching table list from MySQL...\n";
  523. my @migration_tables = get_records_sql($mysql_db, 'SHOW TABLES');
  524. my %tables;
  525. my $table_index = 'Tables_in_' . $DBNAME;
  526. foreach my $row (@migration_tables) {
  527. next unless $row && exists $row->{$table_index};
  528. my $table_name = $row->{$table_index};
  529. # Пропускаем traffic_detail (слишком большая)
  530. $tables{$table_name} = ($table_name !~ /(traffic_detail|sessions)/) ? 1 : 0;
  531. }
  532. # Фильтруем только те, что будем мигрировать
  533. my @tables_to_migrate = sort grep { $tables{$_} } keys %tables;
  534. my $total_tables = scalar @tables_to_migrate;
  535. if ($total_tables == 0) {
  536. print "No tables to migrate!\n";
  537. exit 0;
  538. }
  539. # === Опционально: очистка всех таблиц перед импортом ===
  540. if ($opt_clear) {
  541. print "\n⚠️ --clear mode: Truncating all target tables before import...\n";
  542. for my $table (@tables_to_migrate) {
  543. eval {
  544. $pg_db->do("TRUNCATE TABLE \"$table\" RESTART IDENTITY");
  545. };
  546. if ($@) {
  547. chomp $@;
  548. print " ⚠️ Failed to truncate table '$table': $@\n";
  549. } else {
  550. print " → Truncated: $table\n";
  551. }
  552. }
  553. print "\n";
  554. }
  555. print "\n=== Check DB schema ===\n\n";
  556. my $mysql_schema_status = 1;
  557. my %mysql_tables;
  558. # --- Этап 1: Проверяем, что все таблицы и колонки MySQL есть в PG-схеме ---
  559. for my $idx (0 .. $#tables_to_migrate) {
  560. my $table = $tables_to_migrate[$idx];
  561. my $table_num = $idx + 1;
  562. if ($table =~ /(traffic_detail|sessions)/) { next; }
  563. print "[$table_num/$total_tables] Processing table: $table\n";
  564. if (!exists $pg_schema{$table}) {
  565. print " ❗ WARNING: Table $table not found in Postgres DB schema! Will be skip for migration.\n";
  566. # Не считаем критичной ошибкой
  567. next;
  568. }
  569. my @columns = get_table_columns($mysql_db, $table);
  570. foreach my $column_name (@columns) {
  571. my $col_lower = lc($column_name); # Приводим к нижнему регистру
  572. if (!exists $pg_schema{$table}->{$col_lower}) {
  573. print " ❗ WARNING: Column $column_name in table $table not in PG schema. Will be skip for migration. \n";
  574. # Не считаем критичной ошибкой
  575. } else {
  576. $mysql_tables{$table}->{$col_lower} = 1;
  577. }
  578. }
  579. }
  580. # --- Этап 2: Проверяем, что все таблицы и колонки PG-схемы есть в MySQL ---
  581. for my $table (keys %pg_schema) {
  582. if ($table =~ /(traffic_detail|sessions)/) { next; }
  583. if (!exists $mysql_tables{$table}) {
  584. print " ❗ ERROR: Table $table from PG schema not found in source MySQL database!\n";
  585. $mysql_schema_status = 0;
  586. next;
  587. }
  588. for my $column_name (keys %{ $pg_schema{$table} }) {
  589. if (!exists $mysql_tables{$table}->{$column_name}) {
  590. print " ❗ ERROR: Column $column_name in table $table missing in MySQL!\n";
  591. $mysql_schema_status = 0;
  592. }
  593. }
  594. }
  595. if (!$mysql_schema_status) {
  596. print "\nSchema validation failed. Check database and try again.\n";
  597. exit 103;
  598. }
  599. print "\n=== Starting migration of $total_tables tables ===\n\n";
  600. # === Миграция по таблицам с прогрессом ===
  601. for my $idx (0 .. $#tables_to_migrate) {
  602. my $table = $tables_to_migrate[$idx];
  603. my $table_num = $idx + 1;
  604. if (!exists $pg_schema{$table}) { next; }
  605. print "[$table_num/$total_tables] Processing table: $table\n";
  606. my $rec_count = get_count_records($mysql_db, $table);
  607. print " → Expected records: $rec_count\n";
  608. if ($rec_count == 0) {
  609. print " → Empty table. Skipping.\n\n";
  610. next;
  611. }
  612. # === Построчное чтение ===
  613. my $select_sth = $mysql_db->prepare("SELECT * FROM `$table`");
  614. $select_sth->execute();
  615. my $inserted = 0;
  616. my $errors = 0;
  617. # === Режим вставки: построчный или пакетный ===
  618. if ($opt_batch) {
  619. print " → Using BATCH mode ($chunk_count records per chunk)\n";
  620. # Получаем список колонок один раз
  621. my @columns = get_table_columns($mysql_db, $table);
  622. my $quoted_columns = '"' . join('", "', @columns) . '"';
  623. my $placeholders = join(', ', ('?') x @columns);
  624. my $insert_sql = "INSERT INTO \"$table\" ($quoted_columns) VALUES ($placeholders)";
  625. my @batch_buffer;
  626. my $chunk_size = $chunk_count;
  627. while (my $row = $select_sth->fetchrow_hashref) {
  628. my @values;
  629. for my $key (@columns) {
  630. if (!exists $pg_schema{$table}->{lc($key)}) { next; }
  631. my $value = $row->{$key};
  632. if (lc($key) eq 'ip') {
  633. $value = undef if !defined($value) || $value eq '';
  634. }
  635. push @values, $value;
  636. }
  637. push @batch_buffer, \@values;
  638. if (@batch_buffer >= $chunk_count) {
  639. my $insert_status = batch_sql_cached($pg_db, $insert_sql, \@batch_buffer);
  640. if ($insert_status) { $inserted += @batch_buffer; } else { $errors+=@batch_buffer; }
  641. @batch_buffer = ();
  642. }
  643. }
  644. # Остаток
  645. if (@batch_buffer) {
  646. my $insert_status = batch_sql_cached($pg_db, $insert_sql, \@batch_buffer);
  647. if ($insert_status) { $inserted += @batch_buffer; } else { $errors+=@batch_buffer; }
  648. }
  649. } else {
  650. # === построчный режим ===
  651. while (my $row = $select_sth->fetchrow_hashref) {
  652. # === Приведение имён полей к нижнему регистру ===
  653. my %row_normalized;
  654. while (my ($key, $value) = each %$row) {
  655. my $n_key = lc($key);
  656. if ($n_key eq 'ip') {
  657. if (!defined $value || $value eq '') { $value = undef; }
  658. }
  659. $row_normalized{$n_key} = $value;
  660. }
  661. my $ret_id = insert_record($pg_db, $table, \%row_normalized);
  662. if ($ret_id>0) { $inserted++; } else {
  663. $errors++;
  664. print Dumper(\%row_normalized) if ($debug);
  665. }
  666. }
  667. $select_sth->finish();
  668. }
  669. # === Итог по таблице ===
  670. my $status = ($errors == 0) ? "✅ SUCCESS" : "⚠️ COMPLETED WITH ERRORS";
  671. print " → Result: $status\n";
  672. print " Inserted: $inserted | Errors: $errors | Expected: $rec_count\n";
  673. if ($inserted + $errors != $rec_count) {
  674. print " ❗ WARNING: Record count mismatch! (source: $rec_count, processed: " . ($inserted + $errors) . ")\n";
  675. }
  676. print "\n";
  677. }
  678. print "🎉 Migration completed! Processed $total_tables tables.\n";
  679. exit 0;