dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
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 $table = $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]['table'] ?? [];
265 if (is_array($table)) {
266 $table['server'] = $this->get_dd_server($dd);
267 }
268
269 return [
270 'table' => $table,
271 'fields' => $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]['fields'] ?? [],
272 'indexes' => $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]['indexes'] ?? [],
273 ];
274 }
275
288 public function get_dd_exist($dd): bool
289 {
290 $activ_modul = dbx()->get_system_var('dbx_activ_modul', 'dbx');
291
292 if (strpos($dd, '|') !== false) {
293 $parts = explode('|', $dd, 2);
294 $dd_modul = trim($parts[0]);
295 $dd_name = trim($parts[1]);
296
297 if ($dd_modul === 'modul' || $dd_modul === '') {
298 $dd_modul = $activ_modul;
299 }
300 } else {
301 $dd_modul = $activ_modul;
302 $dd_name = $dd;
303 }
304
305 $dd_file1 = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/' . $dd_modul . '/dd/' . $dd_name . '.dd.php');
306 $dd_file2 = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/dbx/dd/' . $dd_name . '.dd.php');
307
308 return file_exists($dd_file1) || file_exists($dd_file2);
309 }
310
311
312 /* =====================================================
313 * DD TEMPLATE RENDER
314 * ===================================================== */
315
323 protected function get_dd_template_file(string $name): string
324 {
325 $file = dbx()->os_path(dbx()->get_base_dir() . 'dbx/tpl/dd/' . $name . '.php');
326 if (file_exists($file)) {
327 return $file;
328 }
329
330 return dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/dbx/tpl/php/' . $name . '.php');
331 }
332
333 protected function dd_template_value(mixed $value): string
334 {
335 return str_replace(
336 ['\\', "'"],
337 ['\\\\', "\\'"],
338 (string)$value
339 );
340 }
341
355 protected function render_dd_template(string $template_name, array $vars = []): string
356 {
357 $file = $this->get_dd_template_file($template_name);
358
359 if (!file_exists($file)) {
360 dbx()->sys_msg('error', 'dd', $template_name, 'missing template', $file);
361 return '';
362 }
363
364 $content = file_get_contents($file);
365 if ($content === false) {
366 return '';
367 }
368
369 $replace = [];
370 foreach ($vars as $key => $value) {
371 if (is_array($value)) {
372 foreach ($value as $k => $v) {
373 $replace['{' . $k . '}'] = $this->dd_template_value($v);
374 }
375 } else {
376 $replace['{' . $key . '}'] = (string)$value;
377 }
378 }
379
380 return strtr($content, $replace);
381 }
382
391 protected function get_dd_file_path(string $modul, string $dd): string
392 {
393 if (!$modul) {
394 $modul = dbx()->get_system_var('dbx_activ_modul', 'dbx');
395 }
396
397 return dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/' . $modul . '/dd/' . $dd . '.dd.php');
398 }
399
411 public function save_dd(string $modul, string $dd, array $table, array $fields, array $indexes = []): int
412 {
413 $ok = $this->write_dd($modul, $dd, $table, $fields, $indexes);
414
415 if ($ok) {
416 $this->clear_dd_cache($modul . '|' . $dd);
417 }
418
419 return $ok;
420 }
421
437 public function write_dd(string $modul, string $dd, array $table, array $fields, array $indexes = []): int
438 {
439 $table = $this->normalize_table_record($dd, $table);
440
441 $table_block = $this->render_dd_template('dd_table', ['table' => $table]);
442
443 $fields_block = '';
444 foreach ($fields as $field) {
445 $field = $this->normalize_field_record($field);
446 $fields_block .= $this->render_dd_template('dd_field', ['field' => $field]) . "\n";
447 }
448 $fields_block = trim($fields_block);
449
450 $indexes_block = '';
451 foreach ($indexes as $index) {
452 $index = $this->normalize_index_record($index);
453 $indexes_block .= $this->render_dd_template('dd_index', ['index' => $index]) . "\n";
454 }
455 $indexes_block = trim($indexes_block);
456
457 $content = $this->render_dd_template('dd_file', [
458 'table_block' => $table_block,
459 'fields_block' => $fields_block,
460 'indexes_block' => $indexes_block,
461 ]);
462
463 if (!$content) {
464 dbx()->sys_msg('error', 'dd', $dd, 'write_dd', 'empty dd content');
465 return 0;
466 }
467
468 $file = $this->get_dd_file_path($modul, $dd);
469 $dir = dirname($file);
470
471 if (!is_dir($dir)) {
472 @mkdir($dir, 0777, true);
473 }
474
475 $ok = file_put_contents($file, $content);
476 if ($ok === false) {
477 dbx()->sys_msg('error', 'dd', $dd, 'write_dd', $file);
478 return 0;
479 }
480
481 return 1;
482 }
483
484
485 /* =====================================================
486 * NORMALIZE / INFER
487 * ===================================================== */
488
489 public function dd_table_schema_keys(): array
490 {
491 return [
492 'server',
493 'table',
494 'datadic',
495 'primary',
496 'language',
497 'version',
498 'autosync',
499 'cache',
500 'trash',
501 'trace',
502 'update_sql',
503 'default_sort',
504 'form-dd-table',
505 'read',
506 'create',
507 'update',
508 'delete',
509 'read_owner',
510 'create_owner',
511 'update_owner',
512 'delete_owner',
513 ];
514 }
515
516 public function dd_field_schema_keys(): array
517 {
518 return [
519 'name',
520 'type',
521 'index',
522 'length',
523 'default',
524 'label',
525 'rules',
526 'tooltip',
527 'errormsg',
528 'placeholder',
529 'convert',
530 'protect',
531 'group',
532 'mask',
533 'data',
534 'options',
535 'tpl',
536 'js',
537 'prompt',
538 ];
539 }
540
541 public function dd_index_schema_keys(): array
542 {
543 return [
544 'name',
545 'type',
546 'fields',
547 'unique',
548 'comment',
549 ];
550 }
551
552 public function normalize_dd_field(array $field): array
553 {
554 return $this->normalize_field_record($field);
555 }
556
557 public function normalize_dd_index(array $index): array
558 {
559 return $this->normalize_index_record($index);
560 }
561
575 protected function normalize_table_record(string $dd, array $table): array
576 {
577 $defaults = [
578 'server' => $table['server'] ?? 'dbXsystem',
579 'table' => $table['table'] ?? $dd,
580 'datadic' => $dd,
581 'primary' => '',
582 'language' => '',
583 'version' => '1.0',
584 'autosync' => '0',
585 'cache' => '0',
586 'trash' => '0',
587 'trace' => '0',
588 'update_sql' => '',
589 'default_sort' => '',
590 'form-dd-table'=> '',
591
592 'read' => '*',
593 'create' => '*',
594 'update' => '*',
595 'delete' => '*',
596
597 'read_owner' => '*',
598 'create_owner' => '*',
599 'update_owner' => '*',
600 'delete_owner' => '*',
601 ];
602
603 foreach ($defaults as $k => $v) {
604 if (!isset($table[$k])) {
605 $table[$k] = $v;
606 }
607 }
608
609 $table['datadic'] = $dd;
610
611 return $this->sort_schema_record($table, $this->dd_table_schema_keys());
612 }
613
621 protected function normalize_field_record(array $field): array
622 {
623 $defaults = [
624 'name' => '',
625 'type' => 'varchar',
626 'index' => '',
627 'length' => '',
628 'default' => '',
629 'label' => '',
630 'rules' => '',
631 'tooltip' => '',
632 'errormsg' => '',
633 'placeholder' => '',
634 'convert' => '',
635 'protect' => '0',
636 'group' => '',
637 'mask' => '',
638 'data' => '',
639 'options' => '',
640 'tpl' => '',
641 'js' => '',
642 'prompt' => '',
643 ];
644
645 foreach ($defaults as $k => $v) {
646 if (!isset($field[$k])) {
647 $field[$k] = $v;
648 }
649 }
650
651 return $field;
652 }
653
661 protected function normalize_index_record(array $index): array
662 {
663 $defaults = [
664 'name' => '',
665 'type' => 'INDEX',
666 'fields' => '',
667 'unique' => '0',
668 'comment' => '',
669 ];
670
671 foreach ($defaults as $k => $v) {
672 if (!isset($index[$k])) {
673 $index[$k] = $v;
674 }
675 }
676
677 return $index;
678 }
679
687 protected function is_system_field(string $name): bool
688 {
689 return in_array(strtolower($name), ['id', 'create_date', 'create_uid', 'update_date', 'update_uid', 'owner'], true);
690 }
691
699 protected function infer_label_from_name(string $name): string
700 {
701 if ($this->is_system_field($name)) {
702 return str_replace('_', ' ', ucfirst($name));
703 }
704
705 return $name;
706 }
707
715 protected function infer_rules_from_field(array $field): string
716 {
717 $name = strtolower((string)($field['name'] ?? ''));
718 $type = strtolower((string)($field['type'] ?? 'varchar'));
719 $length = trim((string)($field['length'] ?? ''));
720
721 if ($name === 'id') {
722 return 'int';
723 }
724
725 if (in_array($type, ['bool', 'boolean', 'bit'], true)) {
726 return 'int';
727 }
728
729 if (in_array($type, ['int', 'integer', 'bigint', 'smallint', 'mediumint', 'tinyint'], true)) {
730 return 'int';
731 }
732
733 if (in_array($type, ['decimal', 'numeric'], true)) {
734 return 'decimal';
735 }
736
737 if (in_array($type, ['float', 'double', 'real'], true)) {
738 return 'decimal';
739 }
740
741 if (in_array($type, ['date', 'datetime', 'timestamp'], true)) {
742 return $type;
743 }
744
745 if ($type === 'time' || $type === 'year') {
746 return 'parameter';
747 }
748
749 if (($type === 'tinyint' || $type === 'int') && $length === '1') {
750 return 'int';
751 }
752
753 return 'parameter';
754 }
755
763 protected function infer_tpl_from_field(array $field): string
764 {
765 $type = strtolower((string)($field['type'] ?? 'varchar'));
766 $length = trim((string)($field['length'] ?? ''));
767
768 if (in_array($type, ['bool', 'boolean', 'bit'], true)
769 || (in_array($type, ['tinyint', 'int', 'integer'], true) && $length === '1')) {
770 return 'checkbox-label';
771 }
772
773 if (in_array($type, ['int', 'integer', 'bigint', 'smallint', 'mediumint', 'tinyint'], true)) {
774 return 'integer-label';
775 }
776
777 if ($type === 'date') {
778 return 'date-label';
779 }
780
781 if (in_array($type, ['datetime', 'timestamp'], true)) {
782 return 'datetime-label';
783 }
784
785 if (in_array($type, ['text', 'mediumtext', 'longtext'], true)) {
786 return 'textarea-label';
787 }
788
789 return 'text-label';
790 }
791
799 protected function normalize_default_value(mixed $value): string
800 {
801 if ($value === null) {
802 return '';
803 }
804
805 $value = trim((string)$value);
806 $value = trim($value, " \t\n\r\0\x0B'\"");
807
808 return $value;
809 }
810
822 protected function parse_sql_type(string $type): array
823 {
824 $type = trim($type);
825
826 if (preg_match('/^([a-zA-Z0-9_]+)\s*(?:\‍((.*?)\‍))?/i', $type, $m)) {
827 return [
828 'type' => strtolower($m[1]),
829 'length' => (string)($m[2] ?? ''),
830 ];
831 }
832
833 return [
834 'type' => strtolower($type),
835 'length' => '',
836 ];
837 }
838
859 protected function map_db_type_to_dd_type(string $dbType, string $rawType, string $length = ''): string
860 {
861 $dbType = strtolower(trim($dbType));
862 $rawType = strtolower(trim($rawType));
863 $length = trim($length);
864
865 if (in_array($rawType, ['bool', 'boolean'], true)) {
866 return 'bool';
867 }
868
869 if (in_array($rawType, ['tinyint', 'smallint', 'mediumint', 'bigint'], true)) {
870 return $rawType;
871 }
872
873 if (in_array($rawType, ['int', 'integer', 'serial', 'number'], true)) {
874 return 'int';
875 }
876
877 if (in_array($rawType, ['bit'], true)) {
878 return 'bit';
879 }
880
881 if (in_array($rawType, ['date', 'time', 'year'], true)) {
882 return $rawType;
883 }
884
885 if (in_array($rawType, ['datetime', 'timestamp'], true)) {
886 return 'datetime';
887 }
888
889 if (in_array($rawType, ['char', 'nchar'], true)) {
890 return 'char';
891 }
892
893 if (in_array($rawType, ['varchar', 'varchar2', 'nvarchar', 'string'], true)) {
894 return 'varchar';
895 }
896
897 if (in_array($rawType, ['tinytext', 'mediumtext', 'longtext'], true)) {
898 return $rawType;
899 }
900
901 if (in_array($rawType, ['text', 'clob'], true)) {
902 return 'text';
903 }
904
905 if (in_array($rawType, ['decimal', 'numeric'], true)) {
906 return 'decimal';
907 }
908
909 if (in_array($rawType, ['float', 'real'], true)) {
910 return 'float';
911 }
912
913 if (in_array($rawType, ['double'], true)) {
914 return 'double';
915 }
916
917 if (in_array($rawType, ['binary', 'varbinary', 'tinyblob', 'blob', 'mediumblob', 'longblob', 'json', 'enum', 'set'], true)) {
918 return $rawType;
919 }
920
921 if ($dbType === 'sqlite') {
922 return 'text';
923 }
924
925 return 'varchar';
926 }
927
928
929 /* =====================================================
930 * DB -> DD READER
931 * ===================================================== */
932
946 public function get_db_fields($server, $tableName): array
947 {
948 $db_fields = [];
949
950 if (!$this->get_table_exist($server, $tableName)) {
951 return $db_fields;
952 }
953
954 $dbType = $this->get_db_type($server);
955
956 switch ($dbType) {
957 case 'mysql':
958 $sql = "SHOW COLUMNS FROM " . $this->quote_ident($server, $tableName);
959 $rows = $this->rawQuery($server, $sql);
960
961 foreach ($rows as $row) {
962 $typeInfo = $this->parse_sql_type((string)($row['Type'] ?? ''));
963 $db_fields[] = $this->build_dd_field_from_db_meta(
964 (string)($row['Field'] ?? ''),
965 $dbType,
966 (string)($typeInfo['type'] ?? 'varchar'),
967 (string)($typeInfo['length'] ?? ''),
968 ((string)($row['Null'] ?? 'YES') === 'YES') ? '1' : '0',
969 $row['Default'] ?? '',
970 (string)($row['Key'] ?? '')
971 );
972 }
973 break;
974
975 case 'sqlite':
976 $sql = "PRAGMA table_info(" . $this->quote_ident($server, $tableName) . ")";
977 $rows = $this->rawQuery($server, $sql);
978
979 foreach ($rows as $row) {
980 $typeInfo = $this->parse_sql_type((string)($row['type'] ?? ''));
981 $index = ((int)($row['pk'] ?? 0) === 1) ? 'PRI' : '';
982
983 $db_fields[] = $this->build_dd_field_from_db_meta(
984 (string)($row['name'] ?? ''),
985 $dbType,
986 (string)($typeInfo['type'] ?? 'text'),
987 (string)($typeInfo['length'] ?? ''),
988 ((int)($row['notnull'] ?? 0) === 1) ? '0' : '1',
989 $row['dflt_value'] ?? '',
990 $index
991 );
992 }
993 break;
994
995 case 'pgsql':
996 $sql = "
997 SELECT column_name, data_type, character_maximum_length, is_nullable, column_default
998 FROM information_schema.columns
999 WHERE table_name = " . $this->sql_quote($server, $tableName) . "
1000 ORDER BY ordinal_position
1001 ";
1002 $rows = $this->rawQuery($server, $sql);
1003
1004 foreach ($rows as $row) {
1005 $db_fields[] = $this->build_dd_field_from_db_meta(
1006 (string)($row['column_name'] ?? ''),
1007 $dbType,
1008 (string)($row['data_type'] ?? 'text'),
1009 (string)($row['character_maximum_length'] ?? ''),
1010 ((string)($row['is_nullable'] ?? 'YES') === 'YES') ? '1' : '0',
1011 $row['column_default'] ?? '',
1012 ''
1013 );
1014 }
1015 break;
1016
1017 case 'sqlsrv':
1018 $sql = "
1019 SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE, COLUMN_DEFAULT
1020 FROM INFORMATION_SCHEMA.COLUMNS
1021 WHERE TABLE_NAME = " . $this->sql_quote($server, $tableName) . "
1022 ORDER BY ORDINAL_POSITION
1023 ";
1024 $rows = $this->rawQuery($server, $sql);
1025
1026 foreach ($rows as $row) {
1027 $db_fields[] = $this->build_dd_field_from_db_meta(
1028 (string)($row['COLUMN_NAME'] ?? ''),
1029 $dbType,
1030 (string)($row['DATA_TYPE'] ?? 'varchar'),
1031 (string)($row['CHARACTER_MAXIMUM_LENGTH'] ?? ''),
1032 ((string)($row['IS_NULLABLE'] ?? 'YES') === 'YES') ? '1' : '0',
1033 $row['COLUMN_DEFAULT'] ?? '',
1034 ''
1035 );
1036 }
1037 break;
1038
1039 case 'oci':
1040 $sql = "
1041 SELECT column_name, data_type, data_length, nullable, data_default
1042 FROM user_tab_columns
1043 WHERE table_name = UPPER(" . $this->sql_quote($server, $tableName) . ")
1044 ORDER BY column_id
1045 ";
1046 $rows = $this->rawQuery($server, $sql);
1047
1048 foreach ($rows as $row) {
1049 $db_fields[] = $this->build_dd_field_from_db_meta(
1050 (string)($row['COLUMN_NAME'] ?? ''),
1051 $dbType,
1052 (string)($row['DATA_TYPE'] ?? 'varchar2'),
1053 (string)($row['DATA_LENGTH'] ?? ''),
1054 ((string)($row['NULLABLE'] ?? 'Y') === 'Y') ? '1' : '0',
1055 $row['DATA_DEFAULT'] ?? '',
1056 ''
1057 );
1058 }
1059 break;
1060 }
1061
1062 return $db_fields;
1063 }
1064
1073 public function get_db_indexes(string $server, string $tableName): array
1074 {
1075 $indexes = [];
1076
1077 if (!$this->get_table_exist($server, $tableName)) {
1078 return $indexes;
1079 }
1080
1081 $dbType = $this->get_db_type($server);
1082
1083 switch ($dbType) {
1084 case 'mysql':
1085 $sql = "SHOW INDEX FROM " . $this->quote_ident($server, $tableName);
1086 $rows = $this->rawQuery($server, $sql);
1087
1088 $grouped = [];
1089 foreach ($rows as $row) {
1090 $keyName = (string)($row['Key_name'] ?? '');
1091 $colName = (string)($row['Column_name'] ?? '');
1092 $nonUnique = (int)($row['Non_unique'] ?? 1);
1093
1094 if (!isset($grouped[$keyName])) {
1095 $grouped[$keyName] = [
1096 'name' => $keyName,
1097 'type' => ($keyName === 'PRIMARY') ? 'PRIMARY' : (($nonUnique === 0) ? 'UNIQUE' : 'INDEX'),
1098 'fields' => [],
1099 'unique' => ($keyName === 'PRIMARY' || $nonUnique === 0) ? '1' : '0',
1100 'comment' => '',
1101 ];
1102 }
1103
1104 $grouped[$keyName]['fields'][] = $colName;
1105 }
1106
1107 foreach ($grouped as $idx) {
1108 $idx['fields'] = implode(',', $idx['fields']);
1109 $indexes[] = $idx;
1110 }
1111 break;
1112
1113 case 'sqlite':
1114 $sql = "PRAGMA index_list(" . $this->quote_ident($server, $tableName) . ")";
1115 $rows = $this->rawQuery($server, $sql);
1116
1117 foreach ($rows as $row) {
1118 $name = (string)($row['name'] ?? '');
1119 $unique = ((int)($row['unique'] ?? 0) === 1) ? '1' : '0';
1120 $origin = (string)($row['origin'] ?? '');
1121
1122 $sql2 = "PRAGMA index_info(" . $this->quote_ident($server, $name) . ")";
1123 $cols = $this->rawQuery($server, $sql2);
1124
1125 $fields = [];
1126 foreach ($cols as $col) {
1127 $fields[] = (string)($col['name'] ?? '');
1128 }
1129
1130 $type = 'INDEX';
1131 if ($origin === 'pk') {
1132 $type = 'PRIMARY';
1133 $unique = '1';
1134 } elseif ($unique === '1') {
1135 $type = 'UNIQUE';
1136 }
1137
1138 $indexes[] = [
1139 'name' => $name,
1140 'type' => $type,
1141 'fields' => implode(',', $fields),
1142 'unique' => $unique,
1143 'comment' => '',
1144 ];
1145 }
1146 break;
1147
1148 case 'pgsql':
1149 $sql = "
1150 SELECT indexname, indexdef
1151 FROM pg_indexes
1152 WHERE tablename = " . $this->sql_quote($server, $tableName);
1153 $rows = $this->rawQuery($server, $sql);
1154
1155 foreach ($rows as $row) {
1156 $name = (string)($row['indexname'] ?? '');
1157 $def = (string)($row['indexdef'] ?? '');
1158
1159 preg_match('/\‍((.*?)\‍)/', $def, $m);
1160 $fields = $m[1] ?? '';
1161 $fields = str_replace(['"', ' '], '', $fields);
1162
1163 $unique = (stripos($def, 'UNIQUE INDEX') !== false) ? '1' : '0';
1164 $type = ($unique === '1') ? 'UNIQUE' : 'INDEX';
1165
1166 $indexes[] = [
1167 'name' => $name,
1168 'type' => $type,
1169 'fields' => $fields,
1170 'unique' => $unique,
1171 'comment' => '',
1172 ];
1173 }
1174 break;
1175 }
1176
1177 return $indexes;
1178 }
1179
1194 string $name,
1195 string $dbType,
1196 string $rawType,
1197 string $length,
1198 string $isNull,
1199 mixed $default,
1200 string $index = ''
1201 ): array {
1202 $ddType = $this->map_db_type_to_dd_type($dbType, $rawType, $length);
1203
1204 $field = [
1205 'name' => $name,
1206 'type' => $ddType,
1207 'index' => strtoupper((string)$index),
1208 'length' => $length,
1209 'default' => $this->normalize_default_value($default),
1210 'label' => $this->infer_label_from_name($name),
1211 'rules' => '',
1212 'tooltip' => '',
1213 'errormsg' => '',
1214 'placeholder' => '',
1215 'convert' => '',
1216 'protect' => $this->is_system_field($name) ? '2' : '0',
1217 'group' => '',
1218 'mask' => '',
1219 'data' => '',
1220 'options' => '',
1221 'tpl' => '',
1222 'js' => '',
1223 ];
1224
1225 $field['rules'] = $this->infer_rules_from_field($field);
1226 $field['tpl'] = $this->infer_tpl_from_field($field);
1227
1228 return $this->sort_schema_record($field, $this->dd_field_schema_keys());
1229 }
1230
1231
1232 /* =====================================================
1233 * DD -> DB SQL
1234 * ===================================================== */
1235
1244 protected function sql_quote(string $server, mixed $value): string
1245 {
1246 return "'" . $this->escape((string)$value, $server) . "'";
1247 }
1248
1257 protected function quote_ident(string $server, string $name): string
1258 {
1259 $dbType = $this->get_db_type($server);
1260
1261 return match ($dbType) {
1262 'mysql' => '`' . str_replace('`', '``', $name) . '`',
1263 'sqlsrv' => '[' . str_replace(']', ']]', $name) . ']',
1264 default => '"' . str_replace('"', '""', $name) . '"',
1265 };
1266 }
1267
1282 protected function map_dd_type_to_sql_type(string $dbType, string $type, string $length = ''): string
1283 {
1284 $type = strtolower(trim($type));
1285 $len = trim($length);
1286
1287 switch ($dbType) {
1288 case 'sqlite':
1289 return match ($type) {
1290 'bool',
1291 'boolean',
1292 'tinyint',
1293 'smallint',
1294 'mediumint',
1295 'int',
1296 'integer',
1297 'bigint',
1298 'bit' => 'INTEGER',
1299 'decimal',
1300 'numeric',
1301 'float',
1302 'double',
1303 'real' => 'REAL',
1304 'binary',
1305 'varbinary',
1306 'tinyblob',
1307 'blob',
1308 'mediumblob',
1309 'longblob' => 'BLOB',
1310 default => 'TEXT',
1311 };
1312
1313 case 'mysql':
1314 return match ($type) {
1315 'tinyint' => 'TINYINT' . ($len !== '' ? '(' . $len . ')' : ''),
1316 'smallint' => 'SMALLINT' . ($len !== '' ? '(' . $len . ')' : ''),
1317 'mediumint' => 'MEDIUMINT' . ($len !== '' ? '(' . $len . ')' : ''),
1318 'int',
1319 'integer' => 'INT' . ($len !== '' ? '(' . $len . ')' : '(11)'),
1320 'bigint' => 'BIGINT' . ($len !== '' ? '(' . $len . ')' : ''),
1321 'decimal',
1322 'numeric' => 'DECIMAL' . ($len !== '' ? '(' . $len . ')' : '(10,2)'),
1323 'float' => 'FLOAT' . ($len !== '' ? '(' . $len . ')' : ''),
1324 'double',
1325 'real' => 'DOUBLE' . ($len !== '' ? '(' . $len . ')' : ''),
1326 'bit' => 'BIT' . ($len !== '' ? '(' . $len . ')' : '(1)'),
1327 'char' => 'CHAR(' . ($len !== '' ? $len : '1') . ')',
1328 'binary' => 'BINARY(' . ($len !== '' ? $len : '1') . ')',
1329 'varbinary' => 'VARBINARY(' . ($len !== '' ? $len : '255') . ')',
1330 'tinytext' => 'TINYTEXT',
1331 'text' => 'TEXT',
1332 'mediumtext' => 'MEDIUMTEXT',
1333 'longtext' => 'LONGTEXT',
1334 'tinyblob' => 'TINYBLOB',
1335 'blob' => 'BLOB',
1336 'mediumblob' => 'MEDIUMBLOB',
1337 'longblob' => 'LONGBLOB',
1338 'json' => 'JSON',
1339 'enum' => ($len !== '' ? 'ENUM(' . $len . ')' : 'VARCHAR(255)'),
1340 'set' => ($len !== '' ? 'SET(' . $len . ')' : 'VARCHAR(255)'),
1341 'date' => 'DATE',
1342 'time' => 'TIME(6)',
1343 'year' => 'YEAR',
1344 'datetime' => 'DATETIME(6)',
1345 'timestamp' => 'TIMESTAMP(6)',
1346 default => 'VARCHAR(' . ($len !== '' ? $len : '255') . ')',
1347 };
1348
1349 case 'pgsql':
1350 return match ($type) {
1351 'int', 'integer', 'mediumint' => 'INTEGER',
1352 'bigint' => 'BIGINT',
1353 'smallint',
1354 'tinyint' => 'SMALLINT',
1355 'decimal',
1356 'numeric' => 'NUMERIC' . ($len !== '' ? '(' . $len . ')' : ''),
1357 'float',
1358 'real' => 'REAL',
1359 'double' => 'DOUBLE PRECISION',
1360 'date' => 'DATE',
1361 'datetime',
1362 'timestamp' => 'TIMESTAMP',
1363 'text',
1364 'mediumtext',
1365 'longtext' => 'TEXT',
1366 'char' => 'CHAR(' . ($len !== '' ? $len : '1') . ')',
1367 default => 'VARCHAR(' . ($len !== '' ? $len : '255') . ')',
1368 };
1369
1370 default:
1371 return match ($type) {
1372 'int', 'integer', 'mediumint' => 'INTEGER',
1373 'bigint' => 'BIGINT',
1374 'smallint',
1375 'tinyint' => 'SMALLINT',
1376 'decimal',
1377 'numeric' => 'DECIMAL' . ($len !== '' ? '(' . $len . ')' : ''),
1378 'float' => 'FLOAT',
1379 'double',
1380 'real' => 'DOUBLE',
1381 'date' => 'DATE',
1382 'datetime',
1383 'timestamp' => 'TIMESTAMP',
1384 'text',
1385 'mediumtext',
1386 'longtext' => 'TEXT',
1387 default => 'VARCHAR(' . ($len !== '' ? $len : '255') . ')',
1388 };
1389 }
1390 }
1391
1401 protected function build_default_sql(string $server, mixed $default, string $ddType): string
1402 {
1403 $default = (string)$default;
1404 $u = strtoupper($default);
1405
1406 if ($u === 'CURRENT_TIMESTAMP') {
1407 return 'DEFAULT CURRENT_TIMESTAMP';
1408 }
1409
1410 $numericTypes = [
1411 'int', 'integer', 'bigint', 'smallint', 'mediumint', 'tinyint', 'bit',
1412 'decimal', 'numeric', 'float', 'double', 'real',
1413 ];
1414
1415 if (in_array(strtolower($ddType), $numericTypes, true) && is_numeric($default)) {
1416 return 'DEFAULT ' . $default;
1417 }
1418
1419 return 'DEFAULT ' . $this->sql_quote($server, $default);
1420 }
1421
1430 protected function build_sql_column_from_dd(string $server, array $field): string
1431 {
1432 $dbType = $this->get_db_type($server);
1433 $name = $field['name'] ?? '';
1434 $type = strtolower((string)($field['type'] ?? 'varchar'));
1435 $length = (string)($field['length'] ?? '');
1436 $index = strtoupper((string)($field['index'] ?? ''));
1437 $default = $field['default'] ?? '';
1438
1439 $sqlType = $this->map_dd_type_to_sql_type($dbType, $type, $length);
1440 $col = $this->quote_ident($server, $name) . ' ' . $sqlType;
1441
1442 if ($index === 'PRI' && strtolower($name) === 'id') {
1443 switch ($dbType) {
1444 case 'sqlite':
1445 return $this->quote_ident($server, $name) . ' INTEGER PRIMARY KEY AUTOINCREMENT';
1446 case 'mysql':
1447 case 'cubrid':
1448 return $this->quote_ident($server, $name) . ' ' . $sqlType . ' AUTO_INCREMENT PRIMARY KEY';
1449 case 'pgsql':
1450 case 'firebird':
1451 case 'ibm':
1452 case 'odbc':
1453 return $this->quote_ident($server, $name) . ' INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY';
1454 case 'sqlsrv':
1455 case 'dblib':
1456 return $this->quote_ident($server, $name) . ' INTEGER IDENTITY(1,1) PRIMARY KEY';
1457 case 'oci':
1458 return $this->quote_ident($server, $name) . ' NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY';
1459 case 'informix':
1460 return $this->quote_ident($server, $name) . ' SERIAL PRIMARY KEY';
1461 default:
1462 break;
1463 }
1464 }
1465
1466 if ($default !== '' && $default !== null) {
1467 $col .= ' ' . $this->build_default_sql($server, $default, $type);
1468 }
1469
1470 return $col;
1471 }
1472
1485 protected function enforce_auto_increment_id(array $fields): array
1486 {
1487 $idField = array(
1488 'name' => 'id',
1489 'type' => 'int',
1490 'index' => 'PRI',
1491 'length' => '11',
1492 'default' => '',
1493 );
1494 $normalized = array();
1495
1496 foreach ($fields as $field) {
1497 $name = strtolower(trim((string)($field['name'] ?? '')));
1498
1499 if ($name === '') {
1500 continue;
1501 }
1502
1503 if ($name === 'id') {
1504 $idField = array_merge($field, $idField);
1505 continue;
1506 }
1507
1508 if (strtoupper((string)($field['index'] ?? '')) === 'PRI') {
1509 $field['index'] = '';
1510 }
1511
1512 $normalized[] = $field;
1513 }
1514
1515 array_unshift($normalized, $idField);
1516 return $normalized;
1517 }
1518
1523 protected function uses_inline_auto_increment_id(string $dbType): bool
1524 {
1525 return in_array($dbType, array(
1526 'sqlite', 'mysql', 'pgsql', 'sqlsrv', 'oci', 'firebird',
1527 'cubrid', 'dblib', 'ibm', 'informix', 'odbc',
1528 ), true);
1529 }
1530
1538 public function build_create_table_sql(string $dd): string
1539 {
1540 $server = $this->get_dd_server($dd);
1541 $table = $this->get_dd_table($dd);
1543 $dbType = $this->get_db_type($server);
1544
1545 $parts = [];
1546 $primaryFields = [];
1547
1548 foreach ($fields as $field) {
1549 $parts[] = $this->build_sql_column_from_dd($server, $field);
1550 if (strtoupper((string)($field['index'] ?? '')) === 'PRI') {
1551 $isInlinePrimary = strtolower((string)($field['name'] ?? '')) === 'id'
1552 && $this->uses_inline_auto_increment_id($dbType);
1553 if (!$isInlinePrimary) {
1554 $primaryFields[] = $field['name'];
1555 }
1556 }
1557 }
1558
1559 if ($primaryFields) {
1560 $quoted = [];
1561 foreach ($primaryFields as $fld) {
1562 $quoted[] = $this->quote_ident($server, $fld);
1563 }
1564 $parts[] = 'PRIMARY KEY (' . implode(',', $quoted) . ')';
1565 }
1566
1567 $sql = 'CREATE TABLE ' . $this->quote_ident($server, $table) . " (\n";
1568 $sql .= ' ' . implode(",\n ", $parts) . "\n";
1569 $sql .= ')';
1570
1571 return $sql;
1572 }
1573
1583 public function create_db_index(string $server, string $table, array $index): int
1584 {
1585 $name = (string)($index['name'] ?? '');
1586 $type = strtoupper((string)($index['type'] ?? 'INDEX'));
1587 $fields = array_filter(array_map('trim', explode(',', (string)($index['fields'] ?? ''))));
1588
1589 if (!$name || !$fields) {
1590 return 0;
1591 }
1592
1593 $quotedFields = [];
1594 foreach ($fields as $fld) {
1595 $quotedFields[] = $this->quote_ident($server, $fld);
1596 }
1597
1598 $sql = '';
1599 if ($type === 'UNIQUE') {
1600 $sql = 'CREATE UNIQUE INDEX ' . $this->quote_ident($server, $name)
1601 . ' ON ' . $this->quote_ident($server, $table)
1602 . ' (' . implode(',', $quotedFields) . ')';
1603 } elseif ($type !== 'PRIMARY') {
1604 $sql = 'CREATE INDEX ' . $this->quote_ident($server, $name)
1605 . ' ON ' . $this->quote_ident($server, $table)
1606 . ' (' . implode(',', $quotedFields) . ')';
1607 }
1608
1609 if (!$sql) {
1610 return ($type === 'PRIMARY') ? 1 : 0;
1611 }
1612
1613 return $this->exec_query($server, $sql);
1614 }
1615
1624 protected function ensure_sqlite_db_file_for_dd(string $dd): int
1625 {
1626 $server = $this->get_dd_server($dd);
1627
1628 if (!preg_match('/\.(db3|sqlite|sqlite3)$/i', $server)) {
1629 return 1;
1630 }
1631
1632 $dd_sys = $this->get_dd_cache_info($dd);
1633 $modul = $dd_sys['dd_modul'] ?? '';
1634 $name = $server;
1635
1636 if (strpos($server, '|') !== false) {
1637 $parts = explode('|', $server, 2);
1638 $modul = trim($parts[0]);
1639 $name = trim($parts[1]);
1640 }
1641
1642 if ($modul === '' || $modul === 'modul') {
1643 $modul = dbx()->get_system_var('dbx_activ_modul', 'dbx');
1644 }
1645
1646 if ($modul === '') {
1647 $modul = 'dbx';
1648 }
1649
1650 $dir = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/' . $modul . '/db/');
1651 if (!is_dir($dir) && !@mkdir($dir, 0777, true)) {
1652 dbx()->sys_msg('error', 'db', $server, 'create sqlite dir failed', $dir);
1653 return 0;
1654 }
1655
1656 $file = dbx()->os_path($dir . $name);
1657 if (!file_exists($file) && @file_put_contents($file, '') === false) {
1658 dbx()->sys_msg('error', 'db', $server, 'create sqlite file failed', $file);
1659 return 0;
1660 }
1661
1662 $hostRel = dbx()->config_path_store($dir, true);
1663
1664 $_SESSION['dbx']['config']['dbx']['db'][$server] = [
1665 'type' => 'sqlite',
1666 'host' => $hostRel,
1667 'dbname' => basename($file),
1668 'user' => '',
1669 'pass' => '',
1670 'port' => ''
1671 ];
1672
1673 if (!isset($this->db[$server]) && !$this->dbConnect($server, 'sqlite', $hostRel, basename($file))) {
1674 return 0;
1675 }
1676
1677 return 1;
1678 }
1679
1687 public function create_db_tab(string $dd): int
1688 {
1689 $server = $this->get_dd_server($dd);
1690 $table = $this->get_dd_table($dd);
1691
1692 if (!$server || !$table) {
1693 return 0;
1694 }
1695
1696 if (!$this->ensure_sqlite_db_file_for_dd($dd)) {
1697 return 0;
1698 }
1699
1700 if ($this->get_table_exist($server, $table)) {
1701 return 1;
1702 }
1703
1704 $sql = $this->build_create_table_sql($dd);
1705 $ok = $this->exec_query($server, $sql);
1706
1707 if (!$ok) {
1708 return 0;
1709 }
1710
1711 $indexes = $this->get_dd_indexes($dd);
1712 foreach ($indexes as $index) {
1713 if (strtoupper((string)($index['type'] ?? 'INDEX')) === 'PRIMARY') {
1714 continue;
1715 }
1717 }
1718
1719 return $this->get_table_exist($server, $table) ? 1 : 0;
1720 }
1721
1731 public function add_db_field_from_dd(string $server, string $table, array $field): int
1732 {
1733 $sql = 'ALTER TABLE ' . $this->quote_ident($server, $table)
1734 . ' ADD COLUMN ' . $this->build_sql_column_from_dd($server, $field);
1735
1736 return $this->exec_query($server, $sql);
1737 }
1738
1747 public function drop_db_tab(string $server, string $table): int
1748 {
1749 if (!$this->get_table_exist($server, $table)) {
1750 return 1;
1751 }
1752
1753 $sql = 'DROP TABLE ' . $this->quote_ident($server, $table);
1754 return $this->exec_query($server, $sql);
1755 }
1756
1767 public function create_db_tab_from_fields(string $server, string $table, array $fields, array $indexes = []): int
1768 {
1769 if (!$server || !$table || !$fields) {
1770 return 0;
1771 }
1772
1774
1775 if ($this->get_table_exist($server, $table)) {
1776 return 1;
1777 }
1778
1779 $dbType = $this->get_db_type($server);
1780 $parts = [];
1781 $primaryFields = [];
1782
1783 foreach ($fields as $field) {
1784 if (empty($field['name'])) {
1785 continue;
1786 }
1787
1788 $parts[] = $this->build_sql_column_from_dd($server, $field);
1789
1790 if (strtoupper((string)($field['index'] ?? '')) === 'PRI') {
1791 $isInlinePrimary = strtolower((string)($field['name'] ?? '')) === 'id'
1792 && $this->uses_inline_auto_increment_id($dbType);
1793 if (!$isInlinePrimary) {
1794 $primaryFields[] = $field['name'];
1795 }
1796 }
1797 }
1798
1799 if (!$parts) {
1800 return 0;
1801 }
1802
1803 if ($primaryFields) {
1804 $quoted = [];
1805 foreach ($primaryFields as $fld) {
1806 $quoted[] = $this->quote_ident($server, $fld);
1807 }
1808 $parts[] = 'PRIMARY KEY (' . implode(',', $quoted) . ')';
1809 }
1810
1811 $sql = 'CREATE TABLE ' . $this->quote_ident($server, $table) . " (\n";
1812 $sql .= ' ' . implode(",\n ", $parts) . "\n";
1813 $sql .= ')';
1814
1815 $ok = $this->exec_query($server, $sql);
1816 if (!$ok) {
1817 return 0;
1818 }
1819
1820 foreach ($indexes as $index) {
1821 if (strtoupper((string)($index['type'] ?? 'INDEX')) === 'PRIMARY') {
1822 continue;
1823 }
1825 }
1826
1827 return $this->get_table_exist($server, $table) ? 1 : 0;
1828 }
1829
1838 public function empty_db_table(string $server, string $table): int
1839 {
1840 if (!$server || !$table || !$this->get_table_exist($server, $table)) {
1841 return 0;
1842 }
1843
1844 $dbType = $this->get_db_type($server);
1845 $qTable = $this->quote_ident($server, $table);
1846 $ok = 0;
1847
1848 switch ($dbType) {
1849 case 'mysql':
1850 $ok = $this->exec_query($server, 'TRUNCATE TABLE ' . $qTable);
1851 if ($ok) {
1852 $this->exec_query($server, 'ALTER TABLE ' . $qTable . ' AUTO_INCREMENT = 1');
1853 }
1854 break;
1855
1856 case 'sqlite':
1857 $ok = $this->exec_query($server, 'DELETE FROM ' . $qTable);
1858 if ($ok && $this->sqlite_sequence_exists($server)) {
1859 $this->exec_query($server, 'DELETE FROM sqlite_sequence WHERE name=' . $this->sql_quote($server, $table));
1860 }
1861 break;
1862
1863 case 'pgsql':
1864 $ok = $this->exec_query($server, 'TRUNCATE TABLE ' . $qTable . ' RESTART IDENTITY');
1865 break;
1866
1867 default:
1868 $ok = $this->exec_query($server, 'DELETE FROM ' . $qTable);
1869 break;
1870 }
1871
1872 return $ok ? 1 : 0;
1873 }
1874
1883 public function find_dd_for_db_table(string $server, string $table): array
1884 {
1885 $serverKey = strtolower(trim($server));
1886 $tableKey = strtolower(trim($table));
1887
1888 if ($serverKey === '' || $tableKey === '') {
1889 return [];
1890 }
1891
1892 $base = str_replace('\\', '/', dbx()->get_base_dir()) . 'dbx/modules/*/dd/*.dd.php';
1893 foreach (glob($base) as $file) {
1894 $norm = str_replace('\\', '/', $file);
1895 if (!preg_match('#/dbx/modules/([^/]+)/dd/([^/]+)\.dd\.php$#', $norm, $match)) {
1896 continue;
1897 }
1898
1899 $modul = $match[1];
1900 $dd = $match[2];
1901 if ($dd === 'new' || $dd === '') {
1902 continue;
1903 }
1904
1905 $ddRef = $modul . '|' . $dd;
1906 $model = $this->get_dd_model($ddRef);
1907 if (!$model) {
1908 continue;
1909 }
1910
1911 $modelServer = strtolower((string)($model['table']['server'] ?? ''));
1912 $modelTable = strtolower((string)($model['table']['table'] ?? ''));
1913 $serverMatch = ($modelServer === $serverKey);
1914
1915 if (!$serverMatch && strpos($serverKey, '|') !== false) {
1916 [$serverModul, $serverName] = array_pad(explode('|', $serverKey, 2), 2, '');
1917 $serverMatch = ($serverModul === strtolower($modul))
1918 && ($modelServer === $serverName || $modelServer === 'modul');
1919 }
1920
1921 if ($serverMatch && $modelTable === $tableKey) {
1922 return [
1923 'modul' => $modul,
1924 'dd' => $dd,
1925 'dd_ref' => $ddRef,
1926 'model' => $model,
1927 ];
1928 }
1929 }
1930
1931 return [];
1932 }
1933
1945 public function get_preferred_table_schema(string $server, string $table): array
1946 {
1947 $match = $this->find_dd_for_db_table($server, $table);
1948 if ($match && !empty($match['model']['fields'])) {
1949 return [
1950 'source' => 'dd',
1951 'dd_ref' => $match['dd_ref'],
1952 'fields' => $match['model']['fields'] ?? [],
1953 'indexes' => $match['model']['indexes'] ?? [],
1954 ];
1955 }
1956
1957 return [
1958 'source' => 'db',
1959 'dd_ref' => '',
1960 'fields' => $this->get_db_fields($server, $table),
1961 'indexes' => $this->get_db_indexes($server, $table),
1962 ];
1963 }
1964
1976 public function transfer_table(
1977 string $sourceServer,
1978 string $sourceTable,
1979 string $targetServer,
1980 string $targetTable = '',
1981 string $mode = 'step',
1982 int $createTarget = 1,
1983 int $truncateTarget = 1
1984 ): array {
1985 $targetTable = $targetTable ?: $sourceTable;
1986 $mode = strtolower((string)$mode);
1987 if ($mode === '') {
1988 $mode = 'step';
1989 }
1990
1991 $key = $this->proc_key('transfer_table', [
1992 $sourceServer,
1993 $sourceTable,
1994 $targetServer,
1995 $targetTable,
1996 $createTarget,
1997 $truncateTarget,
1998 ]);
1999
2000 if ($mode === 'reset') {
2001 $this->clear_proc_state($key);
2002 return [
2003 'proc_key' => $key,
2004 'status' => 'reset',
2005 'message' => 'transfer state cleared',
2006 'percent' => 0,
2007 'step_percent' => 0,
2008 ];
2009 }
2010
2011 if ($mode === 'status') {
2012 $state = $this->get_proc_state($key);
2013 return $state ?: [
2014 'proc_key' => $key,
2015 'status' => 'new',
2016 'message' => 'no active state',
2017 'percent' => 0,
2018 'step_percent' => 0,
2019 ];
2020 }
2021
2022 if (in_array($mode, ['pause', 'resume', 'continue', 'cancel'], true)) {
2023 return $this->control_proc_state($key, $mode);
2024 }
2025
2026 if ($mode === 'restart') {
2027 $this->clear_proc_state($key);
2028 $mode = 'step';
2029 }
2030
2031 $state = $this->get_proc_state($key);
2032
2033 if (!$state) {
2034 if (!$sourceServer || !$sourceTable || !$targetServer || !$targetTable) {
2035 return $this->proc_error(['proc_key' => $key], 'source or target missing');
2036 }
2037
2038 if (!$this->get_table_exist($sourceServer, $sourceTable)) {
2039 return $this->proc_error(['proc_key' => $key], 'source table not found');
2040 }
2041
2042 $state = $this->init_proc_state('transfer_table', $key, [
2043 'source_server' => $sourceServer,
2044 'source_table' => $sourceTable,
2045 'target_server' => $targetServer,
2046 'target_table' => $targetTable,
2047 'create_target' => $createTarget ? 1 : 0,
2048 'truncate_target' => $truncateTarget ? 1 : 0,
2049 'backup_file' => $this->build_backup_file_name($sourceServer, $sourceTable, true),
2050 'phase' => 'prepare_target',
2051 'percent' => 0,
2052 'message' => 'transfer initialized',
2053 ]);
2054 }
2055
2056 if ($this->proc_is_waiting($state)) {
2057 return $this->proc_response($state);
2058 }
2059
2060 switch ($state['phase']) {
2061 case 'prepare_target':
2062 if (!$this->get_table_exist($state['target_server'], $state['target_table'])) {
2063 if (empty($state['create_target'])) {
2064 $state = $this->proc_error($state, 'target table not found');
2065 $this->set_proc_state($key, $state);
2066 return $state;
2067 }
2068
2069 $schema = $this->get_preferred_table_schema($state['source_server'], $state['source_table']);
2070 $fields = $schema['fields'] ?? [];
2071 $indexes = $schema['indexes'] ?? [];
2072 $ok = $this->create_db_tab_from_fields($state['target_server'], $state['target_table'], $fields, $indexes);
2073
2074 if (!$ok) {
2075 $state = $this->proc_error($state, 'create target table failed');
2076 $this->set_proc_state($key, $state);
2077 return $state;
2078 }
2079 }
2080
2081 if (!empty($state['truncate_target'])) {
2082 $this->empty_db_table($state['target_server'], $state['target_table']);
2083 }
2084
2085 $state['phase'] = 'backup_source';
2086 $state['percent'] = 10;
2087 $state['step_percent'] = 100;
2088 $state['message'] = 'target ready';
2089 $this->set_proc_state($key, $state);
2090 return $this->proc_response($state);
2091
2092 case 'backup_source':
2093 $backup = $this->backup($state['source_server'], $state['source_table'], $state['backup_file'], 1);
2094
2095 if (($backup['status'] ?? '') === 'error') {
2096 $state = $this->proc_error($state, 'backup source failed');
2097 $this->set_proc_state($key, $state);
2098 return $state;
2099 }
2100
2101 if (($backup['status'] ?? '') !== 'finished') {
2102 $state['percent'] = 10 + (int)floor((($backup['percent'] ?? 0) * 0.4));
2103 $state['step_percent'] = (int)($backup['percent'] ?? 0);
2104 $state['message'] = $backup['message'] ?? 'backup source';
2105 $this->set_proc_state($key, $state);
2106 return $this->proc_response($state);
2107 }
2108
2109 $state['phase'] = 'restore_target';
2110 $state['percent'] = 55;
2111 $state['step_percent'] = 100;
2112 $state['message'] = 'backup finished';
2113 $this->set_proc_state($key, $state);
2114 return $this->proc_response($state);
2115
2116 case 'restore_target':
2117 $restore = $this->restore($state['target_server'], $state['target_table'], $state['backup_file'], [], 1);
2118
2119 if (($restore['status'] ?? '') === 'error') {
2120 $state = $this->proc_error($state, 'restore target failed');
2121 $this->set_proc_state($key, $state);
2122 return $state;
2123 }
2124
2125 if (($restore['status'] ?? '') !== 'finished') {
2126 $state['percent'] = 55 + (int)floor((($restore['percent'] ?? 0) * 0.4));
2127 $state['step_percent'] = (int)($restore['percent'] ?? 0);
2128 $state['message'] = $restore['message'] ?? 'restore target';
2129 $this->set_proc_state($key, $state);
2130 return $this->proc_response($state);
2131 }
2132
2133 $state = $this->proc_finish($state, 'transfer finished');
2134 $this->set_proc_state($key, $state);
2135 return $state;
2136 }
2137
2138 $state = $this->proc_error($state, 'unknown transfer phase');
2139 $this->set_proc_state($key, $state);
2140 return $state;
2141 }
2142
2143
2144 /* =====================================================
2145 * STEP STATE
2146 * ===================================================== */
2147
2156 protected function proc_key(string $type, array $parts): string
2157 {
2158 return 'dbxdd_' . $type . '_' . md5(json_encode($parts));
2159 }
2160
2168 protected function get_proc_state(string $key): array
2169 {
2170 $state = dbx()->get_remember_var($key, [], $this->_remember_modul);
2171 return is_array($state) ? $state : [];
2172 }
2173
2182 protected function set_proc_state(string $key, array $state): void
2183 {
2184 dbx()->set_remember_var($key, $state, $this->_remember_modul);
2185 }
2186
2194 protected function clear_proc_state(string $key): void
2195 {
2196 dbx()->set_remember_var($key, [], $this->_remember_modul);
2197 }
2198
2208 protected function init_proc_state(string $type, string $key, array $state): array
2209 {
2210 $state['proc_type'] = $type;
2211 $state['proc_key'] = $key;
2212 $state['status'] = $state['status'] ?? 'running';
2213 $state['message'] = $state['message'] ?? '';
2214 $state['percent'] = $state['percent'] ?? 0;
2215 $state['step_percent'] = $state['step_percent'] ?? 0;
2216 $state['chunk_size'] = $state['chunk_size'] ?? $this->_chunk_size;
2217 $state['step_maxsec'] = $state['step_maxsec'] ?? $this->_max_step_runtime;
2218 $state['started_at'] = $state['started_at'] ?? date('Y-m-d H:i:s');
2219 $state['updated_at'] = date('Y-m-d H:i:s');
2220 $this->set_proc_state($key, $state);
2221 return $state;
2222 }
2223
2231 protected function proc_is_waiting(array $state): bool
2232 {
2233 return in_array(($state['status'] ?? ''), ['finished', 'error', 'paused', 'canceled'], true);
2234 }
2235
2244 protected function control_proc_state(string $key, string $cmd): array
2245 {
2246 $cmd = strtolower(trim($cmd));
2247
2248 if ($cmd === 'restart') {
2249 $this->clear_proc_state($key);
2250 return [
2251 'proc_key' => $key,
2252 'status' => 'reset',
2253 'message' => 'process restarted',
2254 'percent' => 0,
2255 'step_percent' => 0,
2256 'updated_at' => date('Y-m-d H:i:s'),
2257 ];
2258 }
2259
2260 $state = $this->get_proc_state($key);
2261 if (!$state) {
2262 $state = [
2263 'proc_key' => $key,
2264 'status' => 'new',
2265 'message' => 'no active state',
2266 'percent' => 0,
2267 'step_percent' => 0,
2268 ];
2269 }
2270
2271 $status = $state['status'] ?? 'new';
2272
2273 if ($cmd === 'pause') {
2274 if (!in_array($status, ['finished', 'error', 'canceled'], true)) {
2275 $state['status'] = 'paused';
2276 $state['paused_at'] = date('Y-m-d H:i:s');
2277 $state['message'] = 'process paused';
2278 }
2279 } elseif ($cmd === 'resume' || $cmd === 'continue') {
2280 if (in_array($status, ['paused', 'canceled', 'new'], true)) {
2281 $state['status'] = 'running';
2282 $state['resumed_at'] = date('Y-m-d H:i:s');
2283 $state['message'] = ($cmd === 'continue') ? 'process continued' : 'process resumed';
2284 }
2285 } elseif ($cmd === 'cancel') {
2286 if (!in_array($status, ['finished', 'error'], true)) {
2287 $state['status'] = 'canceled';
2288 $state['canceled_at'] = date('Y-m-d H:i:s');
2289 $state['message'] = 'process canceled';
2290 }
2291 }
2292
2293 $state = $this->proc_response($state);
2294 $this->set_proc_state($key, $state);
2295 return $state;
2296 }
2297
2305 protected function proc_response(array $state): array
2306 {
2307 $state['updated_at'] = date('Y-m-d H:i:s');
2308 return $state;
2309 }
2310
2319 protected function proc_error(array $state, string $message): array
2320 {
2321 $state['status'] = 'error';
2322 $state['message'] = $message;
2323 $state['percent'] = $state['percent'] ?? 0;
2324 $state['step_percent'] = $state['step_percent'] ?? 0;
2325 return $this->proc_response($state);
2326 }
2327
2336 protected function proc_finish(array $state, string $message = 'finished'): array
2337 {
2338 $state['status'] = 'finished';
2339 $state['message'] = $message;
2340 $state['percent'] = 100;
2341 $state['step_percent'] = 100;
2342 $state['finished_at'] = date('Y-m-d H:i:s');
2343 return $this->proc_response($state);
2344 }
2345
2351 protected function step_start_time(): float
2352 {
2353 return microtime(true);
2354 }
2355
2364 protected function step_time_left(float $started_at, float $max_seconds): bool
2365 {
2366 return (microtime(true) - $started_at) < $max_seconds;
2367 }
2368
2369
2370 /* =====================================================
2371 * SCHEMA MAPPING
2372 * ===================================================== */
2373
2381 protected function normalize_schema_field_key(string $name): string
2382 {
2383 return strtolower(preg_replace('/[^a-z0-9]+/i', '', trim($name)));
2384 }
2385
2393 protected function schema_mapping_file_part(string $value): string
2394 {
2395 $value = preg_replace('/[^A-Za-z0-9_.-]+/', '_', trim($value));
2396 $value = trim((string)$value, '._-');
2397
2398 return $value !== '' ? $value : 'default';
2399 }
2400
2406 protected function schema_mapping_dir(): string
2407 {
2408 $dir = dbx()->os_path(dbx()->get_file_dir() . 'db/schema-mapping/');
2409 if (!is_dir($dir)) {
2410 @mkdir($dir, 0777, true);
2411 }
2412
2413 return $dir;
2414 }
2415
2424 public function schema_mapping_path(string $kind, array $context): string
2425 {
2426 $kind = strtolower(trim($kind));
2427
2428 $parts = [
2429 $kind ?: 'schema',
2430 $context['modul'] ?? '',
2431 $context['dd'] ?? '',
2432 $context['source_server'] ?? ($context['server'] ?? ''),
2433 $context['source_table'] ?? ($context['table'] ?? ''),
2434 $context['target_server'] ?? '',
2435 $context['target_table'] ?? '',
2436 ];
2437
2438 $parts = array_map(fn($v) => $this->schema_mapping_file_part((string)$v), $parts);
2439
2440 return $this->schema_mapping_dir() . implode('__', $parts) . '.json';
2441 }
2442
2450 protected function schema_fields_by_name(array $fields): array
2451 {
2452 $index = [];
2453
2454 foreach ($fields as $field) {
2455 $name = (string)($field['name'] ?? '');
2456 if ($name !== '') {
2457 $index[$name] = $field;
2458 }
2459 }
2460
2461 return $this->sort_schema_record($index, $this->dd_index_schema_keys());
2462 }
2463
2464 protected function sort_schema_record(array $record, array $keys): array
2465 {
2466 $sorted = [];
2467
2468 foreach ($keys as $key) {
2469 if (array_key_exists($key, $record)) {
2470 $sorted[$key] = $record[$key];
2471 unset($record[$key]);
2472 }
2473 }
2474
2475 foreach ($record as $key => $value) {
2476 $sorted[$key] = $value;
2477 }
2478
2479 return $sorted;
2480 }
2481
2491 public function normalize_schema_mapping(array $mapping, array $sourceFields, array $targetFields): array
2492 {
2493 $sourceByLower = [];
2494 foreach ($sourceFields as $field) {
2495 $name = (string)($field['name'] ?? '');
2496 if ($name !== '') {
2497 $sourceByLower[strtolower($name)] = $name;
2498 }
2499 }
2500
2501 $targetByLower = [];
2502 foreach ($targetFields as $field) {
2503 $name = (string)($field['name'] ?? '');
2504 if ($name !== '') {
2505 $targetByLower[strtolower($name)] = $name;
2506 }
2507 }
2508
2509 $clean = [];
2510 foreach ($mapping as $source => $target) {
2511 $sourceKey = strtolower(trim((string)$source));
2512 $targetKey = strtolower(trim((string)$target));
2513
2514 if ($sourceKey === '' || $targetKey === '') {
2515 continue;
2516 }
2517
2518 if (!isset($sourceByLower[$sourceKey]) || !isset($targetByLower[$targetKey])) {
2519 continue;
2520 }
2521
2522 $sourceName = $sourceByLower[$sourceKey];
2523 $targetName = $targetByLower[$targetKey];
2524
2525 foreach ($clean as $oldSource => $oldTarget) {
2526 if (strcasecmp($oldTarget, $targetName) === 0) {
2527 unset($clean[$oldSource]);
2528 }
2529 }
2530
2531 $clean[$sourceName] = $targetName;
2532 }
2533
2534 ksort($clean);
2535 return $clean;
2536 }
2537
2547 protected function auto_schema_mapping(array $sourceFields, array $targetFields, array $stored = []): array
2548 {
2549 $sourceExact = [];
2550 $sourceNorm = [];
2551
2552 foreach ($sourceFields as $field) {
2553 $name = (string)($field['name'] ?? '');
2554 if ($name === '') {
2555 continue;
2556 }
2557
2558 $sourceExact[strtolower($name)] = $name;
2559 $norm = $this->normalize_schema_field_key($name);
2560 if ($norm !== '' && !isset($sourceNorm[$norm])) {
2561 $sourceNorm[$norm] = $name;
2562 }
2563 }
2564
2565 $mapping = [];
2566 $usedSource = [];
2567
2568 foreach ($targetFields as $field) {
2569 $target = (string)($field['name'] ?? '');
2570 if ($target === '') {
2571 continue;
2572 }
2573
2574 $source = $sourceExact[strtolower($target)] ?? '';
2575 if ($source !== '') {
2576 $mapping[$source] = $target;
2577 $usedSource[strtolower($source)] = true;
2578 }
2579 }
2580
2581 foreach ($targetFields as $field) {
2582 $target = (string)($field['name'] ?? '');
2583 if ($target === '') {
2584 continue;
2585 }
2586
2587 $alreadyMapped = false;
2588 foreach ($mapping as $mappedTarget) {
2589 if (strcasecmp($mappedTarget, $target) === 0) {
2590 $alreadyMapped = true;
2591 break;
2592 }
2593 }
2594
2595 if ($alreadyMapped) {
2596 continue;
2597 }
2598
2599 $norm = $this->normalize_schema_field_key($target);
2600 $source = $sourceNorm[$norm] ?? '';
2601 if ($source !== '' && empty($usedSource[strtolower($source)])) {
2602 $mapping[$source] = $target;
2603 $usedSource[strtolower($source)] = true;
2604 }
2605 }
2606
2607 $stored = $this->normalize_schema_mapping($stored, $sourceFields, $targetFields);
2608 foreach ($stored as $source => $target) {
2609 foreach ($mapping as $oldSource => $oldTarget) {
2610 if (strcasecmp($oldSource, $source) === 0 || strcasecmp($oldTarget, $target) === 0) {
2611 unset($mapping[$oldSource]);
2612 }
2613 }
2614
2615 $mapping[$source] = $target;
2616 }
2617
2618 ksort($mapping);
2619 return $mapping;
2620 }
2621
2630 public function load_schema_mapping(string $kind, array $context): array
2631 {
2632 $file = $this->schema_mapping_path($kind, $context);
2633 if (!is_file($file)) {
2634 return [
2635 'kind' => strtolower(trim($kind)),
2636 'context' => $context,
2637 'mapping' => [],
2638 'file' => $file,
2639 'updated_at' => '',
2640 ];
2641 }
2642
2643 $data = json_decode((string)file_get_contents($file), true);
2644 if (!is_array($data)) {
2645 $data = [];
2646 }
2647
2648 $data['kind'] = $data['kind'] ?? strtolower(trim($kind));
2649 $data['context'] = is_array($data['context'] ?? null) ? $data['context'] : $context;
2650 $data['mapping'] = is_array($data['mapping'] ?? null) ? $data['mapping'] : [];
2651 $data['file'] = $file;
2652
2653 return $data;
2654 }
2655
2664 public function get_schema_mapping_values(string $kind, array $context): array
2665 {
2666 $data = $this->load_schema_mapping($kind, $context);
2667
2668 return is_array($data['mapping'] ?? null) ? $data['mapping'] : [];
2669 }
2670
2679 public function build_schema_mapping(string $kind, array $context): array
2680 {
2681 $kind = strtolower(trim($kind));
2682 if ($kind === '') {
2683 $kind = 'dd_to_db';
2684 }
2685
2686 $modul = (string)($context['modul'] ?? '');
2687 $dd = (string)($context['dd'] ?? '');
2688 $server = (string)($context['server'] ?? ($context['source_server'] ?? ''));
2689 $table = (string)($context['table'] ?? ($context['source_table'] ?? ''));
2690
2691 $sourceFields = [];
2692 $targetFields = [];
2693 $sourceLabel = '';
2694 $targetLabel = '';
2695 $ddRef = ($modul && $dd && strpos($dd, '|') === false) ? $modul . '|' . $dd : $dd;
2696
2697 if ($kind === 'db_to_dd') {
2698 if ($server && $table && $this->get_table_exist($server, $table)) {
2699 $sourceFields = $this->get_db_fields($server, $table);
2700 }
2701
2702 $oldModel = [];
2703 if ($ddRef && ($this->get_dd_exist($ddRef) || $this->get_dd_exist($dd))) {
2704 $oldModel = $this->get_dd_model($ddRef);
2705 if (!$oldModel) {
2706 $oldModel = $this->get_dd_model($dd);
2707 }
2708 }
2709
2710 $targetFields = $oldModel['fields'] ?? $sourceFields;
2711 $sourceLabel = $server . '|' . $table;
2712 $targetLabel = ($modul ? $modul . '|' : '') . $dd;
2713 } elseif ($kind === 'transfer') {
2714 $sourceServer = (string)($context['source_server'] ?? $server);
2715 $sourceTable = (string)($context['source_table'] ?? $table);
2716 $targetServer = (string)($context['target_server'] ?? '');
2717 $targetTable = (string)($context['target_table'] ?? '');
2718
2719 if ($sourceServer && $sourceTable && $this->get_table_exist($sourceServer, $sourceTable)) {
2720 $schema = $this->get_preferred_table_schema($sourceServer, $sourceTable);
2721 $sourceFields = $schema['fields'] ?? [];
2722 }
2723 if ($targetServer && $targetTable && $this->get_table_exist($targetServer, $targetTable)) {
2724 $targetFields = $this->get_db_fields($targetServer, $targetTable);
2725 } else {
2726 $targetFields = $sourceFields;
2727 }
2728
2729 $sourceLabel = $sourceServer . '|' . $sourceTable;
2730 $targetLabel = $targetServer . '|' . $targetTable;
2731 } else {
2732 $kind = 'dd_to_db';
2733 $model = $ddRef ? $this->get_dd_model($ddRef) : [];
2734 if (!$model && $dd) {
2735 $model = $this->get_dd_model($dd);
2736 }
2737
2738 $server = $server ?: (string)($model['table']['server'] ?? '');
2739 $table = $table ?: (string)($model['table']['table'] ?? '');
2740
2741 if ($server && $table && $this->get_table_exist($server, $table)) {
2742 $sourceFields = $this->get_db_fields($server, $table);
2743 }
2744
2745 $targetFields = $model['fields'] ?? [];
2746 $sourceLabel = $server . '|' . $table;
2747 $targetLabel = ($modul ? $modul . '|' : '') . $dd;
2748 }
2749
2750 $context['modul'] = $modul;
2751 $context['dd'] = $dd;
2752 $context['server'] = $server;
2753 $context['table'] = $table;
2754
2755 $storedData = $this->load_schema_mapping($kind, $context);
2756 $stored = is_array($storedData['mapping'] ?? null) ? $storedData['mapping'] : [];
2757 $mapping = $this->auto_schema_mapping($sourceFields, $targetFields, $stored);
2758
2759 $sourceByName = $this->schema_fields_by_name($sourceFields);
2760 $usedSources = [];
2761 $targetRows = [];
2762
2763 foreach ($targetFields as $target) {
2764 $targetName = (string)($target['name'] ?? '');
2765 if ($targetName === '') {
2766 continue;
2767 }
2768
2769 $sourceName = '';
2770 foreach ($mapping as $source => $mappedTarget) {
2771 if (strcasecmp($mappedTarget, $targetName) === 0) {
2772 $sourceName = $source;
2773 $usedSources[strtolower($source)] = true;
2774 break;
2775 }
2776 }
2777
2778 $source = $sourceName !== '' && isset($sourceByName[$sourceName]) ? $sourceByName[$sourceName] : [];
2779 $status = 'new';
2780 if ($sourceName !== '') {
2781 $status = (strcasecmp($sourceName, $targetName) === 0) ? 'exact' : 'mapped';
2782
2784 $status = 'type_conflict';
2785 }
2786 }
2787
2788 $targetRows[] = [
2789 'target' => $target,
2790 'source' => $source,
2791 'source_name' => $sourceName,
2792 'target_name' => $targetName,
2793 'status' => $status,
2794 ];
2795 }
2796
2797 $unmappedSources = [];
2798 foreach ($sourceFields as $source) {
2799 $name = (string)($source['name'] ?? '');
2800 if ($name !== '' && empty($usedSources[strtolower($name)])) {
2801 $unmappedSources[] = $source;
2802 }
2803 }
2804
2805 return [
2806 'kind' => $kind,
2807 'context' => $context,
2808 'source_label' => $sourceLabel,
2809 'target_label' => $targetLabel,
2810 'source_fields' => $sourceFields,
2811 'target_fields' => $targetFields,
2812 'target_rows' => $targetRows,
2813 'unmapped_sources' => $unmappedSources,
2814 'mapping' => $mapping,
2815 'stored_mapping' => $stored,
2816 'file' => $storedData['file'] ?? $this->schema_mapping_path($kind, $context),
2817 'updated_at' => $storedData['updated_at'] ?? '',
2818 ];
2819 }
2820
2830 public function save_schema_mapping(string $kind, array $context, array $mapping): int
2831 {
2832 $model = $this->build_schema_mapping($kind, $context);
2833 $clean = $this->normalize_schema_mapping($mapping, $model['source_fields'] ?? [], $model['target_fields'] ?? []);
2834
2835 $data = [
2836 'kind' => $model['kind'] ?? strtolower(trim($kind)),
2837 'context' => $model['context'] ?? $context,
2838 'source' => $model['source_label'] ?? '',
2839 'target' => $model['target_label'] ?? '',
2840 'mapping' => $clean,
2841 'updated_at' => date('Y-m-d H:i:s'),
2842 ];
2843
2844 $file = $model['file'] ?? $this->schema_mapping_path($kind, $context);
2845 $ok = file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
2846
2847 return $ok === false ? 0 : 1;
2848 }
2849
2850
2851 /* =====================================================
2852 * CREATE DD
2853 * ===================================================== */
2854
2869 public function create_dd(string $modul, string $dd, string $server = 'dbXsystem', string $table = ''): int
2870 {
2871 if (!$table) {
2872 $table = $dd;
2873 }
2874
2875 if (!$this->get_table_exist($server, $table)) {
2876 dbx()->sys_msg('error', 'dd', $dd, 'create_dd', 'table not found');
2877 return 0;
2878 }
2879
2880 $tableMeta = $this->normalize_table_record($dd, [
2881 'server' => $server,
2882 'table' => $table,
2883 ]);
2884
2887
2888 return $this->save_dd($modul, $dd, $tableMeta, $fields, $indexes);
2889 }
2890
2891
2892 /* =====================================================
2893 * BACKUP
2894 * ===================================================== */
2895
2905 protected function build_backup_file_name(string $server, string $table, bool $zip = false): string
2906 {
2907 $dir = dbx()->os_path(dbx()->get_file_dir() . 'db/dd-backup/');
2908 if (!is_dir($dir)) {
2909 @mkdir($dir, 0777, true);
2910 }
2911
2912 $name = $table . '_' . date('Ymd_His') . '.ddb';
2913 if ($zip) {
2914 $name .= '.zip';
2915 }
2916
2917 return $dir . $name;
2918 }
2919
2929 protected function finalize_backup_file(string $tmpFile, string $finalFile, bool $zip): int
2930 {
2931 if (!$zip) {
2932 @unlink($finalFile);
2933 return @rename($tmpFile, $finalFile) ? 1 : 0;
2934 }
2935
2936 if (!class_exists('ZipArchive')) {
2937 $raw = preg_replace('/\.zip$/i', '', $finalFile);
2938 @unlink($raw);
2939 return @rename($tmpFile, $raw) ? 1 : 0;
2940 }
2941
2942 $zipObj = new ZipArchive();
2943 if ($zipObj->open($finalFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
2944 return 0;
2945 }
2946
2947 $inner = basename(preg_replace('/\.zip$/i', '', $finalFile));
2948 $zipObj->addFile($tmpFile, $inner);
2949 $zipObj->close();
2950
2951 @unlink($tmpFile);
2952 return 1;
2953 }
2954
2974 public function backup($server, $table, $file = '', $zip = 0): array
2975 {
2976 $key = $this->proc_key('backup', [$server, $table, $file, $zip]);
2977 $state = $this->get_proc_state($key);
2978
2979 if ($state && $this->proc_is_waiting($state)) {
2980 return $this->proc_response($state);
2981 }
2982
2983 if (!$state) {
2984 if (!$server || !$table) {
2985 return $this->proc_error(['proc_key' => $key], 'server or table missing');
2986 }
2987
2988 if (!$this->get_table_exist($server, $table)) {
2989 return $this->proc_error(['proc_key' => $key], 'table not found');
2990 }
2991
2993 if (!$fields) {
2994 return $this->proc_error(['proc_key' => $key], 'no fields found');
2995 }
2996
2997 $columns = [];
2998 $fieldTypes = [];
2999 $pk = '';
3000 foreach ($fields as $f) {
3001 $columns[] = $f['name'];
3002 $fieldTypes[$f['name']] = strtolower((string)($f['type'] ?? ''));
3003 if (strtoupper((string)($f['index'] ?? '')) === 'PRI' && !$pk) {
3004 $pk = $f['name'];
3005 }
3006 }
3007
3008 $total = $this->count($table, '', $server);
3009 if ($total < 0) {
3010 return $this->proc_error(['proc_key' => $key], 'count failed');
3011 }
3012
3013 if (!$file) {
3014 $file = $this->build_backup_file_name($server, $table, (bool)$zip);
3015 }
3016
3017 $tmpFile = $file . '.part';
3018 @unlink($tmpFile);
3019
3020 $meta = [
3021 'server' => $server,
3022 'table' => $table,
3023 'db_type' => $this->get_db_type($server),
3024 'backup_date' => date('Y-m-d H:i:s'),
3025 'row_count' => $total,
3026 'compact' => 1,
3027 'zip' => $zip ? 1 : 0,
3028 ];
3029
3030 file_put_contents($tmpFile, json_encode(['meta' => $meta], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n");
3031 file_put_contents($tmpFile, json_encode([
3032 'columns' => $columns,
3033 'field_types' => $fieldTypes,
3034 ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n", FILE_APPEND);
3035
3036 $state = $this->init_proc_state('backup', $key, [
3037 'server' => $server,
3038 'table' => $table,
3039 'file' => $file,
3040 'tmp_file' => $tmpFile,
3041 'zip' => $zip ? 1 : 0,
3042 'columns' => $columns,
3043 'pk' => $pk,
3044 'offset' => 0,
3045 'done' => 0,
3046 'total' => $total,
3047 'message' => 'backup initialized',
3048 ]);
3049 }
3050
3051 $started = $this->step_start_time();
3052
3053 while ($this->step_time_left($started, (float)$state['step_maxsec'])) {
3054 if ((int)$state['done'] >= (int)$state['total']) {
3055 $ok = $this->finalize_backup_file($state['tmp_file'], $state['file'], !empty($state['zip']));
3056 if (!$ok) {
3057 $state = $this->proc_error($state, 'finalize backup failed');
3058 $this->set_proc_state($key, $state);
3059 return $state;
3060 }
3061
3062 $state = $this->proc_finish($state, 'backup finished');
3063 $this->set_proc_state($key, $state);
3064 return $state;
3065 }
3066
3067 $chunk = (int)$state['chunk_size'];
3068 $cols = [];
3069 foreach ($state['columns'] as $col) {
3070 $cols[] = $this->quote_ident($state['server'], $col);
3071 }
3072
3073 $order = '';
3074 if (!empty($state['pk'])) {
3075 $order = ' ORDER BY ' . $this->quote_ident($state['server'], $state['pk']);
3076 }
3077
3078 $dbType = $this->get_db_type($state['server']);
3079 if ($dbType === 'mysql') {
3080 $limit = ' LIMIT ' . (int)$state['offset'] . ', ' . $chunk;
3081 } else {
3082 $limit = ' LIMIT ' . $chunk . ' OFFSET ' . (int)$state['offset'];
3083 }
3084
3085 $sql = 'SELECT ' . implode(',', $cols);
3086 $sql .= ' FROM ' . $this->quote_ident($state['server'], $state['table']);
3087 $sql .= $order . $limit;
3088
3089 $rows = $this->rawQuery($state['server'], $sql);
3090 if (!is_array($rows)) {
3091 $state = $this->proc_error($state, 'backup select failed');
3092 $this->set_proc_state($key, $state);
3093 return $state;
3094 }
3095
3096 if (!$rows) {
3097 $state['done'] = $state['total'];
3098 $state['percent'] = 100;
3099 $state['step_percent'] = 100;
3100 continue;
3101 }
3102
3103 $records = [];
3104 foreach ($rows as $row) {
3105 $rec = [];
3106 foreach ($state['columns'] as $col) {
3107 $rec[] = $row[$col] ?? null;
3108 }
3109 $records[] = $rec;
3110 }
3111
3112 file_put_contents(
3113 $state['tmp_file'],
3114 json_encode(['records' => $records], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n",
3115 FILE_APPEND
3116 );
3117
3118 $count = count($rows);
3119 $state['offset'] += $count;
3120 $state['done'] += $count;
3121 $state['percent'] = ($state['total'] > 0) ? (int)floor(($state['done'] / $state['total']) * 100) : 100;
3122 $state['step_percent'] = $state['percent'];
3123 $state['message'] = 'backup rows ' . $state['done'] . ' / ' . $state['total'];
3124 }
3125
3126 $this->set_proc_state($key, $state);
3127 return $this->proc_response($state);
3128 }
3129
3130
3131 /* =====================================================
3132 * RESTORE
3133 * ===================================================== */
3134
3143 protected function prepare_restore_source_file(string $file, bool $zip): string
3144 {
3145 if (!$zip) {
3146 return $file;
3147 }
3148
3149 if (!class_exists('ZipArchive')) {
3150 $raw = preg_replace('/\.zip$/i', '', $file);
3151 return file_exists($raw) ? $raw : '';
3152 }
3153
3154 $tmpDir = dbx()->os_path(dbx()->get_file_dir() . 'temp/dd-restore/');
3155 if (!is_dir($tmpDir)) {
3156 @mkdir($tmpDir, 0777, true);
3157 }
3158
3159 $tmpFile = $tmpDir . md5($file) . '.ddb';
3160
3161 if (file_exists($tmpFile)) {
3162 return $tmpFile;
3163 }
3164
3165 $zipObj = new ZipArchive();
3166 if ($zipObj->open($file) !== true) {
3167 return '';
3168 }
3169
3170 if ($zipObj->numFiles < 1) {
3171 $zipObj->close();
3172 return '';
3173 }
3174
3175 $content = $zipObj->getFromIndex(0);
3176 $zipObj->close();
3177
3178 if ($content === false) {
3179 return '';
3180 }
3181
3182 file_put_contents($tmpFile, $content);
3183 return $tmpFile;
3184 }
3185
3208 public function restore($server, $table, $file, $mapping = [], $zip = 0): array
3209 {
3210 $key = $this->proc_key('restore', [$server, $table, $file, $mapping, $zip]);
3211 $state = $this->get_proc_state($key);
3212
3213 if ($state && $this->proc_is_waiting($state)) {
3214 return $this->proc_response($state);
3215 }
3216
3217 if (!$state) {
3218 if (!$server || !$table || !$file) {
3219 return $this->proc_error(['proc_key' => $key], 'server, table or file missing');
3220 }
3221
3222 if (!$this->get_table_exist($server, $table)) {
3223 return $this->proc_error(['proc_key' => $key], 'target table not found');
3224 }
3225
3226 $sourceFile = $this->prepare_restore_source_file($file, (bool)$zip);
3227 if (!$sourceFile || !file_exists($sourceFile)) {
3228 return $this->proc_error(['proc_key' => $key], 'restore source file not readable');
3229 }
3230
3231 $fh = fopen($sourceFile, 'rb');
3232 if (!$fh) {
3233 return $this->proc_error(['proc_key' => $key], 'restore open source failed');
3234 }
3235
3236 $line1 = fgets($fh);
3237 $line2 = fgets($fh);
3238
3239 $metaRec = json_decode((string)$line1, true);
3240 $colRec = json_decode((string)$line2, true);
3241
3242 if (!is_array($metaRec) || !isset($metaRec['meta']) || !is_array($colRec) || !isset($colRec['columns'])) {
3243 fclose($fh);
3244 return $this->proc_error(['proc_key' => $key], 'invalid backup format');
3245 }
3246
3247 $filePos = ftell($fh);
3248 fclose($fh);
3249
3250 $targetFields = $this->get_db_fields($server, $table);
3251 $targetLookup = [];
3252 $targetTypes = [];
3253 foreach ($targetFields as $f) {
3254 $targetLookup[$f['name']] = true;
3255 $targetTypes[$f['name']] = strtolower((string)($f['type'] ?? ''));
3256 }
3257
3258 if (!$this->empty_db_table($server, $table)) {
3259 return $this->proc_error(['proc_key' => $key], 'target table not found after empty');
3260 }
3261
3262 $state = $this->init_proc_state('restore', $key, [
3263 'server' => $server,
3264 'table' => $table,
3265 'file' => $file,
3266 'source_file' => $sourceFile,
3267 'zip' => $zip ? 1 : 0,
3268 'mapping' => is_array($mapping) ? $mapping : [],
3269 'source_columns' => $colRec['columns'],
3270 'source_types' => is_array($colRec['field_types'] ?? null)
3271 ? $colRec['field_types']
3272 : [],
3273 'target_lookup' => $targetLookup,
3274 'target_types' => $targetTypes,
3275 'target_db_type' => $this->get_db_type($server),
3276 'file_pos' => $filePos,
3277 'done' => 0,
3278 'total' => (int)($metaRec['meta']['row_count'] ?? 0),
3279 'message' => 'restore initialized',
3280 ]);
3281 }
3282
3283 if (!$this->connect_db_server($state['server'])) {
3284 $state = $this->proc_error($state, 'db connect failed');
3285 $this->set_proc_state($key, $state);
3286 return $state;
3287 }
3288
3289 $started = $this->step_start_time();
3290
3291 $fh = fopen($state['source_file'], 'rb');
3292 if (!$fh) {
3293 $state = $this->proc_error($state, 'restore open source failed');
3294 $this->set_proc_state($key, $state);
3295 return $state;
3296 }
3297
3298 fseek($fh, (int)$state['file_pos']);
3299
3300 while ($this->step_time_left($started, (float)$state['step_maxsec'])) {
3301 $line = fgets($fh);
3302
3303 if ($line === false) {
3304 fclose($fh);
3305
3306 if (!empty($state['zip']) && isset($state['source_file']) && $state['source_file'] !== $state['file']) {
3307 @unlink($state['source_file']);
3308 }
3309
3310 $state = $this->proc_finish($state, 'restore finished');
3311 $this->set_proc_state($key, $state);
3312 return $state;
3313 }
3314
3315 $rec = json_decode(trim($line), true);
3316 if (!is_array($rec) || !isset($rec['records']) || !is_array($rec['records'])) {
3317 continue;
3318 }
3319
3320 foreach ($rec['records'] as $rowValues) {
3321 $assoc = [];
3322 foreach ($state['source_columns'] as $i => $col) {
3323 $assoc[$col] = $rowValues[$i] ?? null;
3324 }
3325
3326 if (is_array($state['mapping'])) {
3327 foreach ($state['mapping'] as $old => $new) {
3328 if ((string)$old === (string)$new) {
3329 continue;
3330 }
3331
3332 if (array_key_exists($old, $assoc)) {
3333 $assoc[$new] = $assoc[$old];
3334 unset($assoc[$old]);
3335 }
3336 }
3337 }
3338
3339 $dbRec = [];
3340 foreach ($assoc as $fld => $val) {
3341 if (isset($state['target_lookup'][$fld])) {
3342 $dbRec[$fld] = $this->normalize_restore_value(
3343 (string)($state['target_db_type'] ?? ''),
3344 (string)($state['target_types'][$fld] ?? ''),
3345 $val,
3346 (string)($state['source_types'][$fld] ?? '')
3347 );
3348 }
3349 }
3350
3351 if (isset($dbRec['id']) && ($dbRec['id'] === '' || $dbRec['id'] === null || $dbRec['id'] === 0 || $dbRec['id'] === '0')) {
3352 unset($dbRec['id']);
3353 }
3354
3355 if (!$dbRec) {
3356 continue;
3357 }
3358
3359 $fields = array_keys($dbRec);
3360 $placeholders = array_fill(0, count($fields), '?');
3361
3362 $sql = 'INSERT INTO ' . $this->quote_ident($state['server'], $state['table'])
3363 . ' (' . implode(',', array_map(fn($f) => $this->quote_ident($state['server'], $f), $fields)) . ')'
3364 . ' VALUES (' . implode(',', $placeholders) . ')';
3365
3366 $stmt = $this->db[$state['server']]->prepare($sql);
3367 $stmt->execute(array_values($dbRec));
3368
3369 $state['done']++;
3370 }
3371
3372 $state['file_pos'] = ftell($fh);
3373 $state['percent'] = ($state['total'] > 0) ? (int)floor(($state['done'] / $state['total']) * 100) : 0;
3374 $state['step_percent'] = $state['percent'];
3375 $state['message'] = 'restore rows ' . $state['done'] . ' / ' . $state['total'];
3376 }
3377
3378 fclose($fh);
3379 $this->set_proc_state($key, $state);
3380 return $this->proc_response($state);
3381 }
3382
3391 protected function normalize_restore_value(
3392 string $targetDbType,
3393 string $targetFieldType,
3394 mixed $value,
3395 string $sourceFieldType = ''
3396 ): mixed {
3397 $targetDbType = strtolower(trim($targetDbType));
3398 $targetFieldType = strtolower(trim($targetFieldType));
3399 $sourceFieldType = strtolower(trim($sourceFieldType));
3400 $temporalTypes = ['date', 'time', 'datetime', 'timestamp', 'year'];
3401 $effectiveFieldType = $targetFieldType;
3402
3403 // SQLite speichert Zeittypen technisch als TEXT. Beim Ruecktransfer
3404 // liefert deshalb der Quelltyp die noetige fachliche Information.
3405 if ($targetDbType === 'sqlite'
3406 && in_array($targetFieldType, ['', 'text', 'varchar'], true)
3407 && in_array($sourceFieldType, $temporalTypes, true)
3408 ) {
3409 $effectiveFieldType = $sourceFieldType;
3410 }
3411
3412 if ($targetDbType === 'mysql'
3413 && $value === ''
3414 && in_array($effectiveFieldType, $temporalTypes, true)
3415 ) {
3416 return null;
3417 }
3418
3419 if ($targetDbType === 'sqlite'
3420 && is_string($value)
3421 && in_array($effectiveFieldType, $temporalTypes, true)
3422 && preg_match('/^(.+)\.([0-9]{1,6})$/', $value, $match)
3423 ) {
3424 $fraction = rtrim($match[2], '0');
3425 if ($fraction !== '' && strlen($fraction) < 3) {
3426 $fraction = str_pad($fraction, 3, '0');
3427 }
3428 return $match[1] . ($fraction !== '' ? '.' . $fraction : '');
3429 }
3430
3431 return $value;
3432 }
3433
3434
3435 /* =====================================================
3436 * DD -> DB SYNC
3437 * ===================================================== */
3438
3451 protected function is_semantic_type_match(string $ddType, string $dbType): bool
3452 {
3453 $ddGroup = $this->schema_type_group($ddType);
3454 $dbGroup = $this->schema_type_group($dbType);
3455
3456 if ($ddGroup === $dbGroup) {
3457 return true;
3458 }
3459
3460 if (in_array($ddGroup, ['string', 'text'], true) && in_array($dbGroup, ['string', 'text'], true)) {
3461 return true;
3462 }
3463
3464 if (in_array($ddGroup, ['integer', 'bool'], true) && in_array($dbGroup, ['integer', 'bool'], true)) {
3465 return true;
3466 }
3467
3468 return false;
3469 }
3470
3479 protected function schema_type_group(string $type, string $length = ''): string
3480 {
3481 $type = strtolower(trim($type));
3482 $length = trim($length);
3483
3484 if ($type === '') {
3485 return 'unknown';
3486 }
3487
3488 if (in_array($type, ['bool', 'boolean'], true)) {
3489 return 'bool';
3490 }
3491
3492 if ($type === 'bit') {
3493 return ($length === '' || $length === '1') ? 'bool' : 'integer';
3494 }
3495
3496 if ($type === 'tinyint' && $length === '1') {
3497 return 'bool';
3498 }
3499
3500 if (in_array($type, ['int', 'integer', 'smallint', 'mediumint', 'tinyint', 'bigint', 'serial', 'number'], true)) {
3501 return 'integer';
3502 }
3503
3504 if (in_array($type, ['decimal', 'numeric'], true)) {
3505 return 'decimal';
3506 }
3507
3508 if (in_array($type, ['float', 'double', 'real'], true)) {
3509 return 'float';
3510 }
3511
3512 if ($type === 'date' || $type === 'year') {
3513 return 'date';
3514 }
3515
3516 if ($type === 'time') {
3517 return 'time';
3518 }
3519
3520 if (in_array($type, ['datetime', 'timestamp'], true)) {
3521 return 'datetime';
3522 }
3523
3524 if (in_array($type, ['char', 'nchar', 'varchar', 'varchar2', 'nvarchar', 'string'], true)) {
3525 return 'string';
3526 }
3527
3528 if (in_array($type, ['text', 'tinytext', 'mediumtext', 'longtext', 'clob'], true)) {
3529 return 'text';
3530 }
3531
3532 if (in_array($type, ['binary', 'varbinary', 'tinyblob', 'blob', 'mediumblob', 'longblob'], true)) {
3533 return 'binary';
3534 }
3535
3536 if ($type === 'json') {
3537 return 'json';
3538 }
3539
3540 if (in_array($type, ['enum', 'set'], true)) {
3541 return 'enum';
3542 }
3543
3544 return 'unknown';
3545 }
3546
3559 public function is_dd_field_compatible_with_db(array $ddField, array $dbField, string $dbEngine = ''): bool
3560 {
3561 $dbEngine = strtolower(trim($dbEngine));
3562 $ddType = strtolower((string)($ddField['type'] ?? ''));
3563 $dbType = strtolower((string)($dbField['type'] ?? ''));
3564 $ddGroup = $this->schema_type_group($ddType, (string)($ddField['length'] ?? ''));
3565 $dbGroup = $this->schema_type_group($dbType, (string)($dbField['length'] ?? ''));
3566
3567 if ($ddType !== '' && $dbType !== '' && ($ddType === $dbType || $this->is_semantic_type_match($ddType, $dbType))) {
3568 return true;
3569 }
3570
3571 if ($dbEngine === 'sqlite') {
3572 if (in_array($ddGroup, ['string', 'text', 'date', 'time', 'datetime', 'json', 'enum'], true)
3573 && in_array($dbGroup, ['string', 'text'], true)) {
3574 return true;
3575 }
3576
3577 if (in_array($ddGroup, ['integer', 'bool'], true) && in_array($dbGroup, ['integer', 'bool'], true)) {
3578 return true;
3579 }
3580
3581 if (in_array($ddGroup, ['decimal', 'float'], true) && in_array($dbGroup, ['decimal', 'float', 'integer'], true)) {
3582 return true;
3583 }
3584 }
3585
3586 return false;
3587 }
3588
3598 protected function is_db_sync_field_match(string $dbEngine, array $ddField, array $dbField): bool
3599 {
3600 $dbEngine = strtolower(trim($dbEngine));
3601 $ddType = strtolower((string)($ddField['type'] ?? ''));
3602 $dbType = strtolower((string)($dbField['type'] ?? ''));
3603
3604 if ($ddType === '' || $dbType === '') {
3605 return false;
3606 }
3607
3608 if ($dbEngine === 'sqlite') {
3609 return $this->is_dd_field_compatible_with_db($ddField, $dbField, $dbEngine);
3610 }
3611
3612 if ($ddType === $dbType) {
3613 return true;
3614 }
3615
3616 $ddGroup = $this->schema_type_group($ddType, (string)($ddField['length'] ?? ''));
3617 $dbGroup = $this->schema_type_group($dbType, (string)($dbField['length'] ?? ''));
3618
3619 if (in_array($ddGroup, ['integer', 'bool'], true) && in_array($dbGroup, ['integer', 'bool'], true)) {
3620 return true;
3621 }
3622
3623 if ($ddGroup === 'decimal' && $dbGroup === 'decimal') {
3624 return true;
3625 }
3626
3627 if ($ddGroup === 'float' && in_array($dbGroup, ['float', 'decimal'], true)) {
3628 return true;
3629 }
3630
3631 return false;
3632 }
3633
3643 public function merge_dd_field_with_db_field(array $oldField, array $dbField, string $dbEngine = ''): array
3644 {
3645 $compatible = $this->is_dd_field_compatible_with_db($oldField, $dbField, $dbEngine);
3646 $merged = $dbField;
3647
3648 $preserve = $compatible
3649 ? ['type', 'index', 'length', 'default', 'label', 'rules', 'tooltip', 'errormsg', 'placeholder', 'convert', 'protect', 'group', 'mask', 'data', 'options', 'tpl', 'js']
3650 : ['label', 'tooltip', 'errormsg', 'placeholder', 'convert', 'protect', 'group', 'mask', 'data', 'js'];
3651
3652 foreach ($preserve as $key) {
3653 if (isset($oldField[$key]) && $oldField[$key] !== '' && $oldField[$key] !== null) {
3654 $merged[$key] = $oldField[$key];
3655 }
3656 }
3657
3658 $merged['name'] = $dbField['name'] ?? ($oldField['name'] ?? '');
3659
3660 if (empty($merged['label'])) {
3661 $merged['label'] = $this->infer_label_from_name((string)$merged['name']);
3662 }
3663
3664 if (empty($merged['rules']) || !$compatible) {
3665 $merged['rules'] = $this->infer_rules_from_field($merged);
3666 }
3667
3668 if (empty($merged['tpl']) || !$compatible) {
3669 $merged['tpl'] = $this->infer_tpl_from_field($merged);
3670 }
3671
3672 return $this->normalize_field_record($merged);
3673 }
3674
3687 protected function build_sync_plan_dd_to_db(string $dd): array
3688 {
3689 $model = $this->get_dd_model($dd);
3690
3691 $plan = [
3692 'ok' => 1,
3693 'dd' => $dd,
3694 'server' => $model['table']['server'] ?? '',
3695 'table' => $model['table']['table'] ?? '',
3696 'table_exists' => 0,
3697 'create_table' => 0,
3698 'add_fields' => [],
3699 'missing_in_dd' => [],
3700 'type_conflicts' => [],
3701 'add_indexes' => [],
3702 'rebuild_needed' => 0,
3703 'backup_file' => '',
3704 ];
3705
3706 if (!$model) {
3707 $plan['ok'] = 0;
3708 return $plan;
3709 }
3710
3711 $server = $plan['server'];
3712 $table = $plan['table'];
3713 $ddFields = $model['fields'] ?? [];
3714 $ddIndexes = $model['indexes'] ?? [];
3715
3716 if (!$server || !$table) {
3717 $plan['ok'] = 0;
3718 return $plan;
3719 }
3720
3721 $tableExists = $this->get_table_exist($server, $table) ? 1 : 0;
3722 $plan['table_exists'] = $tableExists;
3723
3724 if (!$tableExists) {
3725 $plan['create_table'] = 1;
3726 return $plan;
3727 }
3728
3729 $dbFields = $this->get_db_fields($server, $table);
3730 $dbIndexes = $this->get_db_indexes($server, $table);
3731
3732 $dbFieldsByName = [];
3733 foreach ($dbFields as $field) {
3734 $dbFieldsByName[strtolower($field['name'])] = $field;
3735 }
3736
3737 $ddFieldsByName = [];
3738 foreach ($ddFields as $field) {
3739 $ddFieldsByName[strtolower($field['name'])] = $field;
3740 }
3741
3742 $targetDbType = $this->get_db_type($server);
3743
3744 foreach ($ddFields as $field) {
3745 $name = strtolower((string)$field['name']);
3746 if (!isset($dbFieldsByName[$name])) {
3747 $plan['add_fields'][] = $field;
3748 continue;
3749 }
3750
3751 $dbf = $dbFieldsByName[$name];
3752
3753 $ddType = strtolower((string)($field['type'] ?? ''));
3754 $dbType = strtolower((string)($dbf['type'] ?? ''));
3755
3756 $ddLen = (string)($field['length'] ?? '');
3757 $dbLen = (string)($dbf['length'] ?? '');
3758
3759 $typeEqual = $this->is_db_sync_field_match($targetDbType, $field, $dbf);
3760 $lenEqual = ($ddLen === $dbLen) || ($ddLen === '' || $dbLen === '');
3761
3767 if ($targetDbType === 'sqlite') {
3768 if ($typeEqual) {
3769 $lenEqual = true;
3770 }
3771 }
3772
3773 if (!$typeEqual || !$lenEqual) {
3774 $plan['type_conflicts'][] = [
3775 'field' => $field['name'],
3776 'dd_type' => $ddType,
3777 'db_type' => $dbType,
3778 'dd_length' => $ddLen,
3779 'db_length' => $dbLen,
3780 ];
3781 }
3782 }
3783
3784 foreach ($dbFields as $field) {
3785 $name = strtolower((string)$field['name']);
3786 if (!isset($ddFieldsByName[$name])) {
3787 $plan['missing_in_dd'][] = $field;
3788 }
3789 }
3790
3791 $dbIndexNames = [];
3792 foreach ($dbIndexes as $idx) {
3793 $dbIndexNames[strtolower((string)$idx['name'])] = true;
3794 }
3795
3796 foreach ($ddIndexes as $idx) {
3797 $name = strtolower((string)($idx['name'] ?? ''));
3798 if (!$name) {
3799 continue;
3800 }
3801
3802 if (strtoupper((string)($idx['type'] ?? 'INDEX')) === 'PRIMARY') {
3803 continue;
3804 }
3805
3806 if (!isset($dbIndexNames[$name])) {
3807 $plan['add_indexes'][] = $idx;
3808 }
3809 }
3810
3811 $plan['rebuild_needed'] = (!empty($plan['type_conflicts']) || !empty($plan['missing_in_dd'])) ? 1 : 0;
3812
3813 return $plan;
3814 }
3815
3838 public function sync_dd_to_db($modul, $dd, $mode = 'step'): array
3839 {
3840 $mode = strtolower((string)$mode);
3841 if ($mode === '') {
3842 $mode = 'step';
3843 }
3844 if ($mode === 'step') {
3845 $mode = 'apply';
3846 }
3847
3848 $ddRef = ($modul && strpos((string)$dd, '|') === false) ? $modul . '|' . $dd : (string)$dd;
3849 $key = $this->proc_key('sync_dd_to_db', [$modul, $dd]);
3850
3851 if ($mode === 'reset') {
3852 $this->clear_proc_state($key);
3853 return [
3854 'proc_key' => $key,
3855 'status' => 'reset',
3856 'message' => 'sync state cleared',
3857 'percent' => 0,
3858 'step_percent' => 0,
3859 ];
3860 }
3861
3862 if ($mode === 'status') {
3863 $state = $this->get_proc_state($key);
3864 return $state ?: [
3865 'proc_key' => $key,
3866 'status' => 'new',
3867 'message' => 'no active state',
3868 'percent' => 0,
3869 'step_percent' => 0,
3870 ];
3871 }
3872
3873 if (in_array($mode, ['pause', 'resume', 'continue', 'cancel'], true)) {
3874 return $this->control_proc_state($key, $mode);
3875 }
3876
3877 if ($mode === 'restart') {
3878 $this->clear_proc_state($key);
3879 $mode = 'apply';
3880 }
3881
3882 $plan = $this->build_sync_plan_dd_to_db($ddRef);
3883
3884 if (in_array($mode, ['check', 'plan'], true)) {
3885 $plan['status'] = 'finished';
3886 $plan['message'] = 'plan ready';
3887 $plan['percent'] = 100;
3888 $plan['step_percent'] = 100;
3889 return $plan;
3890 }
3891
3892 $state = $this->get_proc_state($key);
3893
3894 if (!$state) {
3895 $state = $this->init_proc_state('sync_dd_to_db', $key, [
3896 'mode' => $mode,
3897 'dd' => $dd,
3898 'dd_ref' => $ddRef,
3899 'modul' => $modul,
3900 'server' => $plan['server'],
3901 'table' => $plan['table'],
3902 'phase' => 'prepare',
3903 'plan' => $plan,
3904 'field_pos' => 0,
3905 'index_pos' => 0,
3906 'percent' => 0,
3907 'message' => 'sync initialized',
3908 'mapping' => $this->get_schema_mapping_values('dd_to_db', [
3909 'modul' => $modul,
3910 'dd' => $dd,
3911 'server' => $plan['server'],
3912 'table' => $plan['table'],
3913 ]),
3914 'backup_started' => 0,
3915 'restore_started' => 0,
3916 ]);
3917 }
3918
3919 if ($this->proc_is_waiting($state)) {
3920 return $this->proc_response($state);
3921 }
3922
3923 $server = $state['server'];
3924 $table = $state['table'];
3925 $plan = $state['plan'];
3926
3927 switch ($state['phase']) {
3928 case 'prepare':
3929 if (!empty($plan['create_table'])) {
3930 $state['phase'] = 'create_table';
3931 $state['message'] = 'create table';
3932 $state['percent'] = 10;
3933 $state['step_percent'] = 0;
3934 $this->set_proc_state($key, $state);
3935 return $this->proc_response($state);
3936 }
3937
3938 if (!empty($plan['rebuild_needed'])) {
3939 if (!in_array($state['mode'], ['force', 'rebuild'], true)) {
3940 $state = $this->proc_error($state, 'rebuild needed; use mode force or rebuild');
3941 $this->set_proc_state($key, $state);
3942 return $state;
3943 }
3944
3945 $state['phase'] = 'backup_old';
3946 $state['message'] = 'backup old table';
3947 $state['percent'] = 10;
3948 $state['step_percent'] = 0;
3949 $this->set_proc_state($key, $state);
3950 return $this->proc_response($state);
3951 }
3952
3953 $state['phase'] = 'add_fields';
3954 $state['message'] = 'add missing fields';
3955 $state['percent'] = 20;
3956 $state['step_percent'] = 0;
3957 $this->set_proc_state($key, $state);
3958 return $this->proc_response($state);
3959
3960 case 'create_table':
3961 if ($this->create_db_tab($state['dd_ref'] ?? $ddRef)) {
3962 $state['phase'] = 'add_indexes';
3963 $state['message'] = 'create indexes';
3964 $state['percent'] = 70;
3965 $state['step_percent'] = 100;
3966 } else {
3967 $state = $this->proc_error($state, 'create table failed');
3968 }
3969 $this->set_proc_state($key, $state);
3970 return $this->proc_response($state);
3971
3972 case 'add_fields':
3973 $started = $this->step_start_time();
3974 $fields = $plan['add_fields'] ?? [];
3975
3976 while ($state['field_pos'] < count($fields) && $this->step_time_left($started, (float)$state['step_maxsec'])) {
3977 $field = $fields[$state['field_pos']];
3979
3980 if (!$ok) {
3981 $state = $this->proc_error($state, 'add field failed: ' . ($field['name'] ?? ''));
3982 $this->set_proc_state($key, $state);
3983 return $state;
3984 }
3985
3986 $state['field_pos']++;
3987 $max = max(1, count($fields));
3988 $state['percent'] = 20 + (int)floor(($state['field_pos'] / $max) * 40);
3989 $state['step_percent'] = (int)floor(($state['field_pos'] / $max) * 100);
3990 $state['message'] = 'add field ' . ($field['name'] ?? '');
3991 }
3992
3993 if ($state['field_pos'] >= count($fields)) {
3994 $state['phase'] = 'add_indexes';
3995 $state['message'] = 'add indexes';
3996 $state['percent'] = 70;
3997 $state['step_percent'] = 100;
3998 }
3999
4000 $this->set_proc_state($key, $state);
4001 return $this->proc_response($state);
4002
4003 case 'add_indexes':
4004 $started = $this->step_start_time();
4005 $indexes = $plan['add_indexes'] ?? [];
4006
4007 while ($state['index_pos'] < count($indexes) && $this->step_time_left($started, (float)$state['step_maxsec'])) {
4008 $idx = $indexes[$state['index_pos']];
4009 $ok = $this->create_db_index($server, $table, $idx);
4010
4011 if (!$ok) {
4012 $state = $this->proc_error($state, 'add index failed: ' . ($idx['name'] ?? ''));
4013 $this->set_proc_state($key, $state);
4014 return $state;
4015 }
4016
4017 $state['index_pos']++;
4018 $max = max(1, count($indexes));
4019 $state['percent'] = 70 + (int)floor(($state['index_pos'] / $max) * 20);
4020 $state['step_percent'] = (int)floor(($state['index_pos'] / $max) * 100);
4021 $state['message'] = 'add index ' . ($idx['name'] ?? '');
4022 }
4023
4024 if ($state['index_pos'] >= count($indexes)) {
4025 $state = $this->proc_finish($state, 'sync dd -> db finished');
4026 }
4027
4028 $this->set_proc_state($key, $state);
4029 return $this->proc_response($state);
4030
4031 case 'backup_old':
4032 $backupFile = $state['plan']['backup_file'] ?? '';
4033 if (!$backupFile) {
4035 $state['plan']['backup_file'] = $backupFile;
4036 }
4037
4038 $bak = $this->backup($server, $table, $backupFile, 1);
4039 if (($bak['status'] ?? '') === 'error') {
4040 $state = $this->proc_error($state, 'backup before rebuild failed');
4041 $this->set_proc_state($key, $state);
4042 return $state;
4043 }
4044
4045 if (($bak['status'] ?? '') !== 'finished') {
4046 $state['percent'] = 10 + (int)floor((($bak['percent'] ?? 0) * 0.3));
4047 $state['step_percent'] = (int)($bak['percent'] ?? 0);
4048 $state['message'] = 'backup old table';
4049 $this->set_proc_state($key, $state);
4050 return $this->proc_response($state);
4051 }
4052
4053 $state['phase'] = 'rename_old';
4054 $state['percent'] = 45;
4055 $state['step_percent'] = 100;
4056 $state['message'] = 'rename old table';
4057 $this->set_proc_state($key, $state);
4058 return $this->proc_response($state);
4059
4060 case 'rename_old':
4061 $tmpOld = $table . '__dbxold_' . date('YmdHis');
4062 $sql = 'ALTER TABLE ' . $this->quote_ident($server, $table)
4063 . ' RENAME TO ' . $this->quote_ident($server, $tmpOld);
4064
4065 if (!$this->exec_query($server, $sql)) {
4066 $state = $this->proc_error($state, 'rename old table failed');
4067 $this->set_proc_state($key, $state);
4068 return $state;
4069 }
4070
4071 $state['old_table'] = $tmpOld;
4072 $state['phase'] = 'create_new';
4073 $state['percent'] = 55;
4074 $state['step_percent'] = 100;
4075 $state['message'] = 'create new table';
4076 $this->set_proc_state($key, $state);
4077 return $this->proc_response($state);
4078
4079 case 'create_new':
4080 if (!$this->create_db_tab($state['dd_ref'] ?? $ddRef)) {
4081 $state = $this->proc_error($state, 'create new table failed');
4082 $this->set_proc_state($key, $state);
4083 return $state;
4084 }
4085
4086 $state['phase'] = 'restore_new';
4087 $state['percent'] = 65;
4088 $state['step_percent'] = 100;
4089 $state['message'] = 'restore data into new table';
4090 $this->set_proc_state($key, $state);
4091 return $this->proc_response($state);
4092
4093 case 'restore_new':
4094 $mapping = is_array($state['mapping'] ?? null) ? $state['mapping'] : [];
4095 $restore = $this->restore($server, $table, $state['plan']['backup_file'], $mapping, 1);
4096
4097 if (($restore['status'] ?? '') === 'error') {
4098 $state = $this->proc_error($state, 'restore into new table failed');
4099 $this->set_proc_state($key, $state);
4100 return $state;
4101 }
4102
4103 if (($restore['status'] ?? '') !== 'finished') {
4104 $state['percent'] = 65 + (int)floor((($restore['percent'] ?? 0) * 0.25));
4105 $state['step_percent'] = (int)($restore['percent'] ?? 0);
4106 $state['message'] = 'restore data into new table';
4107 $this->set_proc_state($key, $state);
4108 return $this->proc_response($state);
4109 }
4110
4111 $state['phase'] = 'drop_old';
4112 $state['percent'] = 92;
4113 $state['step_percent'] = 100;
4114 $state['message'] = 'drop old table';
4115 $this->set_proc_state($key, $state);
4116 return $this->proc_response($state);
4117
4118 case 'drop_old':
4119 if (!empty($state['old_table'])) {
4120 $this->drop_db_tab($server, $state['old_table']);
4121 }
4122
4123 $state = $this->proc_finish($state, 'sync dd -> db rebuild finished');
4124 $this->set_proc_state($key, $state);
4125 return $state;
4126 }
4127
4128 $state = $this->proc_error($state, 'unknown sync phase');
4129 $this->set_proc_state($key, $state);
4130 return $state;
4131 }
4132
4133
4134 /* =====================================================
4135 * DB -> DD SYNC
4136 * ===================================================== */
4137
4187 public function sync_db_to_dd($modul, $dd, $mode = 'step', string $server = '', string $table = ''): array
4188 {
4189 $mode = strtolower((string)$mode);
4190 if ($mode === '') {
4191 $mode = 'step';
4192 }
4193 if ($mode === 'step' || $mode === 'apply') {
4194 $mode = 'merge';
4195 }
4196
4197 $server = trim($server);
4198 $table = trim($table);
4199 $key = $this->proc_key('sync_db_to_dd', [$modul, $dd, $server, $table]);
4200
4201 if ($mode === 'reset') {
4202 $this->clear_proc_state($key);
4203 return [
4204 'proc_key' => $key,
4205 'status' => 'reset',
4206 'message' => 'sync state cleared',
4207 'percent' => 0,
4208 'step_percent' => 0,
4209 ];
4210 }
4211
4212 if ($mode === 'status') {
4213 $state = $this->get_proc_state($key);
4214 return $state ?: [
4215 'proc_key' => $key,
4216 'status' => 'new',
4217 'message' => 'no active state',
4218 'percent' => 0,
4219 'step_percent' => 0,
4220 ];
4221 }
4222
4223 if (in_array($mode, ['pause', 'resume', 'continue', 'cancel'], true)) {
4224 return $this->control_proc_state($key, $mode);
4225 }
4226
4227 if ($mode === 'restart') {
4228 $this->clear_proc_state($key);
4229 $mode = 'merge';
4230 }
4231
4232 $state = $this->get_proc_state($key);
4233
4234 if (!$state) {
4235 $oldModel = [];
4236 $syncServer = $server ?: 'dbXsystem';
4237 $syncTable = $table ?: $dd;
4238
4239 if ($this->get_dd_exist($modul . '|' . $dd) || $this->get_dd_exist($dd)) {
4240 $oldModel = $this->get_dd_model($modul . '|' . $dd);
4241 if (!$oldModel) {
4242 $oldModel = $this->get_dd_model($dd);
4243 }
4244
4245 if (!$server) {
4246 $syncServer = $oldModel['table']['server'] ?? $syncServer;
4247 }
4248 if (!$table) {
4249 $syncTable = $oldModel['table']['table'] ?? $syncTable;
4250 }
4251 }
4252
4253 if (!$this->get_table_exist($syncServer, $syncTable)) {
4254 return $this->proc_error(['proc_key' => $key], 'table not found');
4255 }
4256
4257 $state = $this->init_proc_state('sync_db_to_dd', $key, [
4258 'mode' => $mode,
4259 'modul' => $modul,
4260 'dd' => $dd,
4261 'server' => $syncServer,
4262 'table' => $syncTable,
4263 'old_model' => $oldModel,
4264 'mapping' => $this->get_schema_mapping_values('db_to_dd', [
4265 'modul' => $modul,
4266 'dd' => $dd,
4267 'server' => $syncServer,
4268 'table' => $syncTable,
4269 ]),
4270 'phase' => 'read_schema',
4271 'percent' => 0,
4272 'message' => 'sync initialized',
4273 ]);
4274 }
4275
4276 if ($this->proc_is_waiting($state)) {
4277 return $this->proc_response($state);
4278 }
4279
4280 switch ($state['phase']) {
4281 case 'read_schema':
4282 $state['db_fields'] = $this->get_db_fields($state['server'], $state['table']);
4283 $state['db_indexes'] = $this->get_db_indexes($state['server'], $state['table']);
4284 $state['phase'] = 'merge_meta';
4285 $state['percent'] = 30;
4286 $state['step_percent'] = 100;
4287 $state['message'] = 'schema loaded';
4288 $this->set_proc_state($key, $state);
4289 return $this->proc_response($state);
4290
4291 case 'merge_meta':
4292 $oldModel = $state['old_model'] ?? [];
4293
4294 $newTable = $this->normalize_table_record($state['dd'], [
4295 'server' => $state['server'],
4296 'table' => $state['table'],
4297 ]);
4298
4299 if ($state['mode'] === 'merge' && $oldModel) {
4300 foreach ([
4301 'autosync','version','cache','trash','trace',
4302 'read','create','update','delete',
4303 'read_owner','create_owner','update_owner','delete_owner'
4304 ] as $k) {
4305 if (isset($oldModel['table'][$k])) {
4306 $newTable[$k] = $oldModel['table'][$k];
4307 }
4308 }
4309 }
4310
4311 $oldFieldsByName = [];
4312 foreach (($oldModel['fields'] ?? []) as $field) {
4313 $oldFieldsByName[strtolower((string)($field['name'] ?? ''))] = $field;
4314 }
4315
4316 $mapping = is_array($state['mapping'] ?? null) ? $state['mapping'] : [];
4317 $newFields = [];
4318 foreach (($state['db_fields'] ?? []) as $field) {
4319 $name = $field['name'];
4320 $metaName = $mapping[$name] ?? $name;
4321 $metaKey = strtolower((string)$metaName);
4322 if (!isset($oldFieldsByName[$metaKey])) {
4323 $metaName = $name;
4324 $metaKey = strtolower((string)$metaName);
4325 }
4326
4327 if ($state['mode'] === 'merge' && isset($oldFieldsByName[$metaKey])) {
4328 $newFields[] = $this->merge_dd_field_with_db_field(
4329 $oldFieldsByName[$metaKey],
4330 $field,
4331 $this->get_db_type($state['server'])
4332 );
4333 } else {
4334 $newFields[] = $this->normalize_field_record($field);
4335 }
4336 }
4337
4338 $oldIndexesByName = [];
4339 foreach (($oldModel['indexes'] ?? []) as $index) {
4340 $oldIndexesByName[$index['name']] = $index;
4341 }
4342
4343 $newIndexes = [];
4344 foreach (($state['db_indexes'] ?? []) as $index) {
4345 $name = $index['name'];
4346
4347 if ($state['mode'] === 'merge' && isset($oldIndexesByName[$name])) {
4348 $merged = $index;
4349 foreach ($oldIndexesByName[$name] as $k => $v) {
4350 if ($v !== '' && $v !== null) {
4351 $merged[$k] = $v;
4352 }
4353 }
4354 $newIndexes[] = $this->normalize_index_record($merged);
4355 } else {
4356 $newIndexes[] = $this->normalize_index_record($index);
4357 }
4358 }
4359
4360 $state['new_table'] = $newTable;
4361 $state['new_fields'] = $newFields;
4362 $state['new_indexes'] = $newIndexes;
4363 $state['phase'] = 'write_dd';
4364 $state['percent'] = 70;
4365 $state['step_percent'] = 100;
4366 $state['message'] = 'meta merged';
4367 $this->set_proc_state($key, $state);
4368 return $this->proc_response($state);
4369
4370 case 'write_dd':
4371 $ok = $this->write_dd(
4372 $state['modul'],
4373 $state['dd'],
4374 $state['new_table'] ?? [],
4375 $state['new_fields'] ?? [],
4376 $state['new_indexes'] ?? []
4377 );
4378
4379 if (!$ok) {
4380 $state = $this->proc_error($state, 'write dd failed');
4381 $this->set_proc_state($key, $state);
4382 return $state;
4383 }
4384
4385 $this->clear_dd_cache($state['modul'] . '|' . $state['dd']);
4386
4387 $state = $this->proc_finish($state, 'sync db -> dd finished');
4388 $this->set_proc_state($key, $state);
4389 return $state;
4390 }
4391
4392 $state = $this->proc_error($state, 'unknown sync phase');
4393 $this->set_proc_state($key, $state);
4394 return $state;
4395 }
4396
4424 public function dync_db_to_dd($modul, $dd, $mode = 'step', string $server = '', string $table = ''): array
4425 {
4426 return $this->sync_db_to_dd($modul, $dd, $mode, $server, $table);
4427 }
4428
4429
4430 /* =====================================================
4431 * LIST
4432 * ===================================================== */
4433
4446 public function get_dd_tables($path = ''): array
4447 {
4448 $records = [];
4449
4450 if (!$path) {
4451 $path = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/dbx/dd/');
4452 }
4453
4454 if (!is_dir($path)) {
4455 return $records;
4456 }
4457
4458 $files = scandir($path);
4459 foreach ($files as $file) {
4460 if (!str_ends_with($file, '.dd.php')) {
4461 continue;
4462 }
4463
4464 $dd = str_replace('.dd.php', '', $file);
4465 if ($dd === 'new') {
4466 continue;
4467 }
4468
4469 $dd_sys = $this->load_dd($dd);
4470 if (($dd_sys['dd_status'] ?? 0) <= 0) {
4471 continue;
4472 }
4473
4474 $server = $this->get_dd_server($dd);
4475 $table = $this->get_dd_table($dd);
4476 $exist = $this->get_table_exist($server, $table) ? 1 : 0;
4477 $count = $exist ? $this->count($dd) : -1;
4478
4479 $records[] = [
4480 'datadic' => $dd,
4481 'server' => $server,
4482 'table' => $table,
4483 'exist' => $exist,
4484 'count' => $count,
4485 'sync' => $this->get_dd_autosync($dd),
4486 ];
4487 }
4488
4489 return $records;
4490 }
4491}
$table['server']
Definition .dd.php:6
$indexes[]
Definition .dd.php:167
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 lokal wirksamen Server einer Datenbeschreibung.
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.
normalize_restore_value(string $targetDbType, string $targetFieldType, mixed $value, string $sourceFieldType='')
Normalisiert treiberspezifisch problematische Restore-Werte.
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.
get_base_dir(int $cutData=0)
Liefert das Basisverzeichnis der Installation.
Definition dbxApi.php:1507
get_file_dir()
Liefert das files/-Verzeichnis der Installation.
Definition dbxApi.php:1524
if( $base !==$expectedBase) if($files !==$expectedBase . 'files/') $stored
foreach(array('bootstrapRowColumns', 'setBootstrapColumnLayout', 'addBootstrapColumn', 'dissolveBootstrapColumns',) as $function) $keys
DBX schema administration.
foreach($pages as $page) $targetRows