dbXapp 2.0
RAD, CMS, Module und Runtime-IDE fuer dbXapp
Loading...
Searching...
No Matches
dbxDD.class.php
Go to the documentation of this file.
1<?php
2include_once 'dbxDB.class.php';
3
56class dbxDD extends dbxDB
57{
58 protected string $_remember_modul = 'dbx';
59 protected float $_max_step_runtime = 3.0;
60 protected int $_chunk_size = 500;
61
67 public function __construct()
68 {
69 parent::__construct();
70 }
71
72
73 /* =====================================================
74 * CONFIG
75 * ===================================================== */
76
84 public function set_step_runtime(float $seconds): void
85 {
86 if ($seconds > 0) {
87 $this->_max_step_runtime = $seconds;
88 }
89 }
90
96 public function get_step_runtime(): float
97 {
99 }
100
108 public function set_chunk_size(int $chunk_size): void
109 {
110 if ($chunk_size > 0) {
111 $this->_chunk_size = $chunk_size;
112 }
113 }
114
120 public function get_chunk_size(): int
121 {
122 return $this->_chunk_size;
123 }
124
125
126 /* =====================================================
127 * DD LOAD / CACHE
128 * ===================================================== */
129
142 public function load_dd(string $dd): array
143 {
144 return parent::load_dd($dd);
145 }
146
161 protected function get_dd_cache_info(string $dd): array
162 {
163 $dd_sys = $this->load_dd($dd);
164
165 return [
166 'dd_status' => $dd_sys['dd_status'] ?? 0,
167 'dd_modul' => $dd_sys['dd_modul'] ?? '',
168 'dd_name' => $dd_sys['dd_name'] ?? '',
169 ];
170 }
171
183 public function clear_dd_cache(string $dd): void
184 {
185 $dd_sys = $this->get_dd_cache_info($dd);
186 $dd_status = $dd_sys['dd_status'] ?? 0;
187 $dd_modul = $dd_sys['dd_modul'] ?? '';
188 $dd_name = $dd_sys['dd_name'] ?? '';
189
190 if ($dd_modul && $dd_name && isset($_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name])) {
191 unset($_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]);
192 return;
193 }
194
200 $activ_modul = dbx()->get_system_var('dbx_activ_modul', 'dbx');
201
202 if (strpos($dd, '|') !== false) {
203 $parts = explode('|', $dd, 2);
204 $dd_modul = trim($parts[0]);
205 $dd_name = trim($parts[1]);
206
207 if ($dd_modul === 'modul' || $dd_modul === '') {
208 $dd_modul = $activ_modul;
209 }
210 } else {
211 $dd_modul = $activ_modul;
212 $dd_name = $dd;
213 }
214
215 if (isset($_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name])) {
216 unset($_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]);
217 }
218 }
219
227 public function get_dd_indexes(string $dd): array
228 {
229 $dd_sys = $this->get_dd_cache_info($dd);
230 $dd_status = $dd_sys['dd_status'] ?? 0;
231 $dd_modul = $dd_sys['dd_modul'] ?? '';
232 $dd_name = $dd_sys['dd_name'] ?? '';
233
234 if ($dd_status <= 0) {
235 return [];
236 }
237
238 return $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]['indexes'] ?? [];
239 }
240
253 public function get_dd_model(string $dd): array
254 {
255 $dd_sys = $this->get_dd_cache_info($dd);
256 $dd_status = $dd_sys['dd_status'] ?? 0;
257 $dd_modul = $dd_sys['dd_modul'] ?? '';
258 $dd_name = $dd_sys['dd_name'] ?? '';
259
260 if ($dd_status <= 0) {
261 return [];
262 }
263
264 return [
265 'table' => $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]['table'] ?? [],
266 'fields' => $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]['fields'] ?? [],
267 'indexes' => $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]['indexes'] ?? [],
268 ];
269 }
270
283 public function get_dd_exist($dd): bool
284 {
285 $activ_modul = dbx()->get_system_var('dbx_activ_modul', 'dbx');
286
287 if (strpos($dd, '|') !== false) {
288 $parts = explode('|', $dd, 2);
289 $dd_modul = trim($parts[0]);
290 $dd_name = trim($parts[1]);
291
292 if ($dd_modul === 'modul' || $dd_modul === '') {
293 $dd_modul = $activ_modul;
294 }
295 } else {
296 $dd_modul = $activ_modul;
297 $dd_name = $dd;
298 }
299
300 $dd_file1 = dbx_os_path_file(dbx_get_base_dir() . 'dbx/modules/' . $dd_modul . '/dd/' . $dd_name . '.dd.php');
301 $dd_file2 = dbx_os_path_file(dbx_get_base_dir() . 'dbx/modules/dbx/dd/' . $dd_name . '.dd.php');
302
303 return file_exists($dd_file1) || file_exists($dd_file2);
304 }
305
306
307 /* =====================================================
308 * DD TEMPLATE RENDER
309 * ===================================================== */
310
318 protected function get_dd_template_file(string $name): string
319 {
320 $file = dbx_os_path_file(dbx_get_base_dir() . 'dbx/tpl/dd/' . $name . '.php');
321 if (file_exists($file)) {
322 return $file;
323 }
324
325 return dbx_os_path_file(dbx_get_base_dir() . 'dbx/modules/dbx/tpl/php/' . $name . '.php');
326 }
327
328 protected function dd_template_value(mixed $value): string
329 {
330 return str_replace(
331 ['\\', "'"],
332 ['\\\\', "\\'"],
333 (string)$value
334 );
335 }
336
350 protected function render_dd_template(string $template_name, array $vars = []): string
351 {
352 $file = $this->get_dd_template_file($template_name);
353
354 if (!file_exists($file)) {
355 dbx()->sys_msg('error', 'dd', $template_name, 'missing template', $file);
356 return '';
357 }
358
359 $content = file_get_contents($file);
360 if ($content === false) {
361 return '';
362 }
363
364 $replace = [];
365 foreach ($vars as $key => $value) {
366 if (is_array($value)) {
367 foreach ($value as $k => $v) {
368 $replace['{' . $k . '}'] = $this->dd_template_value($v);
369 }
370 } else {
371 $replace['{' . $key . '}'] = (string)$value;
372 }
373 }
374
375 return strtr($content, $replace);
376 }
377
386 protected function get_dd_file_path(string $modul, string $dd): string
387 {
388 if (!$modul) {
389 $modul = dbx()->get_system_var('dbx_activ_modul', 'dbx');
390 }
391
392 return dbx_os_path_file(dbx_get_base_dir() . 'dbx/modules/' . $modul . '/dd/' . $dd . '.dd.php');
393 }
394
406 public function save_dd(string $modul, string $dd, array $table, array $fields, array $indexes = []): int
407 {
408 $ok = $this->write_dd($modul, $dd, $table, $fields, $indexes);
409
410 if ($ok) {
411 $this->clear_dd_cache($modul . '|' . $dd);
412 }
413
414 return $ok;
415 }
416
432 public function write_dd(string $modul, string $dd, array $table, array $fields, array $indexes = []): int
433 {
434 $table = $this->normalize_table_record($dd, $table);
435
436 $table_block = $this->render_dd_template('dd_table', ['table' => $table]);
437
438 $fields_block = '';
439 foreach ($fields as $field) {
440 $field = $this->normalize_field_record($field);
441 $fields_block .= $this->render_dd_template('dd_field', ['field' => $field]) . "\n";
442 }
443 $fields_block = trim($fields_block);
444
445 $indexes_block = '';
446 foreach ($indexes as $index) {
447 $index = $this->normalize_index_record($index);
448 $indexes_block .= $this->render_dd_template('dd_index', ['index' => $index]) . "\n";
449 }
450 $indexes_block = trim($indexes_block);
451
452 $content = $this->render_dd_template('dd_file', [
453 'table_block' => $table_block,
454 'fields_block' => $fields_block,
455 'indexes_block' => $indexes_block,
456 ]);
457
458 if (!$content) {
459 dbx()->sys_msg('error', 'dd', $dd, 'write_dd', 'empty dd content');
460 return 0;
461 }
462
463 $file = $this->get_dd_file_path($modul, $dd);
464 $dir = dirname($file);
465
466 if (!is_dir($dir)) {
467 @mkdir($dir, 0777, true);
468 }
469
470 $ok = file_put_contents($file, $content);
471 if ($ok === false) {
472 dbx()->sys_msg('error', 'dd', $dd, 'write_dd', $file);
473 return 0;
474 }
475
476 return 1;
477 }
478
479
480 /* =====================================================
481 * NORMALIZE / INFER
482 * ===================================================== */
483
484 public function dd_table_schema_keys(): array
485 {
486 return [
487 'server',
488 'table',
489 'datadic',
490 'primary',
491 'language',
492 'version',
493 'autosync',
494 'cache',
495 'trash',
496 'trace',
497 'update_sql',
498 'default_sort',
499 'form-dd-table',
500 'read',
501 'create',
502 'update',
503 'delete',
504 'read_owner',
505 'create_owner',
506 'update_owner',
507 'delete_owner',
508 ];
509 }
510
511 public function dd_field_schema_keys(): array
512 {
513 return [
514 'name',
515 'type',
516 'index',
517 'length',
518 'default',
519 'label',
520 'rules',
521 'tooltip',
522 'errormsg',
523 'placeholder',
524 'convert',
525 'protect',
526 'group',
527 'mask',
528 'data',
529 'options',
530 'tpl',
531 'js',
532 'prompt',
533 ];
534 }
535
536 public function dd_index_schema_keys(): array
537 {
538 return [
539 'name',
540 'type',
541 'fields',
542 'unique',
543 'comment',
544 ];
545 }
546
547 public function normalize_dd_field(array $field): array
548 {
549 return $this->normalize_field_record($field);
550 }
551
552 public function normalize_dd_index(array $index): array
553 {
554 return $this->normalize_index_record($index);
555 }
556
570 protected function normalize_table_record(string $dd, array $table): array
571 {
572 $defaults = [
573 'server' => $table['server'] ?? 'dbXsystem',
574 'table' => $table['table'] ?? $dd,
575 'datadic' => $dd,
576 'primary' => '',
577 'language' => '',
578 'version' => '1.0',
579 'autosync' => '0',
580 'cache' => '0',
581 'trash' => '0',
582 'trace' => '0',
583 'update_sql' => '',
584 'default_sort' => '',
585 'form-dd-table'=> '',
586
587 'read' => '*',
588 'create' => '*',
589 'update' => '*',
590 'delete' => '*',
591
592 'read_owner' => '*',
593 'create_owner' => '*',
594 'update_owner' => '*',
595 'delete_owner' => '*',
596 ];
597
598 foreach ($defaults as $k => $v) {
599 if (!isset($table[$k])) {
600 $table[$k] = $v;
601 }
602 }
603
604 $table['datadic'] = $dd;
605
606 return $this->sort_schema_record($table, $this->dd_table_schema_keys());
607 }
608
616 protected function normalize_field_record(array $field): array
617 {
618 $defaults = [
619 'name' => '',
620 'type' => 'varchar',
621 'index' => '',
622 'length' => '',
623 'default' => '',
624 'label' => '',
625 'rules' => '',
626 'tooltip' => '',
627 'errormsg' => '',
628 'placeholder' => '',
629 'convert' => '',
630 'protect' => '0',
631 'group' => '',
632 'mask' => '',
633 'data' => '',
634 'options' => '',
635 'tpl' => '',
636 'js' => '',
637 'prompt' => '',
638 ];
639
640 foreach ($defaults as $k => $v) {
641 if (!isset($field[$k])) {
642 $field[$k] = $v;
643 }
644 }
645
646 return $field;
647 }
648
656 protected function normalize_index_record(array $index): array
657 {
658 $defaults = [
659 'name' => '',
660 'type' => 'INDEX',
661 'fields' => '',
662 'unique' => '0',
663 'comment' => '',
664 ];
665
666 foreach ($defaults as $k => $v) {
667 if (!isset($index[$k])) {
668 $index[$k] = $v;
669 }
670 }
671
672 return $index;
673 }
674
682 protected function is_system_field(string $name): bool
683 {
684 return in_array(strtolower($name), ['id', 'create_date', 'create_uid', 'update_date', 'update_uid', 'owner'], true);
685 }
686
694 protected function infer_label_from_name(string $name): string
695 {
696 if ($this->is_system_field($name)) {
697 return str_replace('_', ' ', ucfirst($name));
698 }
699
700 return $name;
701 }
702
710 protected function infer_rules_from_field(array $field): string
711 {
712 $name = strtolower((string)($field['name'] ?? ''));
713 $type = strtolower((string)($field['type'] ?? 'varchar'));
714 $length = trim((string)($field['length'] ?? ''));
715
716 if ($name === 'id') {
717 return 'int';
718 }
719
720 if (in_array($type, ['bool', 'boolean', 'bit'], true)) {
721 return 'int';
722 }
723
724 if (in_array($type, ['int', 'integer', 'bigint', 'smallint', 'mediumint', 'tinyint'], true)) {
725 return 'int';
726 }
727
728 if (in_array($type, ['decimal', 'numeric'], true)) {
729 return 'decimal';
730 }
731
732 if (in_array($type, ['float', 'double', 'real'], true)) {
733 return 'decimal';
734 }
735
736 if (in_array($type, ['date', 'datetime', 'timestamp'], true)) {
737 return $type;
738 }
739
740 if ($type === 'time' || $type === 'year') {
741 return 'parameter';
742 }
743
744 if (($type === 'tinyint' || $type === 'int') && $length === '1') {
745 return 'int';
746 }
747
748 return 'parameter';
749 }
750
758 protected function infer_tpl_from_field(array $field): string
759 {
760 $type = strtolower((string)($field['type'] ?? 'varchar'));
761 $length = trim((string)($field['length'] ?? ''));
762
763 if (in_array($type, ['bool', 'boolean', 'bit'], true)
764 || (in_array($type, ['tinyint', 'int', 'integer'], true) && $length === '1')) {
765 return 'checkbox-label';
766 }
767
768 if (in_array($type, ['int', 'integer', 'bigint', 'smallint', 'mediumint', 'tinyint'], true)) {
769 return 'integer-label';
770 }
771
772 if ($type === 'date') {
773 return 'date-label';
774 }
775
776 if (in_array($type, ['datetime', 'timestamp'], true)) {
777 return 'datetime-label';
778 }
779
780 if (in_array($type, ['text', 'mediumtext', 'longtext'], true)) {
781 return 'textarea-label';
782 }
783
784 return 'text-label';
785 }
786
794 protected function normalize_default_value(mixed $value): string
795 {
796 if ($value === null) {
797 return '';
798 }
799
800 $value = trim((string)$value);
801 $value = trim($value, " \t\n\r\0\x0B'\"");
802
803 return $value;
804 }
805
817 protected function parse_sql_type(string $type): array
818 {
819 $type = trim($type);
820
821 if (preg_match('/^([a-zA-Z0-9_]+)\s*(?:\‍((.*?)\‍))?/i', $type, $m)) {
822 return [
823 'type' => strtolower($m[1]),
824 'length' => (string)($m[2] ?? ''),
825 ];
826 }
827
828 return [
829 'type' => strtolower($type),
830 'length' => '',
831 ];
832 }
833
854 protected function map_db_type_to_dd_type(string $dbType, string $rawType, string $length = ''): string
855 {
856 $dbType = strtolower(trim($dbType));
857 $rawType = strtolower(trim($rawType));
858 $length = trim($length);
859
860 if (in_array($rawType, ['bool', 'boolean'], true)) {
861 return 'bool';
862 }
863
864 if (in_array($rawType, ['tinyint', 'smallint', 'mediumint', 'bigint'], true)) {
865 return $rawType;
866 }
867
868 if (in_array($rawType, ['int', 'integer', 'serial', 'number'], true)) {
869 return 'int';
870 }
871
872 if (in_array($rawType, ['bit'], true)) {
873 return 'bit';
874 }
875
876 if (in_array($rawType, ['date', 'time', 'year'], true)) {
877 return $rawType;
878 }
879
880 if (in_array($rawType, ['datetime', 'timestamp'], true)) {
881 return 'datetime';
882 }
883
884 if (in_array($rawType, ['char', 'nchar'], true)) {
885 return 'char';
886 }
887
888 if (in_array($rawType, ['varchar', 'varchar2', 'nvarchar', 'string'], true)) {
889 return 'varchar';
890 }
891
892 if (in_array($rawType, ['tinytext', 'mediumtext', 'longtext'], true)) {
893 return $rawType;
894 }
895
896 if (in_array($rawType, ['text', 'clob'], true)) {
897 return 'text';
898 }
899
900 if (in_array($rawType, ['decimal', 'numeric'], true)) {
901 return 'decimal';
902 }
903
904 if (in_array($rawType, ['float', 'real'], true)) {
905 return 'float';
906 }
907
908 if (in_array($rawType, ['double'], true)) {
909 return 'double';
910 }
911
912 if (in_array($rawType, ['binary', 'varbinary', 'tinyblob', 'blob', 'mediumblob', 'longblob', 'json', 'enum', 'set'], true)) {
913 return $rawType;
914 }
915
916 if ($dbType === 'sqlite') {
917 return 'text';
918 }
919
920 return 'varchar';
921 }
922
923
924 /* =====================================================
925 * DB -> DD READER
926 * ===================================================== */
927
941 public function get_db_fields($server, $tableName): array
942 {
943 $db_fields = [];
944
945 if (!$this->get_table_exist($server, $tableName)) {
946 return $db_fields;
947 }
948
949 $dbType = $this->get_db_type($server);
950
951 switch ($dbType) {
952 case 'mysql':
953 $sql = "SHOW COLUMNS FROM " . $this->quote_ident($server, $tableName);
954 $rows = $this->rawQuery($server, $sql);
955
956 foreach ($rows as $row) {
957 $typeInfo = $this->parse_sql_type((string)($row['Type'] ?? ''));
958 $db_fields[] = $this->build_dd_field_from_db_meta(
959 (string)($row['Field'] ?? ''),
960 $dbType,
961 (string)($typeInfo['type'] ?? 'varchar'),
962 (string)($typeInfo['length'] ?? ''),
963 ((string)($row['Null'] ?? 'YES') === 'YES') ? '1' : '0',
964 $row['Default'] ?? '',
965 (string)($row['Key'] ?? '')
966 );
967 }
968 break;
969
970 case 'sqlite':
971 $sql = "PRAGMA table_info(" . $this->quote_ident($server, $tableName) . ")";
972 $rows = $this->rawQuery($server, $sql);
973
974 foreach ($rows as $row) {
975 $typeInfo = $this->parse_sql_type((string)($row['type'] ?? ''));
976 $index = ((int)($row['pk'] ?? 0) === 1) ? 'PRI' : '';
977
978 $db_fields[] = $this->build_dd_field_from_db_meta(
979 (string)($row['name'] ?? ''),
980 $dbType,
981 (string)($typeInfo['type'] ?? 'text'),
982 (string)($typeInfo['length'] ?? ''),
983 ((int)($row['notnull'] ?? 0) === 1) ? '0' : '1',
984 $row['dflt_value'] ?? '',
985 $index
986 );
987 }
988 break;
989
990 case 'pgsql':
991 $sql = "
992 SELECT column_name, data_type, character_maximum_length, is_nullable, column_default
993 FROM information_schema.columns
994 WHERE table_name = " . $this->sql_quote($server, $tableName) . "
995 ORDER BY ordinal_position
996 ";
997 $rows = $this->rawQuery($server, $sql);
998
999 foreach ($rows as $row) {
1000 $db_fields[] = $this->build_dd_field_from_db_meta(
1001 (string)($row['column_name'] ?? ''),
1002 $dbType,
1003 (string)($row['data_type'] ?? 'text'),
1004 (string)($row['character_maximum_length'] ?? ''),
1005 ((string)($row['is_nullable'] ?? 'YES') === 'YES') ? '1' : '0',
1006 $row['column_default'] ?? '',
1007 ''
1008 );
1009 }
1010 break;
1011
1012 case 'sqlsrv':
1013 $sql = "
1014 SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE, COLUMN_DEFAULT
1015 FROM INFORMATION_SCHEMA.COLUMNS
1016 WHERE TABLE_NAME = " . $this->sql_quote($server, $tableName) . "
1017 ORDER BY ORDINAL_POSITION
1018 ";
1019 $rows = $this->rawQuery($server, $sql);
1020
1021 foreach ($rows as $row) {
1022 $db_fields[] = $this->build_dd_field_from_db_meta(
1023 (string)($row['COLUMN_NAME'] ?? ''),
1024 $dbType,
1025 (string)($row['DATA_TYPE'] ?? 'varchar'),
1026 (string)($row['CHARACTER_MAXIMUM_LENGTH'] ?? ''),
1027 ((string)($row['IS_NULLABLE'] ?? 'YES') === 'YES') ? '1' : '0',
1028 $row['COLUMN_DEFAULT'] ?? '',
1029 ''
1030 );
1031 }
1032 break;
1033
1034 case 'oci':
1035 $sql = "
1036 SELECT column_name, data_type, data_length, nullable, data_default
1037 FROM user_tab_columns
1038 WHERE table_name = UPPER(" . $this->sql_quote($server, $tableName) . ")
1039 ORDER BY column_id
1040 ";
1041 $rows = $this->rawQuery($server, $sql);
1042
1043 foreach ($rows as $row) {
1044 $db_fields[] = $this->build_dd_field_from_db_meta(
1045 (string)($row['COLUMN_NAME'] ?? ''),
1046 $dbType,
1047 (string)($row['DATA_TYPE'] ?? 'varchar2'),
1048 (string)($row['DATA_LENGTH'] ?? ''),
1049 ((string)($row['NULLABLE'] ?? 'Y') === 'Y') ? '1' : '0',
1050 $row['DATA_DEFAULT'] ?? '',
1051 ''
1052 );
1053 }
1054 break;
1055 }
1056
1057 return $db_fields;
1058 }
1059
1068 public function get_db_indexes(string $server, string $tableName): array
1069 {
1070 $indexes = [];
1071
1072 if (!$this->get_table_exist($server, $tableName)) {
1073 return $indexes;
1074 }
1075
1076 $dbType = $this->get_db_type($server);
1077
1078 switch ($dbType) {
1079 case 'mysql':
1080 $sql = "SHOW INDEX FROM " . $this->quote_ident($server, $tableName);
1081 $rows = $this->rawQuery($server, $sql);
1082
1083 $grouped = [];
1084 foreach ($rows as $row) {
1085 $keyName = (string)($row['Key_name'] ?? '');
1086 $colName = (string)($row['Column_name'] ?? '');
1087 $nonUnique = (int)($row['Non_unique'] ?? 1);
1088
1089 if (!isset($grouped[$keyName])) {
1090 $grouped[$keyName] = [
1091 'name' => $keyName,
1092 'type' => ($keyName === 'PRIMARY') ? 'PRIMARY' : (($nonUnique === 0) ? 'UNIQUE' : 'INDEX'),
1093 'fields' => [],
1094 'unique' => ($keyName === 'PRIMARY' || $nonUnique === 0) ? '1' : '0',
1095 'comment' => '',
1096 ];
1097 }
1098
1099 $grouped[$keyName]['fields'][] = $colName;
1100 }
1101
1102 foreach ($grouped as $idx) {
1103 $idx['fields'] = implode(',', $idx['fields']);
1104 $indexes[] = $idx;
1105 }
1106 break;
1107
1108 case 'sqlite':
1109 $sql = "PRAGMA index_list(" . $this->quote_ident($server, $tableName) . ")";
1110 $rows = $this->rawQuery($server, $sql);
1111
1112 foreach ($rows as $row) {
1113 $name = (string)($row['name'] ?? '');
1114 $unique = ((int)($row['unique'] ?? 0) === 1) ? '1' : '0';
1115 $origin = (string)($row['origin'] ?? '');
1116
1117 $sql2 = "PRAGMA index_info(" . $this->quote_ident($server, $name) . ")";
1118 $cols = $this->rawQuery($server, $sql2);
1119
1120 $fields = [];
1121 foreach ($cols as $col) {
1122 $fields[] = (string)($col['name'] ?? '');
1123 }
1124
1125 $type = 'INDEX';
1126 if ($origin === 'pk') {
1127 $type = 'PRIMARY';
1128 $unique = '1';
1129 } elseif ($unique === '1') {
1130 $type = 'UNIQUE';
1131 }
1132
1133 $indexes[] = [
1134 'name' => $name,
1135 'type' => $type,
1136 'fields' => implode(',', $fields),
1137 'unique' => $unique,
1138 'comment' => '',
1139 ];
1140 }
1141 break;
1142
1143 case 'pgsql':
1144 $sql = "
1145 SELECT indexname, indexdef
1146 FROM pg_indexes
1147 WHERE tablename = " . $this->sql_quote($server, $tableName);
1148 $rows = $this->rawQuery($server, $sql);
1149
1150 foreach ($rows as $row) {
1151 $name = (string)($row['indexname'] ?? '');
1152 $def = (string)($row['indexdef'] ?? '');
1153
1154 preg_match('/\‍((.*?)\‍)/', $def, $m);
1155 $fields = $m[1] ?? '';
1156 $fields = str_replace(['"', ' '], '', $fields);
1157
1158 $unique = (stripos($def, 'UNIQUE INDEX') !== false) ? '1' : '0';
1159 $type = ($unique === '1') ? 'UNIQUE' : 'INDEX';
1160
1161 $indexes[] = [
1162 'name' => $name,
1163 'type' => $type,
1164 'fields' => $fields,
1165 'unique' => $unique,
1166 'comment' => '',
1167 ];
1168 }
1169 break;
1170 }
1171
1172 return $indexes;
1173 }
1174
1189 string $name,
1190 string $dbType,
1191 string $rawType,
1192 string $length,
1193 string $isNull,
1194 mixed $default,
1195 string $index = ''
1196 ): array {
1197 $ddType = $this->map_db_type_to_dd_type($dbType, $rawType, $length);
1198
1199 $field = [
1200 'name' => $name,
1201 'type' => $ddType,
1202 'index' => strtoupper((string)$index),
1203 'length' => $length,
1204 'default' => $this->normalize_default_value($default),
1205 'label' => $this->infer_label_from_name($name),
1206 'rules' => '',
1207 'tooltip' => '',
1208 'errormsg' => '',
1209 'placeholder' => '',
1210 'convert' => '',
1211 'protect' => $this->is_system_field($name) ? '2' : '0',
1212 'group' => '',
1213 'mask' => '',
1214 'data' => '',
1215 'options' => '',
1216 'tpl' => '',
1217 'js' => '',
1218 ];
1219
1220 $field['rules'] = $this->infer_rules_from_field($field);
1221 $field['tpl'] = $this->infer_tpl_from_field($field);
1222
1223 return $this->sort_schema_record($field, $this->dd_field_schema_keys());
1224 }
1225
1226
1227 /* =====================================================
1228 * DD -> DB SQL
1229 * ===================================================== */
1230
1239 protected function sql_quote(string $server, mixed $value): string
1240 {
1241 return "'" . $this->escape((string)$value, $server) . "'";
1242 }
1243
1252 protected function quote_ident(string $server, string $name): string
1253 {
1254 $dbType = $this->get_db_type($server);
1255
1256 return match ($dbType) {
1257 'mysql' => '`' . str_replace('`', '``', $name) . '`',
1258 'sqlsrv' => '[' . str_replace(']', ']]', $name) . ']',
1259 default => '"' . str_replace('"', '""', $name) . '"',
1260 };
1261 }
1262
1277 protected function map_dd_type_to_sql_type(string $dbType, string $type, string $length = ''): string
1278 {
1279 $type = strtolower(trim($type));
1280 $len = trim($length);
1281
1282 switch ($dbType) {
1283 case 'sqlite':
1284 return match ($type) {
1285 'bool',
1286 'boolean',
1287 'tinyint',
1288 'smallint',
1289 'mediumint',
1290 'int',
1291 'integer',
1292 'bigint',
1293 'bit' => 'INTEGER',
1294 'decimal',
1295 'numeric',
1296 'float',
1297 'double',
1298 'real' => 'REAL',
1299 'binary',
1300 'varbinary',
1301 'tinyblob',
1302 'blob',
1303 'mediumblob',
1304 'longblob' => 'BLOB',
1305 default => 'TEXT',
1306 };
1307
1308 case 'mysql':
1309 return match ($type) {
1310 'tinyint' => 'TINYINT' . ($len !== '' ? '(' . $len . ')' : ''),
1311 'smallint' => 'SMALLINT' . ($len !== '' ? '(' . $len . ')' : ''),
1312 'mediumint' => 'MEDIUMINT' . ($len !== '' ? '(' . $len . ')' : ''),
1313 'int',
1314 'integer' => 'INT' . ($len !== '' ? '(' . $len . ')' : '(11)'),
1315 'bigint' => 'BIGINT' . ($len !== '' ? '(' . $len . ')' : ''),
1316 'decimal',
1317 'numeric' => 'DECIMAL' . ($len !== '' ? '(' . $len . ')' : '(10,2)'),
1318 'float' => 'FLOAT' . ($len !== '' ? '(' . $len . ')' : ''),
1319 'double',
1320 'real' => 'DOUBLE' . ($len !== '' ? '(' . $len . ')' : ''),
1321 'bit' => 'BIT' . ($len !== '' ? '(' . $len . ')' : '(1)'),
1322 'char' => 'CHAR(' . ($len !== '' ? $len : '1') . ')',
1323 'binary' => 'BINARY(' . ($len !== '' ? $len : '1') . ')',
1324 'varbinary' => 'VARBINARY(' . ($len !== '' ? $len : '255') . ')',
1325 'tinytext' => 'TINYTEXT',
1326 'text' => 'TEXT',
1327 'mediumtext' => 'MEDIUMTEXT',
1328 'longtext' => 'LONGTEXT',
1329 'tinyblob' => 'TINYBLOB',
1330 'blob' => 'BLOB',
1331 'mediumblob' => 'MEDIUMBLOB',
1332 'longblob' => 'LONGBLOB',
1333 'json' => 'JSON',
1334 'enum' => ($len !== '' ? 'ENUM(' . $len . ')' : 'VARCHAR(255)'),
1335 'set' => ($len !== '' ? 'SET(' . $len . ')' : 'VARCHAR(255)'),
1336 'date' => 'DATE',
1337 'time' => 'TIME',
1338 'year' => 'YEAR',
1339 'datetime' => 'DATETIME',
1340 'timestamp' => 'TIMESTAMP',
1341 default => 'VARCHAR(' . ($len !== '' ? $len : '255') . ')',
1342 };
1343
1344 case 'pgsql':
1345 return match ($type) {
1346 'int', 'integer', 'mediumint' => 'INTEGER',
1347 'bigint' => 'BIGINT',
1348 'smallint',
1349 'tinyint' => 'SMALLINT',
1350 'decimal',
1351 'numeric' => 'NUMERIC' . ($len !== '' ? '(' . $len . ')' : ''),
1352 'float',
1353 'real' => 'REAL',
1354 'double' => 'DOUBLE PRECISION',
1355 'date' => 'DATE',
1356 'datetime',
1357 'timestamp' => 'TIMESTAMP',
1358 'text',
1359 'mediumtext',
1360 'longtext' => 'TEXT',
1361 'char' => 'CHAR(' . ($len !== '' ? $len : '1') . ')',
1362 default => 'VARCHAR(' . ($len !== '' ? $len : '255') . ')',
1363 };
1364
1365 default:
1366 return match ($type) {
1367 'int', 'integer', 'mediumint' => 'INTEGER',
1368 'bigint' => 'BIGINT',
1369 'smallint',
1370 'tinyint' => 'SMALLINT',
1371 'decimal',
1372 'numeric' => 'DECIMAL' . ($len !== '' ? '(' . $len . ')' : ''),
1373 'float' => 'FLOAT',
1374 'double',
1375 'real' => 'DOUBLE',
1376 'date' => 'DATE',
1377 'datetime',
1378 'timestamp' => 'TIMESTAMP',
1379 'text',
1380 'mediumtext',
1381 'longtext' => 'TEXT',
1382 default => 'VARCHAR(' . ($len !== '' ? $len : '255') . ')',
1383 };
1384 }
1385 }
1386
1396 protected function build_default_sql(string $server, mixed $default, string $ddType): string
1397 {
1398 $default = (string)$default;
1399 $u = strtoupper($default);
1400
1401 if ($u === 'CURRENT_TIMESTAMP') {
1402 return 'DEFAULT CURRENT_TIMESTAMP';
1403 }
1404
1405 $numericTypes = [
1406 'int', 'integer', 'bigint', 'smallint', 'mediumint', 'tinyint', 'bit',
1407 'decimal', 'numeric', 'float', 'double', 'real',
1408 ];
1409
1410 if (in_array(strtolower($ddType), $numericTypes, true) && is_numeric($default)) {
1411 return 'DEFAULT ' . $default;
1412 }
1413
1414 return 'DEFAULT ' . $this->sql_quote($server, $default);
1415 }
1416
1425 protected function build_sql_column_from_dd(string $server, array $field): string
1426 {
1427 $dbType = $this->get_db_type($server);
1428 $name = $field['name'] ?? '';
1429 $type = strtolower((string)($field['type'] ?? 'varchar'));
1430 $length = (string)($field['length'] ?? '');
1431 $index = strtoupper((string)($field['index'] ?? ''));
1432 $default = $field['default'] ?? '';
1433
1434 $sqlType = $this->map_dd_type_to_sql_type($dbType, $type, $length);
1435 $col = $this->quote_ident($server, $name) . ' ' . $sqlType;
1436
1437 if ($index === 'PRI' && strtolower($name) === 'id') {
1438 switch ($dbType) {
1439 case 'sqlite':
1440 return $this->quote_ident($server, $name) . ' INTEGER PRIMARY KEY AUTOINCREMENT';
1441 case 'mysql':
1442 case 'cubrid':
1443 return $this->quote_ident($server, $name) . ' ' . $sqlType . ' AUTO_INCREMENT PRIMARY KEY';
1444 case 'pgsql':
1445 case 'firebird':
1446 case 'ibm':
1447 case 'odbc':
1448 return $this->quote_ident($server, $name) . ' INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY';
1449 case 'sqlsrv':
1450 case 'dblib':
1451 return $this->quote_ident($server, $name) . ' INTEGER IDENTITY(1,1) PRIMARY KEY';
1452 case 'oci':
1453 return $this->quote_ident($server, $name) . ' NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY';
1454 case 'informix':
1455 return $this->quote_ident($server, $name) . ' SERIAL PRIMARY KEY';
1456 default:
1457 break;
1458 }
1459 }
1460
1461 if ($default !== '' && $default !== null) {
1462 $col .= ' ' . $this->build_default_sql($server, $default, $type);
1463 }
1464
1465 return $col;
1466 }
1467
1480 protected function enforce_auto_increment_id(array $fields): array
1481 {
1482 $idField = array(
1483 'name' => 'id',
1484 'type' => 'int',
1485 'index' => 'PRI',
1486 'length' => '11',
1487 'default' => '',
1488 );
1489 $normalized = array();
1490
1491 foreach ($fields as $field) {
1492 $name = strtolower(trim((string)($field['name'] ?? '')));
1493
1494 if ($name === '') {
1495 continue;
1496 }
1497
1498 if ($name === 'id') {
1499 $idField = array_merge($field, $idField);
1500 continue;
1501 }
1502
1503 if (strtoupper((string)($field['index'] ?? '')) === 'PRI') {
1504 $field['index'] = '';
1505 }
1506
1507 $normalized[] = $field;
1508 }
1509
1510 array_unshift($normalized, $idField);
1511 return $normalized;
1512 }
1513
1518 protected function uses_inline_auto_increment_id(string $dbType): bool
1519 {
1520 return in_array($dbType, array(
1521 'sqlite', 'mysql', 'pgsql', 'sqlsrv', 'oci', 'firebird',
1522 'cubrid', 'dblib', 'ibm', 'informix', 'odbc',
1523 ), true);
1524 }
1525
1533 public function build_create_table_sql(string $dd): string
1534 {
1535 $server = $this->get_dd_server($dd);
1536 $table = $this->get_dd_table($dd);
1537 $fields = $this->enforce_auto_increment_id($this->get_dd_fields($dd));
1538 $dbType = $this->get_db_type($server);
1539
1540 $parts = [];
1541 $primaryFields = [];
1542
1543 foreach ($fields as $field) {
1544 $parts[] = $this->build_sql_column_from_dd($server, $field);
1545 if (strtoupper((string)($field['index'] ?? '')) === 'PRI') {
1546 $isInlinePrimary = strtolower((string)($field['name'] ?? '')) === 'id'
1547 && $this->uses_inline_auto_increment_id($dbType);
1548 if (!$isInlinePrimary) {
1549 $primaryFields[] = $field['name'];
1550 }
1551 }
1552 }
1553
1554 if ($primaryFields) {
1555 $quoted = [];
1556 foreach ($primaryFields as $fld) {
1557 $quoted[] = $this->quote_ident($server, $fld);
1558 }
1559 $parts[] = 'PRIMARY KEY (' . implode(',', $quoted) . ')';
1560 }
1561
1562 $sql = 'CREATE TABLE ' . $this->quote_ident($server, $table) . " (\n";
1563 $sql .= ' ' . implode(",\n ", $parts) . "\n";
1564 $sql .= ')';
1565
1566 return $sql;
1567 }
1568
1578 public function create_db_index(string $server, string $table, array $index): int
1579 {
1580 $name = (string)($index['name'] ?? '');
1581 $type = strtoupper((string)($index['type'] ?? 'INDEX'));
1582 $fields = array_filter(array_map('trim', explode(',', (string)($index['fields'] ?? ''))));
1583
1584 if (!$name || !$fields) {
1585 return 0;
1586 }
1587
1588 $quotedFields = [];
1589 foreach ($fields as $fld) {
1590 $quotedFields[] = $this->quote_ident($server, $fld);
1591 }
1592
1593 $sql = '';
1594 if ($type === 'UNIQUE') {
1595 $sql = 'CREATE UNIQUE INDEX ' . $this->quote_ident($server, $name)
1596 . ' ON ' . $this->quote_ident($server, $table)
1597 . ' (' . implode(',', $quotedFields) . ')';
1598 } elseif ($type !== 'PRIMARY') {
1599 $sql = 'CREATE INDEX ' . $this->quote_ident($server, $name)
1600 . ' ON ' . $this->quote_ident($server, $table)
1601 . ' (' . implode(',', $quotedFields) . ')';
1602 }
1603
1604 if (!$sql) {
1605 return ($type === 'PRIMARY') ? 1 : 0;
1606 }
1607
1608 return $this->exec_query($server, $sql);
1609 }
1610
1619 protected function ensure_sqlite_db_file_for_dd(string $dd): int
1620 {
1621 $server = $this->get_dd_server($dd);
1622
1623 if (!preg_match('/\.(db3|sqlite|sqlite3)$/i', $server)) {
1624 return 1;
1625 }
1626
1627 $dd_sys = $this->get_dd_cache_info($dd);
1628 $modul = $dd_sys['dd_modul'] ?? '';
1629 $name = $server;
1630
1631 if (strpos($server, '|') !== false) {
1632 $parts = explode('|', $server, 2);
1633 $modul = trim($parts[0]);
1634 $name = trim($parts[1]);
1635 }
1636
1637 if ($modul === '' || $modul === 'modul') {
1638 $modul = dbx()->get_system_var('dbx_activ_modul', 'dbx');
1639 }
1640
1641 if ($modul === '') {
1642 $modul = 'dbx';
1643 }
1644
1645 $dir = dbx_os_path_file(dbx_get_base_dir() . 'dbx/modules/' . $modul . '/db/');
1646 if (!is_dir($dir) && !@mkdir($dir, 0777, true)) {
1647 dbx()->sys_msg('error', 'db', $server, 'create sqlite dir failed', $dir);
1648 return 0;
1649 }
1650
1651 $file = dbx_os_path_file($dir . $name);
1652 if (!file_exists($file) && @file_put_contents($file, '') === false) {
1653 dbx()->sys_msg('error', 'db', $server, 'create sqlite file failed', $file);
1654 return 0;
1655 }
1656
1657 $hostRel = dbx_config_path_store($dir, true);
1658
1659 $_SESSION['dbx']['config']['dbx']['db'][$server] = [
1660 'type' => 'sqlite',
1661 'host' => $hostRel,
1662 'dbname' => basename($file),
1663 'user' => '',
1664 'pass' => '',
1665 'port' => ''
1666 ];
1667
1668 if (!isset($this->db[$server]) && !$this->dbConnect($server, 'sqlite', $hostRel, basename($file))) {
1669 return 0;
1670 }
1671
1672 return 1;
1673 }
1674
1682 public function create_db_tab(string $dd): int
1683 {
1684 $server = $this->get_dd_server($dd);
1685 $table = $this->get_dd_table($dd);
1686
1687 if (!$server || !$table) {
1688 return 0;
1689 }
1690
1691 if (!$this->ensure_sqlite_db_file_for_dd($dd)) {
1692 return 0;
1693 }
1694
1695 if ($this->get_table_exist($server, $table)) {
1696 return 1;
1697 }
1698
1699 $sql = $this->build_create_table_sql($dd);
1700 $ok = $this->exec_query($server, $sql);
1701
1702 if (!$ok) {
1703 return 0;
1704 }
1705
1706 $indexes = $this->get_dd_indexes($dd);
1707 foreach ($indexes as $index) {
1708 if (strtoupper((string)($index['type'] ?? 'INDEX')) === 'PRIMARY') {
1709 continue;
1710 }
1712 }
1713
1714 return $this->get_table_exist($server, $table) ? 1 : 0;
1715 }
1716
1726 public function add_db_field_from_dd(string $server, string $table, array $field): int
1727 {
1728 $sql = 'ALTER TABLE ' . $this->quote_ident($server, $table)
1729 . ' ADD COLUMN ' . $this->build_sql_column_from_dd($server, $field);
1730
1731 return $this->exec_query($server, $sql);
1732 }
1733
1742 public function drop_db_tab(string $server, string $table): int
1743 {
1744 if (!$this->get_table_exist($server, $table)) {
1745 return 1;
1746 }
1747
1748 $sql = 'DROP TABLE ' . $this->quote_ident($server, $table);
1749 return $this->exec_query($server, $sql);
1750 }
1751
1762 public function create_db_tab_from_fields(string $server, string $table, array $fields, array $indexes = []): int
1763 {
1764 if (!$server || !$table || !$fields) {
1765 return 0;
1766 }
1767
1769
1770 if ($this->get_table_exist($server, $table)) {
1771 return 1;
1772 }
1773
1774 $dbType = $this->get_db_type($server);
1775 $parts = [];
1776 $primaryFields = [];
1777
1778 foreach ($fields as $field) {
1779 if (empty($field['name'])) {
1780 continue;
1781 }
1782
1783 $parts[] = $this->build_sql_column_from_dd($server, $field);
1784
1785 if (strtoupper((string)($field['index'] ?? '')) === 'PRI') {
1786 $isInlinePrimary = strtolower((string)($field['name'] ?? '')) === 'id'
1787 && $this->uses_inline_auto_increment_id($dbType);
1788 if (!$isInlinePrimary) {
1789 $primaryFields[] = $field['name'];
1790 }
1791 }
1792 }
1793
1794 if (!$parts) {
1795 return 0;
1796 }
1797
1798 if ($primaryFields) {
1799 $quoted = [];
1800 foreach ($primaryFields as $fld) {
1801 $quoted[] = $this->quote_ident($server, $fld);
1802 }
1803 $parts[] = 'PRIMARY KEY (' . implode(',', $quoted) . ')';
1804 }
1805
1806 $sql = 'CREATE TABLE ' . $this->quote_ident($server, $table) . " (\n";
1807 $sql .= ' ' . implode(",\n ", $parts) . "\n";
1808 $sql .= ')';
1809
1810 $ok = $this->exec_query($server, $sql);
1811 if (!$ok) {
1812 return 0;
1813 }
1814
1815 foreach ($indexes as $index) {
1816 if (strtoupper((string)($index['type'] ?? 'INDEX')) === 'PRIMARY') {
1817 continue;
1818 }
1820 }
1821
1822 return $this->get_table_exist($server, $table) ? 1 : 0;
1823 }
1824
1833 public function empty_db_table(string $server, string $table): int
1834 {
1835 if (!$server || !$table || !$this->get_table_exist($server, $table)) {
1836 return 0;
1837 }
1838
1839 $dbType = $this->get_db_type($server);
1840 $qTable = $this->quote_ident($server, $table);
1841 $ok = 0;
1842
1843 switch ($dbType) {
1844 case 'mysql':
1845 $ok = $this->exec_query($server, 'TRUNCATE TABLE ' . $qTable);
1846 if ($ok) {
1847 $this->exec_query($server, 'ALTER TABLE ' . $qTable . ' AUTO_INCREMENT = 1');
1848 }
1849 break;
1850
1851 case 'sqlite':
1852 $ok = $this->exec_query($server, 'DELETE FROM ' . $qTable);
1853 if ($ok && $this->sqlite_sequence_exists($server)) {
1854 $this->exec_query($server, 'DELETE FROM sqlite_sequence WHERE name=' . $this->sql_quote($server, $table));
1855 }
1856 break;
1857
1858 case 'pgsql':
1859 $ok = $this->exec_query($server, 'TRUNCATE TABLE ' . $qTable . ' RESTART IDENTITY');
1860 break;
1861
1862 default:
1863 $ok = $this->exec_query($server, 'DELETE FROM ' . $qTable);
1864 break;
1865 }
1866
1867 return $ok ? 1 : 0;
1868 }
1869
1878 public function find_dd_for_db_table(string $server, string $table): array
1879 {
1880 $serverKey = strtolower(trim($server));
1881 $tableKey = strtolower(trim($table));
1882
1883 if ($serverKey === '' || $tableKey === '') {
1884 return [];
1885 }
1886
1887 $base = str_replace('\\', '/', dbx_get_base_dir()) . 'dbx/modules/*/dd/*.dd.php';
1888 foreach (glob($base) as $file) {
1889 $norm = str_replace('\\', '/', $file);
1890 if (!preg_match('#/dbx/modules/([^/]+)/dd/([^/]+)\.dd\.php$#', $norm, $match)) {
1891 continue;
1892 }
1893
1894 $modul = $match[1];
1895 $dd = $match[2];
1896 if ($dd === 'new' || $dd === '') {
1897 continue;
1898 }
1899
1900 $ddRef = $modul . '|' . $dd;
1901 $model = $this->get_dd_model($ddRef);
1902 if (!$model) {
1903 continue;
1904 }
1905
1906 $modelServer = strtolower((string)($model['table']['server'] ?? ''));
1907 $modelTable = strtolower((string)($model['table']['table'] ?? ''));
1908 $serverMatch = ($modelServer === $serverKey);
1909
1910 if (!$serverMatch && strpos($serverKey, '|') !== false) {
1911 [$serverModul, $serverName] = array_pad(explode('|', $serverKey, 2), 2, '');
1912 $serverMatch = ($serverModul === strtolower($modul))
1913 && ($modelServer === $serverName || $modelServer === 'modul');
1914 }
1915
1916 if ($serverMatch && $modelTable === $tableKey) {
1917 return [
1918 'modul' => $modul,
1919 'dd' => $dd,
1920 'dd_ref' => $ddRef,
1921 'model' => $model,
1922 ];
1923 }
1924 }
1925
1926 return [];
1927 }
1928
1940 public function get_preferred_table_schema(string $server, string $table): array
1941 {
1942 $match = $this->find_dd_for_db_table($server, $table);
1943 if ($match && !empty($match['model']['fields'])) {
1944 return [
1945 'source' => 'dd',
1946 'dd_ref' => $match['dd_ref'],
1947 'fields' => $match['model']['fields'] ?? [],
1948 'indexes' => $match['model']['indexes'] ?? [],
1949 ];
1950 }
1951
1952 return [
1953 'source' => 'db',
1954 'dd_ref' => '',
1955 'fields' => $this->get_db_fields($server, $table),
1956 'indexes' => $this->get_db_indexes($server, $table),
1957 ];
1958 }
1959
1971 public function transfer_table(
1972 string $sourceServer,
1973 string $sourceTable,
1974 string $targetServer,
1975 string $targetTable = '',
1976 string $mode = 'step',
1977 int $createTarget = 1,
1978 int $truncateTarget = 1
1979 ): array {
1980 $targetTable = $targetTable ?: $sourceTable;
1981 $mode = strtolower((string)$mode);
1982 if ($mode === '') {
1983 $mode = 'step';
1984 }
1985
1986 $key = $this->proc_key('transfer_table', [
1987 $sourceServer,
1988 $sourceTable,
1989 $targetServer,
1990 $targetTable,
1991 $createTarget,
1992 $truncateTarget,
1993 ]);
1994
1995 if ($mode === 'reset') {
1996 $this->clear_proc_state($key);
1997 return [
1998 'proc_key' => $key,
1999 'status' => 'reset',
2000 'message' => 'transfer state cleared',
2001 'percent' => 0,
2002 'step_percent' => 0,
2003 ];
2004 }
2005
2006 if ($mode === 'status') {
2007 $state = $this->get_proc_state($key);
2008 return $state ?: [
2009 'proc_key' => $key,
2010 'status' => 'new',
2011 'message' => 'no active state',
2012 'percent' => 0,
2013 'step_percent' => 0,
2014 ];
2015 }
2016
2017 if (in_array($mode, ['pause', 'resume', 'continue', 'cancel'], true)) {
2018 return $this->control_proc_state($key, $mode);
2019 }
2020
2021 if ($mode === 'restart') {
2022 $this->clear_proc_state($key);
2023 $mode = 'step';
2024 }
2025
2026 $state = $this->get_proc_state($key);
2027
2028 if (!$state) {
2029 if (!$sourceServer || !$sourceTable || !$targetServer || !$targetTable) {
2030 return $this->proc_error(['proc_key' => $key], 'source or target missing');
2031 }
2032
2033 if (!$this->get_table_exist($sourceServer, $sourceTable)) {
2034 return $this->proc_error(['proc_key' => $key], 'source table not found');
2035 }
2036
2037 $state = $this->init_proc_state('transfer_table', $key, [
2038 'source_server' => $sourceServer,
2039 'source_table' => $sourceTable,
2040 'target_server' => $targetServer,
2041 'target_table' => $targetTable,
2042 'create_target' => $createTarget ? 1 : 0,
2043 'truncate_target' => $truncateTarget ? 1 : 0,
2044 'backup_file' => $this->build_backup_file_name($sourceServer, $sourceTable, true),
2045 'phase' => 'prepare_target',
2046 'percent' => 0,
2047 'message' => 'transfer initialized',
2048 ]);
2049 }
2050
2051 if ($this->proc_is_waiting($state)) {
2052 return $this->proc_response($state);
2053 }
2054
2055 switch ($state['phase']) {
2056 case 'prepare_target':
2057 if (!$this->get_table_exist($state['target_server'], $state['target_table'])) {
2058 if (empty($state['create_target'])) {
2059 $state = $this->proc_error($state, 'target table not found');
2060 $this->set_proc_state($key, $state);
2061 return $state;
2062 }
2063
2064 $schema = $this->get_preferred_table_schema($state['source_server'], $state['source_table']);
2065 $fields = $schema['fields'] ?? [];
2066 $indexes = $schema['indexes'] ?? [];
2067 $ok = $this->create_db_tab_from_fields($state['target_server'], $state['target_table'], $fields, $indexes);
2068
2069 if (!$ok) {
2070 $state = $this->proc_error($state, 'create target table failed');
2071 $this->set_proc_state($key, $state);
2072 return $state;
2073 }
2074 }
2075
2076 if (!empty($state['truncate_target'])) {
2077 $this->empty_db_table($state['target_server'], $state['target_table']);
2078 }
2079
2080 $state['phase'] = 'backup_source';
2081 $state['percent'] = 10;
2082 $state['step_percent'] = 100;
2083 $state['message'] = 'target ready';
2084 $this->set_proc_state($key, $state);
2085 return $this->proc_response($state);
2086
2087 case 'backup_source':
2088 $backup = $this->backup($state['source_server'], $state['source_table'], $state['backup_file'], 1);
2089
2090 if (($backup['status'] ?? '') === 'error') {
2091 $state = $this->proc_error($state, 'backup source failed');
2092 $this->set_proc_state($key, $state);
2093 return $state;
2094 }
2095
2096 if (($backup['status'] ?? '') !== 'finished') {
2097 $state['percent'] = 10 + (int)floor((($backup['percent'] ?? 0) * 0.4));
2098 $state['step_percent'] = (int)($backup['percent'] ?? 0);
2099 $state['message'] = $backup['message'] ?? 'backup source';
2100 $this->set_proc_state($key, $state);
2101 return $this->proc_response($state);
2102 }
2103
2104 $state['phase'] = 'restore_target';
2105 $state['percent'] = 55;
2106 $state['step_percent'] = 100;
2107 $state['message'] = 'backup finished';
2108 $this->set_proc_state($key, $state);
2109 return $this->proc_response($state);
2110
2111 case 'restore_target':
2112 $restore = $this->restore($state['target_server'], $state['target_table'], $state['backup_file'], [], 1);
2113
2114 if (($restore['status'] ?? '') === 'error') {
2115 $state = $this->proc_error($state, 'restore target failed');
2116 $this->set_proc_state($key, $state);
2117 return $state;
2118 }
2119
2120 if (($restore['status'] ?? '') !== 'finished') {
2121 $state['percent'] = 55 + (int)floor((($restore['percent'] ?? 0) * 0.4));
2122 $state['step_percent'] = (int)($restore['percent'] ?? 0);
2123 $state['message'] = $restore['message'] ?? 'restore target';
2124 $this->set_proc_state($key, $state);
2125 return $this->proc_response($state);
2126 }
2127
2128 $state = $this->proc_finish($state, 'transfer finished');
2129 $this->set_proc_state($key, $state);
2130 return $state;
2131 }
2132
2133 $state = $this->proc_error($state, 'unknown transfer phase');
2134 $this->set_proc_state($key, $state);
2135 return $state;
2136 }
2137
2138
2139 /* =====================================================
2140 * STEP STATE
2141 * ===================================================== */
2142
2151 protected function proc_key(string $type, array $parts): string
2152 {
2153 return 'dbxdd_' . $type . '_' . md5(json_encode($parts));
2154 }
2155
2163 protected function get_proc_state(string $key): array
2164 {
2165 $state = dbx()->get_remember_var($key, [], $this->_remember_modul);
2166 return is_array($state) ? $state : [];
2167 }
2168
2177 protected function set_proc_state(string $key, array $state): void
2178 {
2179 dbx()->set_remember_var($key, $state, $this->_remember_modul);
2180 }
2181
2189 protected function clear_proc_state(string $key): void
2190 {
2191 dbx()->set_remember_var($key, [], $this->_remember_modul);
2192 }
2193
2203 protected function init_proc_state(string $type, string $key, array $state): array
2204 {
2205 $state['proc_type'] = $type;
2206 $state['proc_key'] = $key;
2207 $state['status'] = $state['status'] ?? 'running';
2208 $state['message'] = $state['message'] ?? '';
2209 $state['percent'] = $state['percent'] ?? 0;
2210 $state['step_percent'] = $state['step_percent'] ?? 0;
2211 $state['chunk_size'] = $state['chunk_size'] ?? $this->_chunk_size;
2212 $state['step_maxsec'] = $state['step_maxsec'] ?? $this->_max_step_runtime;
2213 $state['started_at'] = $state['started_at'] ?? date('Y-m-d H:i:s');
2214 $state['updated_at'] = date('Y-m-d H:i:s');
2215 $this->set_proc_state($key, $state);
2216 return $state;
2217 }
2218
2226 protected function proc_is_waiting(array $state): bool
2227 {
2228 return in_array(($state['status'] ?? ''), ['finished', 'error', 'paused', 'canceled'], true);
2229 }
2230
2239 protected function control_proc_state(string $key, string $cmd): array
2240 {
2241 $cmd = strtolower(trim($cmd));
2242
2243 if ($cmd === 'restart') {
2244 $this->clear_proc_state($key);
2245 return [
2246 'proc_key' => $key,
2247 'status' => 'reset',
2248 'message' => 'process restarted',
2249 'percent' => 0,
2250 'step_percent' => 0,
2251 'updated_at' => date('Y-m-d H:i:s'),
2252 ];
2253 }
2254
2255 $state = $this->get_proc_state($key);
2256 if (!$state) {
2257 $state = [
2258 'proc_key' => $key,
2259 'status' => 'new',
2260 'message' => 'no active state',
2261 'percent' => 0,
2262 'step_percent' => 0,
2263 ];
2264 }
2265
2266 $status = $state['status'] ?? 'new';
2267
2268 if ($cmd === 'pause') {
2269 if (!in_array($status, ['finished', 'error', 'canceled'], true)) {
2270 $state['status'] = 'paused';
2271 $state['paused_at'] = date('Y-m-d H:i:s');
2272 $state['message'] = 'process paused';
2273 }
2274 } elseif ($cmd === 'resume' || $cmd === 'continue') {
2275 if (in_array($status, ['paused', 'canceled', 'new'], true)) {
2276 $state['status'] = 'running';
2277 $state['resumed_at'] = date('Y-m-d H:i:s');
2278 $state['message'] = ($cmd === 'continue') ? 'process continued' : 'process resumed';
2279 }
2280 } elseif ($cmd === 'cancel') {
2281 if (!in_array($status, ['finished', 'error'], true)) {
2282 $state['status'] = 'canceled';
2283 $state['canceled_at'] = date('Y-m-d H:i:s');
2284 $state['message'] = 'process canceled';
2285 }
2286 }
2287
2288 $state = $this->proc_response($state);
2289 $this->set_proc_state($key, $state);
2290 return $state;
2291 }
2292
2300 protected function proc_response(array $state): array
2301 {
2302 $state['updated_at'] = date('Y-m-d H:i:s');
2303 return $state;
2304 }
2305
2314 protected function proc_error(array $state, string $message): array
2315 {
2316 $state['status'] = 'error';
2317 $state['message'] = $message;
2318 $state['percent'] = $state['percent'] ?? 0;
2319 $state['step_percent'] = $state['step_percent'] ?? 0;
2320 return $this->proc_response($state);
2321 }
2322
2331 protected function proc_finish(array $state, string $message = 'finished'): array
2332 {
2333 $state['status'] = 'finished';
2334 $state['message'] = $message;
2335 $state['percent'] = 100;
2336 $state['step_percent'] = 100;
2337 $state['finished_at'] = date('Y-m-d H:i:s');
2338 return $this->proc_response($state);
2339 }
2340
2346 protected function step_start_time(): float
2347 {
2348 return microtime(true);
2349 }
2350
2359 protected function step_time_left(float $started_at, float $max_seconds): bool
2360 {
2361 return (microtime(true) - $started_at) < $max_seconds;
2362 }
2363
2364
2365 /* =====================================================
2366 * SCHEMA MAPPING
2367 * ===================================================== */
2368
2376 protected function normalize_schema_field_key(string $name): string
2377 {
2378 return strtolower(preg_replace('/[^a-z0-9]+/i', '', trim($name)));
2379 }
2380
2388 protected function schema_mapping_file_part(string $value): string
2389 {
2390 $value = preg_replace('/[^A-Za-z0-9_.-]+/', '_', trim($value));
2391 $value = trim((string)$value, '._-');
2392
2393 return $value !== '' ? $value : 'default';
2394 }
2395
2401 protected function schema_mapping_dir(): string
2402 {
2403 $dir = dbx_os_path_file(dbx_get_file_dir() . 'db/schema-mapping/');
2404 if (!is_dir($dir)) {
2405 @mkdir($dir, 0777, true);
2406 }
2407
2408 return $dir;
2409 }
2410
2419 public function schema_mapping_path(string $kind, array $context): string
2420 {
2421 $kind = strtolower(trim($kind));
2422
2423 $parts = [
2424 $kind ?: 'schema',
2425 $context['modul'] ?? '',
2426 $context['dd'] ?? '',
2427 $context['source_server'] ?? ($context['server'] ?? ''),
2428 $context['source_table'] ?? ($context['table'] ?? ''),
2429 $context['target_server'] ?? '',
2430 $context['target_table'] ?? '',
2431 ];
2432
2433 $parts = array_map(fn($v) => $this->schema_mapping_file_part((string)$v), $parts);
2434
2435 return $this->schema_mapping_dir() . implode('__', $parts) . '.json';
2436 }
2437
2445 protected function schema_fields_by_name(array $fields): array
2446 {
2447 $index = [];
2448
2449 foreach ($fields as $field) {
2450 $name = (string)($field['name'] ?? '');
2451 if ($name !== '') {
2452 $index[$name] = $field;
2453 }
2454 }
2455
2456 return $this->sort_schema_record($index, $this->dd_index_schema_keys());
2457 }
2458
2459 protected function sort_schema_record(array $record, array $keys): array
2460 {
2461 $sorted = [];
2462
2463 foreach ($keys as $key) {
2464 if (array_key_exists($key, $record)) {
2465 $sorted[$key] = $record[$key];
2466 unset($record[$key]);
2467 }
2468 }
2469
2470 foreach ($record as $key => $value) {
2471 $sorted[$key] = $value;
2472 }
2473
2474 return $sorted;
2475 }
2476
2486 public function normalize_schema_mapping(array $mapping, array $sourceFields, array $targetFields): array
2487 {
2488 $sourceByLower = [];
2489 foreach ($sourceFields as $field) {
2490 $name = (string)($field['name'] ?? '');
2491 if ($name !== '') {
2492 $sourceByLower[strtolower($name)] = $name;
2493 }
2494 }
2495
2496 $targetByLower = [];
2497 foreach ($targetFields as $field) {
2498 $name = (string)($field['name'] ?? '');
2499 if ($name !== '') {
2500 $targetByLower[strtolower($name)] = $name;
2501 }
2502 }
2503
2504 $clean = [];
2505 foreach ($mapping as $source => $target) {
2506 $sourceKey = strtolower(trim((string)$source));
2507 $targetKey = strtolower(trim((string)$target));
2508
2509 if ($sourceKey === '' || $targetKey === '') {
2510 continue;
2511 }
2512
2513 if (!isset($sourceByLower[$sourceKey]) || !isset($targetByLower[$targetKey])) {
2514 continue;
2515 }
2516
2517 $sourceName = $sourceByLower[$sourceKey];
2518 $targetName = $targetByLower[$targetKey];
2519
2520 foreach ($clean as $oldSource => $oldTarget) {
2521 if (strcasecmp($oldTarget, $targetName) === 0) {
2522 unset($clean[$oldSource]);
2523 }
2524 }
2525
2526 $clean[$sourceName] = $targetName;
2527 }
2528
2529 ksort($clean);
2530 return $clean;
2531 }
2532
2542 protected function auto_schema_mapping(array $sourceFields, array $targetFields, array $stored = []): array
2543 {
2544 $sourceExact = [];
2545 $sourceNorm = [];
2546
2547 foreach ($sourceFields as $field) {
2548 $name = (string)($field['name'] ?? '');
2549 if ($name === '') {
2550 continue;
2551 }
2552
2553 $sourceExact[strtolower($name)] = $name;
2554 $norm = $this->normalize_schema_field_key($name);
2555 if ($norm !== '' && !isset($sourceNorm[$norm])) {
2556 $sourceNorm[$norm] = $name;
2557 }
2558 }
2559
2560 $mapping = [];
2561 $usedSource = [];
2562
2563 foreach ($targetFields as $field) {
2564 $target = (string)($field['name'] ?? '');
2565 if ($target === '') {
2566 continue;
2567 }
2568
2569 $source = $sourceExact[strtolower($target)] ?? '';
2570 if ($source !== '') {
2571 $mapping[$source] = $target;
2572 $usedSource[strtolower($source)] = true;
2573 }
2574 }
2575
2576 foreach ($targetFields as $field) {
2577 $target = (string)($field['name'] ?? '');
2578 if ($target === '') {
2579 continue;
2580 }
2581
2582 $alreadyMapped = false;
2583 foreach ($mapping as $mappedTarget) {
2584 if (strcasecmp($mappedTarget, $target) === 0) {
2585 $alreadyMapped = true;
2586 break;
2587 }
2588 }
2589
2590 if ($alreadyMapped) {
2591 continue;
2592 }
2593
2594 $norm = $this->normalize_schema_field_key($target);
2595 $source = $sourceNorm[$norm] ?? '';
2596 if ($source !== '' && empty($usedSource[strtolower($source)])) {
2597 $mapping[$source] = $target;
2598 $usedSource[strtolower($source)] = true;
2599 }
2600 }
2601
2602 $stored = $this->normalize_schema_mapping($stored, $sourceFields, $targetFields);
2603 foreach ($stored as $source => $target) {
2604 foreach ($mapping as $oldSource => $oldTarget) {
2605 if (strcasecmp($oldSource, $source) === 0 || strcasecmp($oldTarget, $target) === 0) {
2606 unset($mapping[$oldSource]);
2607 }
2608 }
2609
2610 $mapping[$source] = $target;
2611 }
2612
2613 ksort($mapping);
2614 return $mapping;
2615 }
2616
2625 public function load_schema_mapping(string $kind, array $context): array
2626 {
2627 $file = $this->schema_mapping_path($kind, $context);
2628 if (!is_file($file)) {
2629 return [
2630 'kind' => strtolower(trim($kind)),
2631 'context' => $context,
2632 'mapping' => [],
2633 'file' => $file,
2634 'updated_at' => '',
2635 ];
2636 }
2637
2638 $data = json_decode((string)file_get_contents($file), true);
2639 if (!is_array($data)) {
2640 $data = [];
2641 }
2642
2643 $data['kind'] = $data['kind'] ?? strtolower(trim($kind));
2644 $data['context'] = is_array($data['context'] ?? null) ? $data['context'] : $context;
2645 $data['mapping'] = is_array($data['mapping'] ?? null) ? $data['mapping'] : [];
2646 $data['file'] = $file;
2647
2648 return $data;
2649 }
2650
2659 public function get_schema_mapping_values(string $kind, array $context): array
2660 {
2661 $data = $this->load_schema_mapping($kind, $context);
2662
2663 return is_array($data['mapping'] ?? null) ? $data['mapping'] : [];
2664 }
2665
2674 public function build_schema_mapping(string $kind, array $context): array
2675 {
2676 $kind = strtolower(trim($kind));
2677 if ($kind === '') {
2678 $kind = 'dd_to_db';
2679 }
2680
2681 $modul = (string)($context['modul'] ?? '');
2682 $dd = (string)($context['dd'] ?? '');
2683 $server = (string)($context['server'] ?? ($context['source_server'] ?? ''));
2684 $table = (string)($context['table'] ?? ($context['source_table'] ?? ''));
2685
2686 $sourceFields = [];
2687 $targetFields = [];
2688 $sourceLabel = '';
2689 $targetLabel = '';
2690 $ddRef = ($modul && $dd && strpos($dd, '|') === false) ? $modul . '|' . $dd : $dd;
2691
2692 if ($kind === 'db_to_dd') {
2693 if ($server && $table && $this->get_table_exist($server, $table)) {
2694 $sourceFields = $this->get_db_fields($server, $table);
2695 }
2696
2697 $oldModel = [];
2698 if ($ddRef && ($this->get_dd_exist($ddRef) || $this->get_dd_exist($dd))) {
2699 $oldModel = $this->get_dd_model($ddRef);
2700 if (!$oldModel) {
2701 $oldModel = $this->get_dd_model($dd);
2702 }
2703 }
2704
2705 $targetFields = $oldModel['fields'] ?? $sourceFields;
2706 $sourceLabel = $server . '|' . $table;
2707 $targetLabel = ($modul ? $modul . '|' : '') . $dd;
2708 } elseif ($kind === 'transfer') {
2709 $sourceServer = (string)($context['source_server'] ?? $server);
2710 $sourceTable = (string)($context['source_table'] ?? $table);
2711 $targetServer = (string)($context['target_server'] ?? '');
2712 $targetTable = (string)($context['target_table'] ?? '');
2713
2714 if ($sourceServer && $sourceTable && $this->get_table_exist($sourceServer, $sourceTable)) {
2715 $schema = $this->get_preferred_table_schema($sourceServer, $sourceTable);
2716 $sourceFields = $schema['fields'] ?? [];
2717 }
2718 if ($targetServer && $targetTable && $this->get_table_exist($targetServer, $targetTable)) {
2719 $targetFields = $this->get_db_fields($targetServer, $targetTable);
2720 } else {
2721 $targetFields = $sourceFields;
2722 }
2723
2724 $sourceLabel = $sourceServer . '|' . $sourceTable;
2725 $targetLabel = $targetServer . '|' . $targetTable;
2726 } else {
2727 $kind = 'dd_to_db';
2728 $model = $ddRef ? $this->get_dd_model($ddRef) : [];
2729 if (!$model && $dd) {
2730 $model = $this->get_dd_model($dd);
2731 }
2732
2733 $server = $server ?: (string)($model['table']['server'] ?? '');
2734 $table = $table ?: (string)($model['table']['table'] ?? '');
2735
2736 if ($server && $table && $this->get_table_exist($server, $table)) {
2737 $sourceFields = $this->get_db_fields($server, $table);
2738 }
2739
2740 $targetFields = $model['fields'] ?? [];
2741 $sourceLabel = $server . '|' . $table;
2742 $targetLabel = ($modul ? $modul . '|' : '') . $dd;
2743 }
2744
2745 $context['modul'] = $modul;
2746 $context['dd'] = $dd;
2747 $context['server'] = $server;
2748 $context['table'] = $table;
2749
2750 $storedData = $this->load_schema_mapping($kind, $context);
2751 $stored = is_array($storedData['mapping'] ?? null) ? $storedData['mapping'] : [];
2752 $mapping = $this->auto_schema_mapping($sourceFields, $targetFields, $stored);
2753
2754 $sourceByName = $this->schema_fields_by_name($sourceFields);
2755 $usedSources = [];
2756 $targetRows = [];
2757
2758 foreach ($targetFields as $target) {
2759 $targetName = (string)($target['name'] ?? '');
2760 if ($targetName === '') {
2761 continue;
2762 }
2763
2764 $sourceName = '';
2765 foreach ($mapping as $source => $mappedTarget) {
2766 if (strcasecmp($mappedTarget, $targetName) === 0) {
2767 $sourceName = $source;
2768 $usedSources[strtolower($source)] = true;
2769 break;
2770 }
2771 }
2772
2773 $source = $sourceName !== '' && isset($sourceByName[$sourceName]) ? $sourceByName[$sourceName] : [];
2774 $status = 'new';
2775 if ($sourceName !== '') {
2776 $status = (strcasecmp($sourceName, $targetName) === 0) ? 'exact' : 'mapped';
2777
2778 if ($source && $target && !$this->is_dd_field_compatible_with_db($target, $source, $server ? $this->get_db_type($server) : '')) {
2779 $status = 'type_conflict';
2780 }
2781 }
2782
2783 $targetRows[] = [
2784 'target' => $target,
2785 'source' => $source,
2786 'source_name' => $sourceName,
2787 'target_name' => $targetName,
2788 'status' => $status,
2789 ];
2790 }
2791
2792 $unmappedSources = [];
2793 foreach ($sourceFields as $source) {
2794 $name = (string)($source['name'] ?? '');
2795 if ($name !== '' && empty($usedSources[strtolower($name)])) {
2796 $unmappedSources[] = $source;
2797 }
2798 }
2799
2800 return [
2801 'kind' => $kind,
2802 'context' => $context,
2803 'source_label' => $sourceLabel,
2804 'target_label' => $targetLabel,
2805 'source_fields' => $sourceFields,
2806 'target_fields' => $targetFields,
2807 'target_rows' => $targetRows,
2808 'unmapped_sources' => $unmappedSources,
2809 'mapping' => $mapping,
2810 'stored_mapping' => $stored,
2811 'file' => $storedData['file'] ?? $this->schema_mapping_path($kind, $context),
2812 'updated_at' => $storedData['updated_at'] ?? '',
2813 ];
2814 }
2815
2825 public function save_schema_mapping(string $kind, array $context, array $mapping): int
2826 {
2827 $model = $this->build_schema_mapping($kind, $context);
2828 $clean = $this->normalize_schema_mapping($mapping, $model['source_fields'] ?? [], $model['target_fields'] ?? []);
2829
2830 $data = [
2831 'kind' => $model['kind'] ?? strtolower(trim($kind)),
2832 'context' => $model['context'] ?? $context,
2833 'source' => $model['source_label'] ?? '',
2834 'target' => $model['target_label'] ?? '',
2835 'mapping' => $clean,
2836 'updated_at' => date('Y-m-d H:i:s'),
2837 ];
2838
2839 $file = $model['file'] ?? $this->schema_mapping_path($kind, $context);
2840 $ok = file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
2841
2842 return $ok === false ? 0 : 1;
2843 }
2844
2845
2846 /* =====================================================
2847 * CREATE DD
2848 * ===================================================== */
2849
2864 public function create_dd(string $modul, string $dd, string $server = 'dbXsystem', string $table = ''): int
2865 {
2866 if (!$table) {
2867 $table = $dd;
2868 }
2869
2870 if (!$this->get_table_exist($server, $table)) {
2871 dbx()->sys_msg('error', 'dd', $dd, 'create_dd', 'table not found');
2872 return 0;
2873 }
2874
2875 $tableMeta = $this->normalize_table_record($dd, [
2876 'server' => $server,
2877 'table' => $table,
2878 ]);
2879
2882
2883 return $this->save_dd($modul, $dd, $tableMeta, $fields, $indexes);
2884 }
2885
2886
2887 /* =====================================================
2888 * BACKUP
2889 * ===================================================== */
2890
2900 protected function build_backup_file_name(string $server, string $table, bool $zip = false): string
2901 {
2902 $dir = dbx_os_path_file(dbx_get_file_dir() . 'db/dd-backup/');
2903 if (!is_dir($dir)) {
2904 @mkdir($dir, 0777, true);
2905 }
2906
2907 $name = $table . '_' . date('Ymd_His') . '.ddb';
2908 if ($zip) {
2909 $name .= '.zip';
2910 }
2911
2912 return $dir . $name;
2913 }
2914
2924 protected function finalize_backup_file(string $tmpFile, string $finalFile, bool $zip): int
2925 {
2926 if (!$zip) {
2927 @unlink($finalFile);
2928 return @rename($tmpFile, $finalFile) ? 1 : 0;
2929 }
2930
2931 if (!class_exists('ZipArchive')) {
2932 $raw = preg_replace('/\.zip$/i', '', $finalFile);
2933 @unlink($raw);
2934 return @rename($tmpFile, $raw) ? 1 : 0;
2935 }
2936
2937 $zipObj = new ZipArchive();
2938 if ($zipObj->open($finalFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
2939 return 0;
2940 }
2941
2942 $inner = basename(preg_replace('/\.zip$/i', '', $finalFile));
2943 $zipObj->addFile($tmpFile, $inner);
2944 $zipObj->close();
2945
2946 @unlink($tmpFile);
2947 return 1;
2948 }
2949
2969 public function backup($server, $table, $file = '', $zip = 0): array
2970 {
2971 $key = $this->proc_key('backup', [$server, $table, $file, $zip]);
2972 $state = $this->get_proc_state($key);
2973
2974 if ($state && $this->proc_is_waiting($state)) {
2975 return $this->proc_response($state);
2976 }
2977
2978 if (!$state) {
2979 if (!$server || !$table) {
2980 return $this->proc_error(['proc_key' => $key], 'server or table missing');
2981 }
2982
2983 if (!$this->get_table_exist($server, $table)) {
2984 return $this->proc_error(['proc_key' => $key], 'table not found');
2985 }
2986
2988 if (!$fields) {
2989 return $this->proc_error(['proc_key' => $key], 'no fields found');
2990 }
2991
2992 $columns = [];
2993 $pk = '';
2994 foreach ($fields as $f) {
2995 $columns[] = $f['name'];
2996 if (strtoupper((string)($f['index'] ?? '')) === 'PRI' && !$pk) {
2997 $pk = $f['name'];
2998 }
2999 }
3000
3001 $total = $this->count($table, '', $server);
3002 if ($total < 0) {
3003 return $this->proc_error(['proc_key' => $key], 'count failed');
3004 }
3005
3006 if (!$file) {
3007 $file = $this->build_backup_file_name($server, $table, (bool)$zip);
3008 }
3009
3010 $tmpFile = $file . '.part';
3011 @unlink($tmpFile);
3012
3013 $meta = [
3014 'server' => $server,
3015 'table' => $table,
3016 'db_type' => $this->get_db_type($server),
3017 'backup_date' => date('Y-m-d H:i:s'),
3018 'row_count' => $total,
3019 'compact' => 1,
3020 'zip' => $zip ? 1 : 0,
3021 ];
3022
3023 file_put_contents($tmpFile, json_encode(['meta' => $meta], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n");
3024 file_put_contents($tmpFile, json_encode(['columns' => $columns], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n", FILE_APPEND);
3025
3026 $state = $this->init_proc_state('backup', $key, [
3027 'server' => $server,
3028 'table' => $table,
3029 'file' => $file,
3030 'tmp_file' => $tmpFile,
3031 'zip' => $zip ? 1 : 0,
3032 'columns' => $columns,
3033 'pk' => $pk,
3034 'offset' => 0,
3035 'done' => 0,
3036 'total' => $total,
3037 'message' => 'backup initialized',
3038 ]);
3039 }
3040
3041 $started = $this->step_start_time();
3042
3043 while ($this->step_time_left($started, (float)$state['step_maxsec'])) {
3044 if ((int)$state['done'] >= (int)$state['total']) {
3045 $ok = $this->finalize_backup_file($state['tmp_file'], $state['file'], !empty($state['zip']));
3046 if (!$ok) {
3047 $state = $this->proc_error($state, 'finalize backup failed');
3048 $this->set_proc_state($key, $state);
3049 return $state;
3050 }
3051
3052 $state = $this->proc_finish($state, 'backup finished');
3053 $this->set_proc_state($key, $state);
3054 return $state;
3055 }
3056
3057 $chunk = (int)$state['chunk_size'];
3058 $cols = [];
3059 foreach ($state['columns'] as $col) {
3060 $cols[] = $this->quote_ident($state['server'], $col);
3061 }
3062
3063 $order = '';
3064 if (!empty($state['pk'])) {
3065 $order = ' ORDER BY ' . $this->quote_ident($state['server'], $state['pk']);
3066 }
3067
3068 $dbType = $this->get_db_type($state['server']);
3069 if ($dbType === 'mysql') {
3070 $limit = ' LIMIT ' . (int)$state['offset'] . ', ' . $chunk;
3071 } else {
3072 $limit = ' LIMIT ' . $chunk . ' OFFSET ' . (int)$state['offset'];
3073 }
3074
3075 $sql = 'SELECT ' . implode(',', $cols);
3076 $sql .= ' FROM ' . $this->quote_ident($state['server'], $state['table']);
3077 $sql .= $order . $limit;
3078
3079 $rows = $this->rawQuery($state['server'], $sql);
3080 if (!is_array($rows)) {
3081 $state = $this->proc_error($state, 'backup select failed');
3082 $this->set_proc_state($key, $state);
3083 return $state;
3084 }
3085
3086 if (!$rows) {
3087 $state['done'] = $state['total'];
3088 $state['percent'] = 100;
3089 $state['step_percent'] = 100;
3090 continue;
3091 }
3092
3093 $records = [];
3094 foreach ($rows as $row) {
3095 $rec = [];
3096 foreach ($state['columns'] as $col) {
3097 $rec[] = $row[$col] ?? null;
3098 }
3099 $records[] = $rec;
3100 }
3101
3102 file_put_contents(
3103 $state['tmp_file'],
3104 json_encode(['records' => $records], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n",
3105 FILE_APPEND
3106 );
3107
3108 $count = count($rows);
3109 $state['offset'] += $count;
3110 $state['done'] += $count;
3111 $state['percent'] = ($state['total'] > 0) ? (int)floor(($state['done'] / $state['total']) * 100) : 100;
3112 $state['step_percent'] = $state['percent'];
3113 $state['message'] = 'backup rows ' . $state['done'] . ' / ' . $state['total'];
3114 }
3115
3116 $this->set_proc_state($key, $state);
3117 return $this->proc_response($state);
3118 }
3119
3120
3121 /* =====================================================
3122 * RESTORE
3123 * ===================================================== */
3124
3133 protected function prepare_restore_source_file(string $file, bool $zip): string
3134 {
3135 if (!$zip) {
3136 return $file;
3137 }
3138
3139 if (!class_exists('ZipArchive')) {
3140 $raw = preg_replace('/\.zip$/i', '', $file);
3141 return file_exists($raw) ? $raw : '';
3142 }
3143
3144 $tmpDir = dbx_os_path_file(dbx_get_file_dir() . 'temp/dd-restore/');
3145 if (!is_dir($tmpDir)) {
3146 @mkdir($tmpDir, 0777, true);
3147 }
3148
3149 $tmpFile = $tmpDir . md5($file) . '.ddb';
3150
3151 if (file_exists($tmpFile)) {
3152 return $tmpFile;
3153 }
3154
3155 $zipObj = new ZipArchive();
3156 if ($zipObj->open($file) !== true) {
3157 return '';
3158 }
3159
3160 if ($zipObj->numFiles < 1) {
3161 $zipObj->close();
3162 return '';
3163 }
3164
3165 $content = $zipObj->getFromIndex(0);
3166 $zipObj->close();
3167
3168 if ($content === false) {
3169 return '';
3170 }
3171
3172 file_put_contents($tmpFile, $content);
3173 return $tmpFile;
3174 }
3175
3198 public function restore($server, $table, $file, $mapping = [], $zip = 0): array
3199 {
3200 $key = $this->proc_key('restore', [$server, $table, $file, $mapping, $zip]);
3201 $state = $this->get_proc_state($key);
3202
3203 if ($state && $this->proc_is_waiting($state)) {
3204 return $this->proc_response($state);
3205 }
3206
3207 if (!$state) {
3208 if (!$server || !$table || !$file) {
3209 return $this->proc_error(['proc_key' => $key], 'server, table or file missing');
3210 }
3211
3212 if (!$this->get_table_exist($server, $table)) {
3213 return $this->proc_error(['proc_key' => $key], 'target table not found');
3214 }
3215
3216 $sourceFile = $this->prepare_restore_source_file($file, (bool)$zip);
3217 if (!$sourceFile || !file_exists($sourceFile)) {
3218 return $this->proc_error(['proc_key' => $key], 'restore source file not readable');
3219 }
3220
3221 $fh = fopen($sourceFile, 'rb');
3222 if (!$fh) {
3223 return $this->proc_error(['proc_key' => $key], 'restore open source failed');
3224 }
3225
3226 $line1 = fgets($fh);
3227 $line2 = fgets($fh);
3228
3229 $metaRec = json_decode((string)$line1, true);
3230 $colRec = json_decode((string)$line2, true);
3231
3232 if (!is_array($metaRec) || !isset($metaRec['meta']) || !is_array($colRec) || !isset($colRec['columns'])) {
3233 fclose($fh);
3234 return $this->proc_error(['proc_key' => $key], 'invalid backup format');
3235 }
3236
3237 $filePos = ftell($fh);
3238 fclose($fh);
3239
3240 $targetFields = $this->get_db_fields($server, $table);
3241 $targetLookup = [];
3242 foreach ($targetFields as $f) {
3243 $targetLookup[$f['name']] = true;
3244 }
3245
3246 if (!$this->empty_db_table($server, $table)) {
3247 return $this->proc_error(['proc_key' => $key], 'target table not found after empty');
3248 }
3249
3250 $state = $this->init_proc_state('restore', $key, [
3251 'server' => $server,
3252 'table' => $table,
3253 'file' => $file,
3254 'source_file' => $sourceFile,
3255 'zip' => $zip ? 1 : 0,
3256 'mapping' => is_array($mapping) ? $mapping : [],
3257 'source_columns' => $colRec['columns'],
3258 'target_lookup' => $targetLookup,
3259 'file_pos' => $filePos,
3260 'done' => 0,
3261 'total' => (int)($metaRec['meta']['row_count'] ?? 0),
3262 'message' => 'restore initialized',
3263 ]);
3264 }
3265
3266 if (!$this->connect_db_server($state['server'])) {
3267 $state = $this->proc_error($state, 'db connect failed');
3268 $this->set_proc_state($key, $state);
3269 return $state;
3270 }
3271
3272 $started = $this->step_start_time();
3273
3274 $fh = fopen($state['source_file'], 'rb');
3275 if (!$fh) {
3276 $state = $this->proc_error($state, 'restore open source failed');
3277 $this->set_proc_state($key, $state);
3278 return $state;
3279 }
3280
3281 fseek($fh, (int)$state['file_pos']);
3282
3283 while ($this->step_time_left($started, (float)$state['step_maxsec'])) {
3284 $line = fgets($fh);
3285
3286 if ($line === false) {
3287 fclose($fh);
3288
3289 if (!empty($state['zip']) && isset($state['source_file']) && $state['source_file'] !== $state['file']) {
3290 @unlink($state['source_file']);
3291 }
3292
3293 $state = $this->proc_finish($state, 'restore finished');
3294 $this->set_proc_state($key, $state);
3295 return $state;
3296 }
3297
3298 $rec = json_decode(trim($line), true);
3299 if (!is_array($rec) || !isset($rec['records']) || !is_array($rec['records'])) {
3300 continue;
3301 }
3302
3303 foreach ($rec['records'] as $rowValues) {
3304 $assoc = [];
3305 foreach ($state['source_columns'] as $i => $col) {
3306 $assoc[$col] = $rowValues[$i] ?? null;
3307 }
3308
3309 if (is_array($state['mapping'])) {
3310 foreach ($state['mapping'] as $old => $new) {
3311 if ((string)$old === (string)$new) {
3312 continue;
3313 }
3314
3315 if (array_key_exists($old, $assoc)) {
3316 $assoc[$new] = $assoc[$old];
3317 unset($assoc[$old]);
3318 }
3319 }
3320 }
3321
3322 $dbRec = [];
3323 foreach ($assoc as $fld => $val) {
3324 if (isset($state['target_lookup'][$fld])) {
3325 $dbRec[$fld] = $val;
3326 }
3327 }
3328
3329 if (isset($dbRec['id']) && ($dbRec['id'] === '' || $dbRec['id'] === null || $dbRec['id'] === 0 || $dbRec['id'] === '0')) {
3330 unset($dbRec['id']);
3331 }
3332
3333 if (!$dbRec) {
3334 continue;
3335 }
3336
3337 $fields = array_keys($dbRec);
3338 $placeholders = array_fill(0, count($fields), '?');
3339
3340 $sql = 'INSERT INTO ' . $this->quote_ident($state['server'], $state['table'])
3341 . ' (' . implode(',', array_map(fn($f) => $this->quote_ident($state['server'], $f), $fields)) . ')'
3342 . ' VALUES (' . implode(',', $placeholders) . ')';
3343
3344 $stmt = $this->db[$state['server']]->prepare($sql);
3345 $stmt->execute(array_values($dbRec));
3346
3347 $state['done']++;
3348 }
3349
3350 $state['file_pos'] = ftell($fh);
3351 $state['percent'] = ($state['total'] > 0) ? (int)floor(($state['done'] / $state['total']) * 100) : 0;
3352 $state['step_percent'] = $state['percent'];
3353 $state['message'] = 'restore rows ' . $state['done'] . ' / ' . $state['total'];
3354 }
3355
3356 fclose($fh);
3357 $this->set_proc_state($key, $state);
3358 return $this->proc_response($state);
3359 }
3360
3361
3362 /* =====================================================
3363 * DD -> DB SYNC
3364 * ===================================================== */
3365
3378 protected function is_semantic_type_match(string $ddType, string $dbType): bool
3379 {
3380 $ddGroup = $this->schema_type_group($ddType);
3381 $dbGroup = $this->schema_type_group($dbType);
3382
3383 if ($ddGroup === $dbGroup) {
3384 return true;
3385 }
3386
3387 if (in_array($ddGroup, ['string', 'text'], true) && in_array($dbGroup, ['string', 'text'], true)) {
3388 return true;
3389 }
3390
3391 if (in_array($ddGroup, ['integer', 'bool'], true) && in_array($dbGroup, ['integer', 'bool'], true)) {
3392 return true;
3393 }
3394
3395 return false;
3396 }
3397
3406 protected function schema_type_group(string $type, string $length = ''): string
3407 {
3408 $type = strtolower(trim($type));
3409 $length = trim($length);
3410
3411 if ($type === '') {
3412 return 'unknown';
3413 }
3414
3415 if (in_array($type, ['bool', 'boolean'], true)) {
3416 return 'bool';
3417 }
3418
3419 if ($type === 'bit') {
3420 return ($length === '' || $length === '1') ? 'bool' : 'integer';
3421 }
3422
3423 if ($type === 'tinyint' && $length === '1') {
3424 return 'bool';
3425 }
3426
3427 if (in_array($type, ['int', 'integer', 'smallint', 'mediumint', 'tinyint', 'bigint', 'serial', 'number'], true)) {
3428 return 'integer';
3429 }
3430
3431 if (in_array($type, ['decimal', 'numeric'], true)) {
3432 return 'decimal';
3433 }
3434
3435 if (in_array($type, ['float', 'double', 'real'], true)) {
3436 return 'float';
3437 }
3438
3439 if ($type === 'date' || $type === 'year') {
3440 return 'date';
3441 }
3442
3443 if ($type === 'time') {
3444 return 'time';
3445 }
3446
3447 if (in_array($type, ['datetime', 'timestamp'], true)) {
3448 return 'datetime';
3449 }
3450
3451 if (in_array($type, ['char', 'nchar', 'varchar', 'varchar2', 'nvarchar', 'string'], true)) {
3452 return 'string';
3453 }
3454
3455 if (in_array($type, ['text', 'tinytext', 'mediumtext', 'longtext', 'clob'], true)) {
3456 return 'text';
3457 }
3458
3459 if (in_array($type, ['binary', 'varbinary', 'tinyblob', 'blob', 'mediumblob', 'longblob'], true)) {
3460 return 'binary';
3461 }
3462
3463 if ($type === 'json') {
3464 return 'json';
3465 }
3466
3467 if (in_array($type, ['enum', 'set'], true)) {
3468 return 'enum';
3469 }
3470
3471 return 'unknown';
3472 }
3473
3486 public function is_dd_field_compatible_with_db(array $ddField, array $dbField, string $dbEngine = ''): bool
3487 {
3488 $dbEngine = strtolower(trim($dbEngine));
3489 $ddType = strtolower((string)($ddField['type'] ?? ''));
3490 $dbType = strtolower((string)($dbField['type'] ?? ''));
3491 $ddGroup = $this->schema_type_group($ddType, (string)($ddField['length'] ?? ''));
3492 $dbGroup = $this->schema_type_group($dbType, (string)($dbField['length'] ?? ''));
3493
3494 if ($ddType !== '' && $dbType !== '' && ($ddType === $dbType || $this->is_semantic_type_match($ddType, $dbType))) {
3495 return true;
3496 }
3497
3498 if ($dbEngine === 'sqlite') {
3499 if (in_array($ddGroup, ['string', 'text', 'date', 'time', 'datetime', 'json', 'enum'], true)
3500 && in_array($dbGroup, ['string', 'text'], true)) {
3501 return true;
3502 }
3503
3504 if (in_array($ddGroup, ['integer', 'bool'], true) && in_array($dbGroup, ['integer', 'bool'], true)) {
3505 return true;
3506 }
3507
3508 if (in_array($ddGroup, ['decimal', 'float'], true) && in_array($dbGroup, ['decimal', 'float', 'integer'], true)) {
3509 return true;
3510 }
3511 }
3512
3513 return false;
3514 }
3515
3525 protected function is_db_sync_field_match(string $dbEngine, array $ddField, array $dbField): bool
3526 {
3527 $dbEngine = strtolower(trim($dbEngine));
3528 $ddType = strtolower((string)($ddField['type'] ?? ''));
3529 $dbType = strtolower((string)($dbField['type'] ?? ''));
3530
3531 if ($ddType === '' || $dbType === '') {
3532 return false;
3533 }
3534
3535 if ($dbEngine === 'sqlite') {
3536 return $this->is_dd_field_compatible_with_db($ddField, $dbField, $dbEngine);
3537 }
3538
3539 if ($ddType === $dbType) {
3540 return true;
3541 }
3542
3543 $ddGroup = $this->schema_type_group($ddType, (string)($ddField['length'] ?? ''));
3544 $dbGroup = $this->schema_type_group($dbType, (string)($dbField['length'] ?? ''));
3545
3546 if (in_array($ddGroup, ['integer', 'bool'], true) && in_array($dbGroup, ['integer', 'bool'], true)) {
3547 return true;
3548 }
3549
3550 if ($ddGroup === 'decimal' && $dbGroup === 'decimal') {
3551 return true;
3552 }
3553
3554 if ($ddGroup === 'float' && in_array($dbGroup, ['float', 'decimal'], true)) {
3555 return true;
3556 }
3557
3558 return false;
3559 }
3560
3570 public function merge_dd_field_with_db_field(array $oldField, array $dbField, string $dbEngine = ''): array
3571 {
3572 $compatible = $this->is_dd_field_compatible_with_db($oldField, $dbField, $dbEngine);
3573 $merged = $dbField;
3574
3575 $preserve = $compatible
3576 ? ['type', 'index', 'length', 'default', 'label', 'rules', 'tooltip', 'errormsg', 'placeholder', 'convert', 'protect', 'group', 'mask', 'data', 'options', 'tpl', 'js']
3577 : ['label', 'tooltip', 'errormsg', 'placeholder', 'convert', 'protect', 'group', 'mask', 'data', 'js'];
3578
3579 foreach ($preserve as $key) {
3580 if (isset($oldField[$key]) && $oldField[$key] !== '' && $oldField[$key] !== null) {
3581 $merged[$key] = $oldField[$key];
3582 }
3583 }
3584
3585 $merged['name'] = $dbField['name'] ?? ($oldField['name'] ?? '');
3586
3587 if (empty($merged['label'])) {
3588 $merged['label'] = $this->infer_label_from_name((string)$merged['name']);
3589 }
3590
3591 if (empty($merged['rules']) || !$compatible) {
3592 $merged['rules'] = $this->infer_rules_from_field($merged);
3593 }
3594
3595 if (empty($merged['tpl']) || !$compatible) {
3596 $merged['tpl'] = $this->infer_tpl_from_field($merged);
3597 }
3598
3599 return $this->normalize_field_record($merged);
3600 }
3601
3614 protected function build_sync_plan_dd_to_db(string $dd): array
3615 {
3616 $model = $this->get_dd_model($dd);
3617
3618 $plan = [
3619 'ok' => 1,
3620 'dd' => $dd,
3621 'server' => $model['table']['server'] ?? '',
3622 'table' => $model['table']['table'] ?? '',
3623 'table_exists' => 0,
3624 'create_table' => 0,
3625 'add_fields' => [],
3626 'missing_in_dd' => [],
3627 'type_conflicts' => [],
3628 'add_indexes' => [],
3629 'rebuild_needed' => 0,
3630 'backup_file' => '',
3631 ];
3632
3633 if (!$model) {
3634 $plan['ok'] = 0;
3635 return $plan;
3636 }
3637
3638 $server = $plan['server'];
3639 $table = $plan['table'];
3640 $ddFields = $model['fields'] ?? [];
3641 $ddIndexes = $model['indexes'] ?? [];
3642
3643 if (!$server || !$table) {
3644 $plan['ok'] = 0;
3645 return $plan;
3646 }
3647
3648 $tableExists = $this->get_table_exist($server, $table) ? 1 : 0;
3649 $plan['table_exists'] = $tableExists;
3650
3651 if (!$tableExists) {
3652 $plan['create_table'] = 1;
3653 return $plan;
3654 }
3655
3656 $dbFields = $this->get_db_fields($server, $table);
3657 $dbIndexes = $this->get_db_indexes($server, $table);
3658
3659 $dbFieldsByName = [];
3660 foreach ($dbFields as $field) {
3661 $dbFieldsByName[strtolower($field['name'])] = $field;
3662 }
3663
3664 $ddFieldsByName = [];
3665 foreach ($ddFields as $field) {
3666 $ddFieldsByName[strtolower($field['name'])] = $field;
3667 }
3668
3669 $targetDbType = $this->get_db_type($server);
3670
3671 foreach ($ddFields as $field) {
3672 $name = strtolower((string)$field['name']);
3673 if (!isset($dbFieldsByName[$name])) {
3674 $plan['add_fields'][] = $field;
3675 continue;
3676 }
3677
3678 $dbf = $dbFieldsByName[$name];
3679
3680 $ddType = strtolower((string)($field['type'] ?? ''));
3681 $dbType = strtolower((string)($dbf['type'] ?? ''));
3682
3683 $ddLen = (string)($field['length'] ?? '');
3684 $dbLen = (string)($dbf['length'] ?? '');
3685
3686 $typeEqual = $this->is_db_sync_field_match($targetDbType, $field, $dbf);
3687 $lenEqual = ($ddLen === $dbLen) || ($ddLen === '' || $dbLen === '');
3688
3694 if ($targetDbType === 'sqlite') {
3695 if ($typeEqual) {
3696 $lenEqual = true;
3697 }
3698 }
3699
3700 if (!$typeEqual || !$lenEqual) {
3701 $plan['type_conflicts'][] = [
3702 'field' => $field['name'],
3703 'dd_type' => $ddType,
3704 'db_type' => $dbType,
3705 'dd_length' => $ddLen,
3706 'db_length' => $dbLen,
3707 ];
3708 }
3709 }
3710
3711 foreach ($dbFields as $field) {
3712 $name = strtolower((string)$field['name']);
3713 if (!isset($ddFieldsByName[$name])) {
3714 $plan['missing_in_dd'][] = $field;
3715 }
3716 }
3717
3718 $dbIndexNames = [];
3719 foreach ($dbIndexes as $idx) {
3720 $dbIndexNames[strtolower((string)$idx['name'])] = true;
3721 }
3722
3723 foreach ($ddIndexes as $idx) {
3724 $name = strtolower((string)($idx['name'] ?? ''));
3725 if (!$name) {
3726 continue;
3727 }
3728
3729 if (strtoupper((string)($idx['type'] ?? 'INDEX')) === 'PRIMARY') {
3730 continue;
3731 }
3732
3733 if (!isset($dbIndexNames[$name])) {
3734 $plan['add_indexes'][] = $idx;
3735 }
3736 }
3737
3738 $plan['rebuild_needed'] = (!empty($plan['type_conflicts']) || !empty($plan['missing_in_dd'])) ? 1 : 0;
3739
3740 return $plan;
3741 }
3742
3765 public function sync_dd_to_db($modul, $dd, $mode = 'step'): array
3766 {
3767 $mode = strtolower((string)$mode);
3768 if ($mode === '') {
3769 $mode = 'step';
3770 }
3771 if ($mode === 'step') {
3772 $mode = 'apply';
3773 }
3774
3775 $ddRef = ($modul && strpos((string)$dd, '|') === false) ? $modul . '|' . $dd : (string)$dd;
3776 $key = $this->proc_key('sync_dd_to_db', [$modul, $dd]);
3777
3778 if ($mode === 'reset') {
3779 $this->clear_proc_state($key);
3780 return [
3781 'proc_key' => $key,
3782 'status' => 'reset',
3783 'message' => 'sync state cleared',
3784 'percent' => 0,
3785 'step_percent' => 0,
3786 ];
3787 }
3788
3789 if ($mode === 'status') {
3790 $state = $this->get_proc_state($key);
3791 return $state ?: [
3792 'proc_key' => $key,
3793 'status' => 'new',
3794 'message' => 'no active state',
3795 'percent' => 0,
3796 'step_percent' => 0,
3797 ];
3798 }
3799
3800 if (in_array($mode, ['pause', 'resume', 'continue', 'cancel'], true)) {
3801 return $this->control_proc_state($key, $mode);
3802 }
3803
3804 if ($mode === 'restart') {
3805 $this->clear_proc_state($key);
3806 $mode = 'apply';
3807 }
3808
3809 $plan = $this->build_sync_plan_dd_to_db($ddRef);
3810
3811 if (in_array($mode, ['check', 'plan'], true)) {
3812 $plan['status'] = 'finished';
3813 $plan['message'] = 'plan ready';
3814 $plan['percent'] = 100;
3815 $plan['step_percent'] = 100;
3816 return $plan;
3817 }
3818
3819 $state = $this->get_proc_state($key);
3820
3821 if (!$state) {
3822 $state = $this->init_proc_state('sync_dd_to_db', $key, [
3823 'mode' => $mode,
3824 'dd' => $dd,
3825 'dd_ref' => $ddRef,
3826 'modul' => $modul,
3827 'server' => $plan['server'],
3828 'table' => $plan['table'],
3829 'phase' => 'prepare',
3830 'plan' => $plan,
3831 'field_pos' => 0,
3832 'index_pos' => 0,
3833 'percent' => 0,
3834 'message' => 'sync initialized',
3835 'mapping' => $this->get_schema_mapping_values('dd_to_db', [
3836 'modul' => $modul,
3837 'dd' => $dd,
3838 'server' => $plan['server'],
3839 'table' => $plan['table'],
3840 ]),
3841 'backup_started' => 0,
3842 'restore_started' => 0,
3843 ]);
3844 }
3845
3846 if ($this->proc_is_waiting($state)) {
3847 return $this->proc_response($state);
3848 }
3849
3850 $server = $state['server'];
3851 $table = $state['table'];
3852 $plan = $state['plan'];
3853
3854 switch ($state['phase']) {
3855 case 'prepare':
3856 if (!empty($plan['create_table'])) {
3857 $state['phase'] = 'create_table';
3858 $state['message'] = 'create table';
3859 $state['percent'] = 10;
3860 $state['step_percent'] = 0;
3861 $this->set_proc_state($key, $state);
3862 return $this->proc_response($state);
3863 }
3864
3865 if (!empty($plan['rebuild_needed'])) {
3866 if (!in_array($state['mode'], ['force', 'rebuild'], true)) {
3867 $state = $this->proc_error($state, 'rebuild needed; use mode force or rebuild');
3868 $this->set_proc_state($key, $state);
3869 return $state;
3870 }
3871
3872 $state['phase'] = 'backup_old';
3873 $state['message'] = 'backup old table';
3874 $state['percent'] = 10;
3875 $state['step_percent'] = 0;
3876 $this->set_proc_state($key, $state);
3877 return $this->proc_response($state);
3878 }
3879
3880 $state['phase'] = 'add_fields';
3881 $state['message'] = 'add missing fields';
3882 $state['percent'] = 20;
3883 $state['step_percent'] = 0;
3884 $this->set_proc_state($key, $state);
3885 return $this->proc_response($state);
3886
3887 case 'create_table':
3888 if ($this->create_db_tab($state['dd_ref'] ?? $ddRef)) {
3889 $state['phase'] = 'add_indexes';
3890 $state['message'] = 'create indexes';
3891 $state['percent'] = 70;
3892 $state['step_percent'] = 100;
3893 } else {
3894 $state = $this->proc_error($state, 'create table failed');
3895 }
3896 $this->set_proc_state($key, $state);
3897 return $this->proc_response($state);
3898
3899 case 'add_fields':
3900 $started = $this->step_start_time();
3901 $fields = $plan['add_fields'] ?? [];
3902
3903 while ($state['field_pos'] < count($fields) && $this->step_time_left($started, (float)$state['step_maxsec'])) {
3904 $field = $fields[$state['field_pos']];
3905 $ok = $this->add_db_field_from_dd($server, $table, $field);
3906
3907 if (!$ok) {
3908 $state = $this->proc_error($state, 'add field failed: ' . ($field['name'] ?? ''));
3909 $this->set_proc_state($key, $state);
3910 return $state;
3911 }
3912
3913 $state['field_pos']++;
3914 $max = max(1, count($fields));
3915 $state['percent'] = 20 + (int)floor(($state['field_pos'] / $max) * 40);
3916 $state['step_percent'] = (int)floor(($state['field_pos'] / $max) * 100);
3917 $state['message'] = 'add field ' . ($field['name'] ?? '');
3918 }
3919
3920 if ($state['field_pos'] >= count($fields)) {
3921 $state['phase'] = 'add_indexes';
3922 $state['message'] = 'add indexes';
3923 $state['percent'] = 70;
3924 $state['step_percent'] = 100;
3925 }
3926
3927 $this->set_proc_state($key, $state);
3928 return $this->proc_response($state);
3929
3930 case 'add_indexes':
3931 $started = $this->step_start_time();
3932 $indexes = $plan['add_indexes'] ?? [];
3933
3934 while ($state['index_pos'] < count($indexes) && $this->step_time_left($started, (float)$state['step_maxsec'])) {
3935 $idx = $indexes[$state['index_pos']];
3936 $ok = $this->create_db_index($server, $table, $idx);
3937
3938 if (!$ok) {
3939 $state = $this->proc_error($state, 'add index failed: ' . ($idx['name'] ?? ''));
3940 $this->set_proc_state($key, $state);
3941 return $state;
3942 }
3943
3944 $state['index_pos']++;
3945 $max = max(1, count($indexes));
3946 $state['percent'] = 70 + (int)floor(($state['index_pos'] / $max) * 20);
3947 $state['step_percent'] = (int)floor(($state['index_pos'] / $max) * 100);
3948 $state['message'] = 'add index ' . ($idx['name'] ?? '');
3949 }
3950
3951 if ($state['index_pos'] >= count($indexes)) {
3952 $state = $this->proc_finish($state, 'sync dd -> db finished');
3953 }
3954
3955 $this->set_proc_state($key, $state);
3956 return $this->proc_response($state);
3957
3958 case 'backup_old':
3959 $backupFile = $state['plan']['backup_file'] ?? '';
3960 if (!$backupFile) {
3961 $backupFile = $this->build_backup_file_name($server, $table, true);
3962 $state['plan']['backup_file'] = $backupFile;
3963 }
3964
3965 $bak = $this->backup($server, $table, $backupFile, 1);
3966 if (($bak['status'] ?? '') === 'error') {
3967 $state = $this->proc_error($state, 'backup before rebuild failed');
3968 $this->set_proc_state($key, $state);
3969 return $state;
3970 }
3971
3972 if (($bak['status'] ?? '') !== 'finished') {
3973 $state['percent'] = 10 + (int)floor((($bak['percent'] ?? 0) * 0.3));
3974 $state['step_percent'] = (int)($bak['percent'] ?? 0);
3975 $state['message'] = 'backup old table';
3976 $this->set_proc_state($key, $state);
3977 return $this->proc_response($state);
3978 }
3979
3980 $state['phase'] = 'rename_old';
3981 $state['percent'] = 45;
3982 $state['step_percent'] = 100;
3983 $state['message'] = 'rename old table';
3984 $this->set_proc_state($key, $state);
3985 return $this->proc_response($state);
3986
3987 case 'rename_old':
3988 $tmpOld = $table . '__dbxold_' . date('YmdHis');
3989 $sql = 'ALTER TABLE ' . $this->quote_ident($server, $table)
3990 . ' RENAME TO ' . $this->quote_ident($server, $tmpOld);
3991
3992 if (!$this->exec_query($server, $sql)) {
3993 $state = $this->proc_error($state, 'rename old table failed');
3994 $this->set_proc_state($key, $state);
3995 return $state;
3996 }
3997
3998 $state['old_table'] = $tmpOld;
3999 $state['phase'] = 'create_new';
4000 $state['percent'] = 55;
4001 $state['step_percent'] = 100;
4002 $state['message'] = 'create new table';
4003 $this->set_proc_state($key, $state);
4004 return $this->proc_response($state);
4005
4006 case 'create_new':
4007 if (!$this->create_db_tab($state['dd_ref'] ?? $ddRef)) {
4008 $state = $this->proc_error($state, 'create new table failed');
4009 $this->set_proc_state($key, $state);
4010 return $state;
4011 }
4012
4013 $state['phase'] = 'restore_new';
4014 $state['percent'] = 65;
4015 $state['step_percent'] = 100;
4016 $state['message'] = 'restore data into new table';
4017 $this->set_proc_state($key, $state);
4018 return $this->proc_response($state);
4019
4020 case 'restore_new':
4021 $mapping = is_array($state['mapping'] ?? null) ? $state['mapping'] : [];
4022 $restore = $this->restore($server, $table, $state['plan']['backup_file'], $mapping, 1);
4023
4024 if (($restore['status'] ?? '') === 'error') {
4025 $state = $this->proc_error($state, 'restore into new table failed');
4026 $this->set_proc_state($key, $state);
4027 return $state;
4028 }
4029
4030 if (($restore['status'] ?? '') !== 'finished') {
4031 $state['percent'] = 65 + (int)floor((($restore['percent'] ?? 0) * 0.25));
4032 $state['step_percent'] = (int)($restore['percent'] ?? 0);
4033 $state['message'] = 'restore data into new table';
4034 $this->set_proc_state($key, $state);
4035 return $this->proc_response($state);
4036 }
4037
4038 $state['phase'] = 'drop_old';
4039 $state['percent'] = 92;
4040 $state['step_percent'] = 100;
4041 $state['message'] = 'drop old table';
4042 $this->set_proc_state($key, $state);
4043 return $this->proc_response($state);
4044
4045 case 'drop_old':
4046 if (!empty($state['old_table'])) {
4047 $this->drop_db_tab($server, $state['old_table']);
4048 }
4049
4050 $state = $this->proc_finish($state, 'sync dd -> db rebuild finished');
4051 $this->set_proc_state($key, $state);
4052 return $state;
4053 }
4054
4055 $state = $this->proc_error($state, 'unknown sync phase');
4056 $this->set_proc_state($key, $state);
4057 return $state;
4058 }
4059
4060
4061 /* =====================================================
4062 * DB -> DD SYNC
4063 * ===================================================== */
4064
4114 public function sync_db_to_dd($modul, $dd, $mode = 'step', string $server = '', string $table = ''): array
4115 {
4116 $mode = strtolower((string)$mode);
4117 if ($mode === '') {
4118 $mode = 'step';
4119 }
4120 if ($mode === 'step' || $mode === 'apply') {
4121 $mode = 'merge';
4122 }
4123
4124 $server = trim($server);
4125 $table = trim($table);
4126 $key = $this->proc_key('sync_db_to_dd', [$modul, $dd, $server, $table]);
4127
4128 if ($mode === 'reset') {
4129 $this->clear_proc_state($key);
4130 return [
4131 'proc_key' => $key,
4132 'status' => 'reset',
4133 'message' => 'sync state cleared',
4134 'percent' => 0,
4135 'step_percent' => 0,
4136 ];
4137 }
4138
4139 if ($mode === 'status') {
4140 $state = $this->get_proc_state($key);
4141 return $state ?: [
4142 'proc_key' => $key,
4143 'status' => 'new',
4144 'message' => 'no active state',
4145 'percent' => 0,
4146 'step_percent' => 0,
4147 ];
4148 }
4149
4150 if (in_array($mode, ['pause', 'resume', 'continue', 'cancel'], true)) {
4151 return $this->control_proc_state($key, $mode);
4152 }
4153
4154 if ($mode === 'restart') {
4155 $this->clear_proc_state($key);
4156 $mode = 'merge';
4157 }
4158
4159 $state = $this->get_proc_state($key);
4160
4161 if (!$state) {
4162 $oldModel = [];
4163 $syncServer = $server ?: 'dbXsystem';
4164 $syncTable = $table ?: $dd;
4165
4166 if ($this->get_dd_exist($modul . '|' . $dd) || $this->get_dd_exist($dd)) {
4167 $oldModel = $this->get_dd_model($modul . '|' . $dd);
4168 if (!$oldModel) {
4169 $oldModel = $this->get_dd_model($dd);
4170 }
4171
4172 if (!$server) {
4173 $syncServer = $oldModel['table']['server'] ?? $syncServer;
4174 }
4175 if (!$table) {
4176 $syncTable = $oldModel['table']['table'] ?? $syncTable;
4177 }
4178 }
4179
4180 if (!$this->get_table_exist($syncServer, $syncTable)) {
4181 return $this->proc_error(['proc_key' => $key], 'table not found');
4182 }
4183
4184 $state = $this->init_proc_state('sync_db_to_dd', $key, [
4185 'mode' => $mode,
4186 'modul' => $modul,
4187 'dd' => $dd,
4188 'server' => $syncServer,
4189 'table' => $syncTable,
4190 'old_model' => $oldModel,
4191 'mapping' => $this->get_schema_mapping_values('db_to_dd', [
4192 'modul' => $modul,
4193 'dd' => $dd,
4194 'server' => $syncServer,
4195 'table' => $syncTable,
4196 ]),
4197 'phase' => 'read_schema',
4198 'percent' => 0,
4199 'message' => 'sync initialized',
4200 ]);
4201 }
4202
4203 if ($this->proc_is_waiting($state)) {
4204 return $this->proc_response($state);
4205 }
4206
4207 switch ($state['phase']) {
4208 case 'read_schema':
4209 $state['db_fields'] = $this->get_db_fields($state['server'], $state['table']);
4210 $state['db_indexes'] = $this->get_db_indexes($state['server'], $state['table']);
4211 $state['phase'] = 'merge_meta';
4212 $state['percent'] = 30;
4213 $state['step_percent'] = 100;
4214 $state['message'] = 'schema loaded';
4215 $this->set_proc_state($key, $state);
4216 return $this->proc_response($state);
4217
4218 case 'merge_meta':
4219 $oldModel = $state['old_model'] ?? [];
4220
4221 $newTable = $this->normalize_table_record($state['dd'], [
4222 'server' => $state['server'],
4223 'table' => $state['table'],
4224 ]);
4225
4226 if ($state['mode'] === 'merge' && $oldModel) {
4227 foreach ([
4228 'autosync','version','cache','trash','trace',
4229 'read','create','update','delete',
4230 'read_owner','create_owner','update_owner','delete_owner'
4231 ] as $k) {
4232 if (isset($oldModel['table'][$k])) {
4233 $newTable[$k] = $oldModel['table'][$k];
4234 }
4235 }
4236 }
4237
4238 $oldFieldsByName = [];
4239 foreach (($oldModel['fields'] ?? []) as $field) {
4240 $oldFieldsByName[strtolower((string)($field['name'] ?? ''))] = $field;
4241 }
4242
4243 $mapping = is_array($state['mapping'] ?? null) ? $state['mapping'] : [];
4244 $newFields = [];
4245 foreach (($state['db_fields'] ?? []) as $field) {
4246 $name = $field['name'];
4247 $metaName = $mapping[$name] ?? $name;
4248 $metaKey = strtolower((string)$metaName);
4249 if (!isset($oldFieldsByName[$metaKey])) {
4250 $metaName = $name;
4251 $metaKey = strtolower((string)$metaName);
4252 }
4253
4254 if ($state['mode'] === 'merge' && isset($oldFieldsByName[$metaKey])) {
4255 $newFields[] = $this->merge_dd_field_with_db_field(
4256 $oldFieldsByName[$metaKey],
4257 $field,
4258 $this->get_db_type($state['server'])
4259 );
4260 } else {
4261 $newFields[] = $this->normalize_field_record($field);
4262 }
4263 }
4264
4265 $oldIndexesByName = [];
4266 foreach (($oldModel['indexes'] ?? []) as $index) {
4267 $oldIndexesByName[$index['name']] = $index;
4268 }
4269
4270 $newIndexes = [];
4271 foreach (($state['db_indexes'] ?? []) as $index) {
4272 $name = $index['name'];
4273
4274 if ($state['mode'] === 'merge' && isset($oldIndexesByName[$name])) {
4275 $merged = $index;
4276 foreach ($oldIndexesByName[$name] as $k => $v) {
4277 if ($v !== '' && $v !== null) {
4278 $merged[$k] = $v;
4279 }
4280 }
4281 $newIndexes[] = $this->normalize_index_record($merged);
4282 } else {
4283 $newIndexes[] = $this->normalize_index_record($index);
4284 }
4285 }
4286
4287 $state['new_table'] = $newTable;
4288 $state['new_fields'] = $newFields;
4289 $state['new_indexes'] = $newIndexes;
4290 $state['phase'] = 'write_dd';
4291 $state['percent'] = 70;
4292 $state['step_percent'] = 100;
4293 $state['message'] = 'meta merged';
4294 $this->set_proc_state($key, $state);
4295 return $this->proc_response($state);
4296
4297 case 'write_dd':
4298 $ok = $this->write_dd(
4299 $state['modul'],
4300 $state['dd'],
4301 $state['new_table'] ?? [],
4302 $state['new_fields'] ?? [],
4303 $state['new_indexes'] ?? []
4304 );
4305
4306 if (!$ok) {
4307 $state = $this->proc_error($state, 'write dd failed');
4308 $this->set_proc_state($key, $state);
4309 return $state;
4310 }
4311
4312 $this->clear_dd_cache($state['modul'] . '|' . $state['dd']);
4313
4314 $state = $this->proc_finish($state, 'sync db -> dd finished');
4315 $this->set_proc_state($key, $state);
4316 return $state;
4317 }
4318
4319 $state = $this->proc_error($state, 'unknown sync phase');
4320 $this->set_proc_state($key, $state);
4321 return $state;
4322 }
4323
4351 public function dync_db_to_dd($modul, $dd, $mode = 'step', string $server = '', string $table = ''): array
4352 {
4353 return $this->sync_db_to_dd($modul, $dd, $mode, $server, $table);
4354 }
4355
4356
4357 /* =====================================================
4358 * LIST
4359 * ===================================================== */
4360
4373 public function get_dd_tables($path = ''): array
4374 {
4375 $records = [];
4376
4377 if (!$path) {
4378 $path = dbx_os_path_file(dbx_get_base_dir() . 'dbx/modules/dbx/dd/');
4379 }
4380
4381 if (!is_dir($path)) {
4382 return $records;
4383 }
4384
4385 $files = scandir($path);
4386 foreach ($files as $file) {
4387 if (!str_ends_with($file, '.dd.php')) {
4388 continue;
4389 }
4390
4391 $dd = str_replace('.dd.php', '', $file);
4392 if ($dd === 'new') {
4393 continue;
4394 }
4395
4396 $dd_sys = $this->load_dd($dd);
4397 if (($dd_sys['dd_status'] ?? 0) <= 0) {
4398 continue;
4399 }
4400
4401 $server = $this->get_dd_server($dd);
4402 $table = $this->get_dd_table($dd);
4403 $exist = $this->get_table_exist($server, $table) ? 1 : 0;
4404 $count = $exist ? $this->count($dd) : -1;
4405
4406 $records[] = [
4407 'datadic' => $dd,
4408 'server' => $server,
4409 'table' => $table,
4410 'exist' => $exist,
4411 'count' => $count,
4412 'sync' => $this->get_dd_autosync($dd),
4413 ];
4414 }
4415
4416 return $records;
4417 }
4418}
$table['server']
Definition .dd.php:6
$indexes[]
Definition .dd.php:167
$index['name']
Definition .dd.php:162
Zentrale Datenbank- und DD-Systemklasse von DBX.
get_dd_autosync($dd, $rec=0)
Gibt das Autosync-Flag einer DD zurück.
get_table_exist($dd, $dbtab='')
Prueft, ob eine Tabelle existiert.
rawQuery(string $server, string $query)
Führt eine rohe SQL-Abfrage auf dem angegebenen Server aus.
sqlite_sequence_exists(string $server)
Prueft ohne Schema-Warnung, ob SQLite seine AUTOINCREMENT-Tabelle hat.
count($dd, $where='', $server='')
Zählt Datensätze einer DD oder Tabelle.
empty($dd)
Leert eine Tabelle anhand ihrer DD.
dbConnect($server, $dbType, $dbHost, $dbName='', $dbUser='', $dbPass='', $dbPort='')
Stellt eine Verbindung zu einer Datenbank her und speichert sie in $this->db[$server].
get_dd_table($dd, $rec=0)
Gibt den Tabellennamen oder die komplette Table-Definition einer DD zurück.
exec_query($server, $sql)
Führt eine generische SQL-Exec-Anweisung aus.
escape($string, $server)
Escaped einen String für die sichere Verwendung in einer SQL-Abfrage.
get_dd_server(string $dd)
Ermittelt den Server einer Datenbeschreibung (DD).
get_db_type($server)
Ermittelt den Datenbanktyp für einen Server.
get_dd_fields($dd, $label=0)
Gibt die Felddefinitionen einer DD zurück.
connect_db_server(string $server)
Verbindet sich mit einem angegebenen Datenbankserver.
step_start_time()
Liefert einen Step-Startzeitpunkt.
init_proc_state(string $type, string $key, array $state)
Initialisiert einen technischen Prozessstatus.
get_dd_file_path(string $modul, string $dd)
Ermittelt den Dateipfad einer DD-Datei.
sync_dd_to_db($modul, $dd, $mode='step')
Synchronisiert ein DD technisch in Richtung DB.
get_schema_mapping_values(string $kind, array $context)
Gibt nur die gespeicherten Mapping-Werte zurueck.
get_step_runtime()
Gibt die maximale Laufzeit eines technischen Prozess-Schritts zurück.
save_schema_mapping(string $kind, array $context, array $mapping)
Speichert ein Schema-Mapping als wiederverwendbare Mapping-Datei.
get_preferred_table_schema(string $server, string $table)
Liefert die beste Strukturquelle fuer eine DB-Tabelle.
schema_mapping_file_part(string $value)
Baut einen sicheren Dateinamenbestandteil.
is_system_field(string $name)
Prüft, ob ein Feld ein DBX-Systemfeld ist.
map_db_type_to_dd_type(string $dbType, string $rawType, string $length='')
Mappt einen DB-Roh-Typ auf einen kanonischen DD-Typ.
build_backup_file_name(string $server, string $table, bool $zip=false)
Baut einen Backup-Dateinamen.
dd_template_value(mixed $value)
create_db_index(string $server, string $table, array $index)
Erzeugt einen DB-Index aus einer DD-Indexdefinition.
dd_table_schema_keys()
build_schema_mapping(string $kind, array $context)
Baut die Mapping-Ansicht fuer Admin-UI und Prozesse.
schema_fields_by_name(array $fields)
Baut einen Feldindex nach echtem Feldnamen.
build_create_table_sql(string $dd)
Baut das CREATE-TABLE-SQL aus einem DD.
get_chunk_size()
Gibt die Standard-Chunk-Größe für technische Prozesse zurück.
dd_index_schema_keys()
normalize_dd_field(array $field)
parse_sql_type(string $type)
Zerlegt einen SQL-Typ in Basis-Typ und Länge.
is_semantic_type_match(string $ddType, string $dbType)
Prüft, ob zwei Typen semantisch kompatibel sind.
get_db_fields($server, $tableName)
Liest die Feldstruktur einer DB-Tabelle und erzeugt daraus kanonische DD-Felddefinitionen.
prepare_restore_source_file(string $file, bool $zip)
Bereitet eine Restore-Quelldatei vor.
get_dd_template_file(string $name)
Ermittelt den absoluten Dateipfad eines DD-Templates.
finalize_backup_file(string $tmpFile, string $finalFile, bool $zip)
Finalisiert eine Backup-Datei optional als ZIP.
proc_key(string $type, array $parts)
Erzeugt einen technischen Prozessschlüssel.
sql_quote(string $server, mixed $value)
Quotet einen SQL-Wert für Roh-SQL.
add_db_field_from_dd(string $server, string $table, array $field)
Fügt ein Feld aus DD-Definition in eine bestehende DB-Tabelle ein.
normalize_schema_field_key(string $name)
Normalisiert einen Feldnamen fuer robuste Auto-Zuordnung.
dync_db_to_dd($modul, $dd, $mode='step', string $server='', string $table='')
Kompatibilitaetsalias fuer sync_db_to_dd().
build_sql_column_from_dd(string $server, array $field)
Baut eine SQL-Spaltendefinition aus einem DD-Feld.
get_dd_exist($dd)
Prüft, ob eine DD-Datei existiert.
uses_inline_auto_increment_id(string $dbType)
Liefert, ob der Treiber die Auto-ID zusammen mit PRIMARY KEY direkt an der Spalte definiert.
string $_remember_modul
normalize_schema_mapping(array $mapping, array $sourceFields, array $targetFields)
Normalisiert eine technische Source->Target-Zuordnung.
clear_dd_cache(string $dd)
Löscht den DD-Cache für eine DD.
quote_ident(string $server, string $name)
Quotet einen SQL-Identifier abhängig vom DB-Typ.
create_db_tab_from_fields(string $server, string $table, array $fields, array $indexes=[])
Erzeugt eine DB-Tabelle direkt aus Feld-/Index-Metadaten.
normalize_index_record(array $index)
Normalisiert einen DD-Indexsatz.
build_dd_field_from_db_meta(string $name, string $dbType, string $rawType, string $length, string $isNull, mixed $default, string $index='')
Baut aus gelesenen DB-Metadaten einen DD-Feldsatz.
restore($server, $table, $file, $mapping=[], $zip=0)
Stellt Daten aus einem kompakten Tabellenbackup wieder her.
dd_field_schema_keys()
ensure_sqlite_db_file_for_dd(string $dd)
Legt eine fehlende SQLite-Datei fuer ein DD an, damit die Tabelle anschliessend ueber die bestehende ...
sync_db_to_dd($modul, $dd, $mode='step', string $server='', string $table='')
Synchronisiert eine physische Datenbanktabelle technisch in Richtung DD.
step_time_left(float $started_at, float $max_seconds)
Prüft, ob noch Laufzeit für den aktuellen Prozessschritt übrig ist.
empty_db_table(string $server, string $table)
Leert eine konkrete DB-Tabelle ohne DD-Aufloesung.
merge_dd_field_with_db_field(array $oldField, array $dbField, string $dbEngine='')
Fuehrt ein vorhandenes DD-Feld mit einer DB-Felddefinition zusammen.
map_dd_type_to_sql_type(string $dbType, string $type, string $length='')
Mappt einen kanonischen DD-Typ auf einen konkreten SQL-Typ des Zielsystems.
proc_finish(array $state, string $message='finished')
Erzeugt einen Fertig-Status für einen Prozess.
__construct()
Initialisiert dbxDD.
get_dd_tables($path='')
Liefert DD-Übersichtsdaten aus einem DD-Verzeichnis.
schema_type_group(string $type, string $length='')
Gruppiert DD-/DB-Feldtypen fachlich.
backup($server, $table, $file='', $zip=0)
Erstellt ein tabellenbasiertes Datenbackup.
transfer_table(string $sourceServer, string $sourceTable, string $targetServer, string $targetTable='', string $mode='step', int $createTarget=1, int $truncateTarget=1)
Transferiert eine DB-Tabelle serveruebergreifend als Schrittprozess.
get_dd_cache_info(string $dd)
Ermittelt die aufgelöste Cache-Position einer DD.
find_dd_for_db_table(string $server, string $table)
Sucht ein DD, das auf eine konkrete DB-Tabelle zeigt.
load_dd(string $dd)
Nutzt direkt die bestehende dbxDB-Logik.
proc_response(array $state)
Aktualisiert einen Prozessstatus für Rückgabe.
proc_is_waiting(array $state)
Prueft, ob ein Prozess aktuell keine weitere Arbeit ausfuehren darf.
render_dd_template(string $template_name, array $vars=[])
Rendert ein DD-Template lokal als PHP-Quelltext.
create_dd(string $modul, string $dd, string $server='dbXsystem', string $table='')
Erzeugt eine DD aus einer vorhandenen DB-Tabelle.
infer_rules_from_field(array $field)
Leitet eine Standard-Regel aus einem Feld ab.
set_chunk_size(int $chunk_size)
Setzt die Standard-Chunk-Größe für technische Prozesse.
set_proc_state(string $key, array $state)
Speichert einen technischen Prozessstatus.
load_schema_mapping(string $kind, array $context)
Laedt ein gespeichertes Schema-Mapping.
clear_proc_state(string $key)
Löscht einen technischen Prozessstatus.
get_db_indexes(string $server, string $tableName)
Liest die Indexstruktur einer DB-Tabelle.
save_dd(string $modul, string $dd, array $table, array $fields, array $indexes=[])
Speichert eine DD-Datei und leert danach den DD-Cache.
write_dd(string $modul, string $dd, array $table, array $fields, array $indexes=[])
Schreibt eine DD-Datei anhand der DD-Templates.
proc_error(array $state, string $message)
Erzeugt einen Fehlerstatus für einen Prozess.
control_proc_state(string $key, string $cmd)
Steuert einen vorhandenen Prozessstatus.
build_default_sql(string $server, mixed $default, string $ddType)
Baut einen DEFAULT-SQL-Teil für einen DD-Default.
get_dd_model(string $dd)
Gibt das komplette DD-Modell zurück.
normalize_table_record(string $dd, array $table)
Normalisiert einen DD-Tabellensatz.
int $_chunk_size
enforce_auto_increment_id(array $fields)
Erzwingt die globale Tabellenidentitaet fuer jede neue Fachtabelle.
build_sync_plan_dd_to_db(string $dd)
Baut einen technischen Sync-Plan DD -> DB.
get_dd_indexes(string $dd)
Gibt die Indexdefinitionen einer DD zurück.
schema_mapping_path(string $kind, array $context)
Liefert den Pfad zur Mapping-Datei.
sort_schema_record(array $record, array $keys)
infer_tpl_from_field(array $field)
Leitet ein sinnvolles Form-Template aus einer Felddefinition ab.
normalize_field_record(array $field)
Normalisiert einen DD-Feldsatz.
normalize_default_value(mixed $value)
Normalisiert einen Default-Wert aus DB-Metadaten.
drop_db_tab(string $server, string $table)
Löscht eine Datenbanktabelle.
normalize_dd_index(array $index)
schema_mapping_dir()
Liefert das Verzeichnis fuer gespeicherte Schema-Mappings.
set_step_runtime(float $seconds)
Setzt die maximale Laufzeit eines technischen Prozess-Schritts.
auto_schema_mapping(array $sourceFields, array $targetFields, array $stored=[])
Baut ein Auto-Mapping und uebersteuert es mit gespeicherter Zuordnung.
create_db_tab(string $dd)
Erzeugt eine Datenbanktabelle aus einem DD.
get_proc_state(string $key)
Liest einen technischen Prozessstatus.
is_db_sync_field_match(string $dbEngine, array $ddField, array $dbField)
Prüft, ob ein DB-Feld physisch synchron zu einem DD-Feld ist.
float $_max_step_runtime
infer_label_from_name(string $name)
Leitet ein Standard-Label aus dem Feldnamen ab.
is_dd_field_compatible_with_db(array $ddField, array $dbField, string $dbEngine='')
Prüft, ob ein DD-Feld fachlich zu einem DB-Feld passt.
$fields[]
Definition config.dd.php:16
$field
Definition config.dd.php:4
dbx_os_path_file($path_file)
Definition index.php:164
dbx_config_path_store(string $path, bool $dirTrailingSlash=false)
Definition index.php:200
dbx_get_file_dir()
Definition index.php:265
dbx_get_base_dir($cut_Data=0)
Definition index.php:157
DBX schema administration.