PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.14
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.14
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / vendor / wpfluent / framework / src / WPFluent / Database / Schema.php

Schema.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.14, at vendor/wpfluent/framework/src/WPFluent/Database/Schema.php

1,010 lines 27.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentForm\Framework\Database;
4
5 use FluentForm\Framework\Database\Concerns\MaintainsDatabase;
6
7 class Schema
8 {
9 use MaintainsDatabase;
10
11 /**
12 * Keep track of custom tables when unit testing
13 *
14 * @var array
15 */
16 public static $customTempTables = [];
17
18 /**
19 * Get the global $wpdb instance
20 *
21 * @return \wpdb The global $wpdb
22 */
23 public static function db()
24 {
25 return $GLOBALS['wpdb'];
26 }
27
28 /**
29 * Get schema/db information
30 *
31 * @return string|array
32 */
33 public static function getInfo($key = null)
34 {
35 $db = static::db();
36
37 if (static::isSqlite()) {
38 $info = [
39 'dbname' => 'sqlite',
40 'prefix' => $db->prefix,
41 'dbhost' => '',
42 'username' => '',
43 'password' => '',
44 'charset' => 'utf8',
45 'collation' => '',
46 'tables' => static::getTableList(),
47 ];
48
49 return $key ? ($info[$key] ?? null) : $info;
50 }
51
52 $info = [
53 // @phpstan-ignore-next-line
54 'dbname' => $db->dbname,
55 'prefix' => $db->prefix,
56 // @phpstan-ignore-next-line
57 'dbhost' => $db->dbhost,
58 // @phpstan-ignore-next-line
59 'username' => $db->dbuser,
60 // @phpstan-ignore-next-line
61 'password' => $db->dbpassword,
62 'charset' => $db->charset,
63 'collation' => $db->collate,
64 'tables' => static::getTableList(),
65 ];
66
67 return $key ? $info[$key] : $info;
68 }
69
70 /**
71 * Migrates database table(s)
72 *
73 * @param string|array $table The table name without prefix
74 * or an array where each key is table name and value is sql.
75 *
76 * @param string $sql Optional
77 * @return mixed
78 */
79 public static function migrate($table, $sql = null)
80 {
81 if (!$sql && is_array($table)) {
82 $result = [];
83 foreach ($table as $t => $s) {
84 $result = array_merge(
85 $result, (array) static::createTable($t, $s)
86 );
87 }
88 return $result;
89 } else {
90 return static::createTable($table, $sql);
91 }
92 }
93
94 /**
95 * Creates a new table if doesn't exist using dbDelta function
96 *
97 * @param string $table The table name without the prefix
98 * @param string $sql The sql to create table or an absolute path of a
99 * .sql file containing the column definations for creating the new table.
100 *
101 * @return string Message
102 */
103 public static function createTableIfNotExist($table, $sql)
104 {
105 if (!static::hasTable($table)) {
106 return static::createTable($table, $sql);
107 }
108 }
109
110 /**
111 * Checks if a column exists in a table
112 *
113 * @param string $column The column name of the table
114 * @param string $table The table name without prefix
115 * @return boolean
116 */
117
118 public static function hasColumn($column, $table)
119 {
120 $wpdb = static::db();
121
122 $table = static::table($table);
123
124 $columns = $wpdb->get_col("DESCRIBE $table");
125
126 return in_array($column, $columns);
127 }
128
129 /**
130 * Checks if a table exists
131 *
132 * @param string $table The table name without prefix
133 * @return boolean
134 */
135 public static function hasTable($table)
136 {
137 $wpdb = static::db();
138
139 $table = static::table($table);
140
141 $result = $wpdb->get_var("SHOW TABLES LIKE '" . $table . "'") == $table;
142
143 if ($result) {
144 return $result;
145 }
146
147 // Check if any temporary table exists by this table name.
148
149 // At first, store the original state of the error suppress
150 // error and then turn off the error from being shown, so
151 // error will be not shown if there is no temporary
152 // table, then restore the error state.
153 $isErrorSuppressed = $wpdb->suppress_errors;
154
155 $wpdb->suppress_errors = true;
156
157 static::query("SELECT 1 FROM %{$table}% WHERE 0");
158
159 $hasError = !empty($wpdb->last_error);
160
161 $wpdb->suppress_errors = $isErrorSuppressed;
162
163 return !$hasError;
164 }
165
166 /**
167 * Resolves the table prefix and makes the table name with prefix
168 *
169 * @param string $table The table name without the prefix
170 * @return string The resolved table name with the prefix
171 */
172 public static function table($table)
173 {
174 $wpdb = static::db();
175
176 $prefix = $wpdb->prefix;
177
178 if (strpos($table, $prefix) === 0) {
179 return $table;
180 }
181
182 return isset($wpdb->{$table}) ? $wpdb->{$table} : ($wpdb->prefix.$table);
183 }
184
185
186 /**
187 * Resolves the sql prefix
188 *
189 * @param string $sql The file name or the raw sql
190 * @return string The resolved sql
191 */
192 public static function sql($sql = '')
193 {
194 $allowedSqlFileFormats = [
195 'sql'
196 ];
197
198 foreach ($allowedSqlFileFormats as $format) {
199 if (str_ends_with($sql, '.' . $format)) {
200 $sql = @file_exists($sql) ? file_get_contents($sql) : $sql;
201 break;
202 }
203 }
204
205 return $sql;
206 }
207
208 /**
209 * Clean the sql string by removing the comments.
210 *
211 * @param string $sql
212 * @return string
213 */
214 public static function cleanUp($sql)
215 {
216 $sql = static::sql($sql);
217
218 // Remove inline -- comments
219 $sql = preg_replace('/--.*$/m', '', $sql);
220
221 // Remove inline # comments
222 $sql = preg_replace('/#.*$/m', '', $sql);
223
224 // Remove block /* ... */
225 $sql = preg_replace('/\/\*.*?\*\//s', '', $sql);
226
227 // Collapse multiple spaces & newlines
228 // $sql = preg_replace('/\s+/', ' ', $sql);
229
230 // Remove dangling commas before closing parenthesis
231 $sql = preg_replace('/,\s*\)/', ')', $sql);
232
233 return trim($sql);
234 }
235
236 /**
237 * Creates a new table using dbDelta function or alters the table if exists.
238 *
239 * @param string $table The table name without the prefix
240 * @param string $sql The sql to create table or an absolute path of a
241 * .sql file containing the column definations for creating the new table.
242 *
243 * @return string message
244 */
245 public static function createTable($table, $sql)
246 {
247 $table = static::table($table);
248
249 $sql = static::cleanUp($sql);
250
251 if (static::isSqlite()) {
252 $sql = preg_replace('/\bjson\b/i', 'longtext', $sql);
253 }
254
255 $collate = static::db()->get_charset_collate();
256
257 return static::callDBDelta(
258 "CREATE TABLE $table (
259 ".PHP_EOL.trim(trim($sql), ',').PHP_EOL."
260 ) $collate;"
261 );
262 }
263
264 /**
265 * Alters an existing table if exists
266 *
267 * @param string $table The table name without the prefix
268 * @param string $sql The sql to create table or an absolute path of a
269 * .sql file containing the column definations for creating the new table.
270 *
271 * @return string message
272 */
273 public static function alterTableIfExists($table, $sql)
274 {
275 if (static::hasTable($table)) {
276 return static::alterTable($table, $sql);
277 }
278 }
279
280 /**
281 * Alters an existing table
282 *
283 * @param string $table The table name without the prefix
284 * @param string $sql The sql to create table or an absolute path of a
285 * .sql file containing the column definations for creating the new table.
286 *
287 * @return string message
288 */
289 public static function alterTable($table, $sql)
290 {
291 $table = static::table($table);
292
293 $sql = static::cleanUp($sql);
294
295 if (static::isSqlite()) {
296 $sql = preg_replace('/\bjson\b/i', 'longtext', $sql);
297 }
298
299 $parts = array_filter(array_map('trim', static::splitAlterClauses($sql)));
300
301 if (static::isSqlite()) {
302 // SQLite only supports one ALTER TABLE operation per statement.
303 $result = null;
304 foreach ($parts as $part) {
305 $result = static::query("ALTER TABLE $table {$part};");
306 }
307 return $result;
308 }
309
310 $sql = "ALTER TABLE $table ".PHP_EOL.rtrim(
311 trim(implode(','.PHP_EOL, $parts)), ';'
312 ).";";
313
314 return static::query($sql);
315 }
316
317 /**
318 * Split a comma-separated ALTER TABLE clause list, respecting parentheses
319 * so that DECIMAL(10,2) and similar types are not split mid-definition.
320 *
321 * @param string $sql
322 * @return string[]
323 */
324 protected static function splitAlterClauses($sql)
325 {
326 $parts = [];
327 $depth = 0;
328 $current = '';
329
330 for ($i = 0, $len = strlen($sql); $i < $len; $i++) {
331 $char = $sql[$i];
332 if ($char === '(') {
333 $depth++;
334 } elseif ($char === ')') {
335 $depth--;
336 } elseif ($char === ',' && $depth === 0) {
337 $parts[] = $current;
338 $current = '';
339 continue;
340 }
341 $current .= $char;
342 }
343
344 if ($current !== '') {
345 $parts[] = $current;
346 }
347
348 return $parts;
349 }
350
351 /**
352 * Alters an existing table using dbDelta function if exists, otherwise creates
353 * it. Alters an existing table but takes the table creation column defination.
354 * This is because, the dbDelta functioin can create or update a table using
355 * the table creation defination. In this, case, if a table exists and the
356 * columns are matched then nothing happens but if there's any difference
357 * in the new sql then the dbDelta alters the table using the new sql
358 * defination but doesn't delete any columns. So, after the dbDelta
359 * finishes it's job, any non-existing columns in the new sql
360 * defination will be deleted from the existing table. if
361 * table is not there then the table gets created.
362 *
363 * @param string $table The table name without the prefix
364 * @param string $sql The sql to create table or an absolute path of a
365 * .sql file containing the column definations for creating the new table.
366 *
367 * @return string message
368 */
369 public static function updateTable($table, $sql)
370 {
371 $sql = static::cleanUp($sql);
372
373 if (static::isSqlite()) {
374 $sql = preg_replace('/\bjson\b/i', 'longtext', $sql);
375 }
376
377 $columnsDefinitions = array_map('trim', static::splitAlterClauses($sql));
378 $schemaSql = implode(",\n", $columnsDefinitions);
379
380 // Extract desired column names (skip constraint/index clauses)
381 $columns = [];
382 foreach ($columnsDefinitions as $definition) {
383 if (preg_match('/^(?:PRIMARY\s+KEY|UNIQUE(?:(?:\s+KEY|\s+INDEX))?|KEY|INDEX|CONSTRAINT|FOREIGN\s+KEY|FULLTEXT|SPATIAL)\b/i', ltrim($definition))) {
384 continue;
385 }
386
387 if (preg_match('/^`?(\w+)`?\s+/i', $definition, $matches)) {
388 $columns[$matches[1]] = $definition;
389 }
390 }
391
392 $tbl = static::table($table);
393
394 // SQLite cannot drop PRIMARY KEY columns or columns referenced by indexes
395 // via native ALTER TABLE DROP COLUMN.
396 //
397 // Workarounds:
398 // - Indexed columns: drop the index first with DROP INDEX,
399 // then drop the column.
400 // - PRIMARY KEY columns: use CHANGE COLUMN to rebuild the table without
401 // the PK attribute (renaming the column to a temp name), then drop
402 // the renamed column.
403 if (static::isSqlite()) {
404 // Collect PRIMARY KEY columns and per-column index key names.
405 $indexRows = (array) static::db()->get_results("SHOW INDEX FROM {$tbl}");
406 $pkColumns = [];
407 $columnIndexes = []; // column_name => [key_name, ...]
408 foreach ($indexRows as $row) {
409 if ($row->Key_name === 'PRIMARY') {
410 $pkColumns[] = $row->Column_name;
411 } else {
412 $columnIndexes[$row->Column_name][] = $row->Key_name;
413 }
414 }
415
416 $existingColumns = static::getColumns($table) ?: [];
417
418 // 1. Add any desired columns not yet in the table.
419 foreach ($columns as $colName => $definition) {
420 if (!in_array($colName, $existingColumns)) {
421 static::db()->query("ALTER TABLE {$tbl} ADD COLUMN {$definition}");
422 }
423 }
424
425 // 2. Drop every column that is not in the desired schema.
426 foreach ($existingColumns as $column) {
427 if (isset($columns[$column])) {
428 continue; // desired — keep it
429 }
430
431 // Drop non-primary indexes referencing this column first, otherwise
432 // native SQLite ALTER TABLE DROP COLUMN fails.
433 foreach ($columnIndexes[$column] ?? [] as $keyName) {
434 static::db()->query("ALTER TABLE {$tbl} DROP INDEX {$keyName}");
435 }
436
437 if (in_array($column, $pkColumns)) {
438 // Native SQLite ALTER TABLE DROP COLUMN rejects PRIMARY KEY columns.
439 // Use CHANGE COLUMN to trigger an internal table rebuild that strips
440 // the PK attribute, then drop the (now ordinary) renamed column.
441 $tmpCol = $column . '_wpf_drop';
442 static::db()->query("ALTER TABLE {$tbl} CHANGE COLUMN {$column} {$tmpCol} INT NULL");
443 static::db()->query("ALTER TABLE {$tbl} DROP COLUMN {$tmpCol}");
444 } else {
445 static::db()->query("ALTER TABLE {$tbl} DROP COLUMN {$column}");
446 }
447 }
448
449 return [$tbl => "Updated table structure"];
450 }
451
452 // MySQL path: use dbDelta to create/update, then drop extra columns.
453 $result = static::createTable($table, $schemaSql);
454 $existingColumns = static::getColumns($table) ?: [];
455
456 // Add missing columns
457 foreach ($columns as $colName => $definition) {
458 if (!in_array($colName, $existingColumns)) {
459 static::query("ALTER TABLE $tbl ADD COLUMN $definition");
460 $result[$tbl.'.'.$colName] = "Added column {$tbl}.{$colName}";
461 }
462 }
463
464 // Drop extra columns
465 foreach ($existingColumns as $column) {
466 if (!isset($columns[$column])) {
467 static::query("ALTER TABLE $tbl DROP COLUMN $column");
468 $result[$tbl.'.'.$column] = "Dropped column {$tbl}.{$column}";
469 }
470 }
471
472 return $result;
473 }
474
475 /**
476 * Drops/deletes an existing table.
477 *
478 * @param string $table The table name without the prefix
479 * @param bool $disableForeignKeyCheck Optional.
480 * @return bool
481 */
482 public static function dropTable($table, $disableForeignKeyCheck = true)
483 {
484 return static::db()->query('DROP TABLE ' . static::table($table));
485 }
486
487 /**
488 * Drops/deletes an existing table if exists
489 *
490 * @param string $table The table name without the prefix
491 * @param bool $disableForeignKeyCheck Optional.
492 * @return bool
493 */
494 public static function dropTableIfExists($table, $disableForeignKeyCheck = true)
495 {
496 if (static::hasTable($table)) {
497 return static::dropTable($table, $disableForeignKeyCheck);
498 }
499 }
500
501 /**
502 * Truncate a table.
503 *
504 * @param string $table
505 * @return bool
506 */
507 public static function truncate($table)
508 {
509 $table = static::table($table);
510
511 $result = static::db()->query("TRUNCATE TABLE $table");
512
513 if (static::isSqlite()) {
514 // SQLite translates TRUNCATE TABLE to DELETE FROM, which does NOT reset
515 // the auto-increment counter. Manually clear the sqlite_sequence row so
516 // that the next INSERT starts at id=1 (matching MySQL TRUNCATE behaviour).
517 static::db()->query("DELETE FROM sqlite_sequence WHERE name='{$table}'");
518 }
519
520 return $result;
521 }
522
523 /**
524 * Truncate a table if exists.
525 *
526 * @param string $table
527 * @return bool
528 */
529 public static function truncateTableIfExists($table)
530 {
531 if (static::hasTable($table)) {
532 return static::truncate($table);
533 }
534 }
535
536 /**
537 * Adds a new index to a column of given table.
538 *
539 * @param string $table Table name
540 * @param string $index Columns name
541 * @return bool
542 * @see https://developer.wordpress.org/reference/functions/add_clean_index
543 */
544 public static function addIndex($table, $index)
545 {
546 return add_clean_index(static::table($table), $index);
547 }
548
549 /**
550 * Drops an index from a column of given table.
551 *
552 * @param string $table Table name
553 * @param string $index Columns name
554 * @return bool
555 * @see https://developer.wordpress.org/reference/functions/drop_index
556 */
557 public static function dropIndex($table, $index)
558 {
559 if (static::isSqlite()) {
560 $tbl = static::table($table);
561 return static::db()->query("ALTER TABLE {$tbl} DROP INDEX {$index}");
562 }
563
564 return drop_index(static::table($table), $index);
565 }
566
567 /**
568 * Add a FULLTEXT index to the given columns, required by the query
569 * builder's relevance ranking (selectRelevance/orderByRelevance and the
570 * Searchable relevanceSearch scope). No-ops on SQLite, which has no
571 * native FULLTEXT support and degrades to LIKE-based matching.
572 *
573 * @param string $table Table name without prefix
574 * @param string|array $columns
575 * @param string|null $name Optional index name
576 * @return mixed
577 */
578 public static function fullText($table, $columns, $name = null)
579 {
580 if (static::isSqlite()) {
581 return false;
582 }
583
584 $columns = is_array($columns)
585 ? $columns
586 : array_map('trim', explode(',', $columns));
587
588 $tbl = static::table($table);
589 $name = $name ?: ('ft_' . implode('_', $columns));
590 $list = implode(', ', $columns);
591
592 return static::db()->query(
593 "ALTER TABLE {$tbl} ADD FULLTEXT {$name} ({$list})"
594 );
595 }
596
597 /**
598 * Drop a FULLTEXT index by name.
599 *
600 * @param string $table Table name without prefix
601 * @param string $name Index name
602 * @return mixed
603 */
604 public static function dropFullText($table, $name)
605 {
606 if (static::isSqlite()) {
607 return false;
608 }
609
610 $tbl = static::table($table);
611
612 return static::db()->query("ALTER TABLE {$tbl} DROP INDEX {$name}");
613 }
614
615 /**
616 * Makes raw query and can resolve the table name from the query
617 * and can form a full table name including the table prefix if
618 * the table name is wrapped like: %table_name% in the query.
619 *
620 * @param string $query
621 * @return mixed
622 */
623 public static function query($query)
624 {
625 $query = preg_replace_callback('/(?<![\'"])%([a-zA-Z0-9_-]+)%(?![\'"])/', function ($matches) {
626 return static::table($matches[1]);
627 }, $query);
628
629 if (preg_match('/^(SELECT|SHOW|DESCRIBE|EXPLAIN)\s+/i', trim($query))) {
630 return static::db()->get_results($query, OBJECT);
631 }
632
633 return static::db()->query($query);
634 }
635
636 /**
637 * Get a list of all columns from the given table name.
638 *
639 * @param string $table The table name without the prefix
640 * @return array|null
641 */
642 public static function getColumns($table)
643 {
644 if (!static::hasTable($table)) {
645 return null;
646 }
647
648 return static::db()->get_col(
649 'DESCRIBE ' . static::table($table), 0
650 );
651 }
652
653 /**
654 * Gets a list of all columns including column information
655 *
656 * @param string $table The table name without the prefix
657 * @return array
658 */
659 protected static function protectedGetColumnsWithTypes($table)
660 {
661 if (!static::hasTable($table)) return;
662
663 $table = static::table($table);
664
665 if (static::isSqlite()) {
666 // INFORMATION_SCHEMA is not available in SQLite.
667 // Use DESCRIBE which the WP SQLite plugin translates with
668 // proper MySQL type mapping from its data type cache.
669 $columns = (array) static::db()->get_results("DESCRIBE {$table}");
670 return array_map(function ($col, $pos) {
671 $extra = $col->Extra ?? '';
672 // SQLite INTEGER PRIMARY KEY is auto_increment but DESCRIBE
673 // doesn't report it in Extra — detect from Key + Type.
674 if (empty($extra) && ($col->Key ?? '') === 'PRI') {
675 $type = strtoupper($col->Type ?? '');
676 if (in_array($type, ['INTEGER', 'BIGINT', 'BIGINT(20)', 'BIGINT(20) UNSIGNED', 'INT'])) {
677 $extra = 'auto_increment';
678 }
679 }
680
681 return [
682 'column_name' => $col->Field,
683 'ordinal_position' => $pos + 1,
684 'column_default' => $col->Default,
685 'is_nullable' => ($col->Null === 'YES' ? 'YES' : 'NO'),
686 'data_type' => strtolower(
687 preg_replace('/\(.*/', '', $col->Type)
688 ),
689 'character_maximum_length' => preg_match(
690 '/\((\d+)\)/', $col->Type, $matches
691 ) ? (int)$matches[1] : null,
692 'numeric_precision' => null,
693 'numeric_scale' => null,
694 'column_key' => $col->Key ?? '',
695 'extra' => $extra,
696 ];
697 }, $columns, array_keys($columns));
698 }
699
700 // @phpstan-ignore-next-line
701 $db = static::db()->dbname;
702
703 $fields = [
704 'COLUMN_NAME',
705 'ORDINAL_POSITION',
706 'COLUMN_DEFAULT',
707 'IS_NULLABLE',
708 'DATA_TYPE',
709 'CHARACTER_MAXIMUM_LENGTH',
710 'NUMERIC_PRECISION',
711 'NUMERIC_SCALE',
712 'COLUMN_KEY',
713 'EXTRA',
714 ];
715
716 $sql = "SELECT " . implode(',', $fields) . " FROM INFORMATION_SCHEMA.COLUMNS";
717 $sql .= " WHERE TABLE_NAME = '".$table."' AND TABLE_SCHEMA = '".$db."'";
718
719 return array_map(function($i) {
720 $item = [];
721 foreach ((array) $i as $key => $value) {
722 $item[strtolower($key)] = $value;
723 }
724 return $item;
725 }, (array) static::db()->get_results($sql));
726 }
727
728 public static function getColumnsWithTypes($table)
729 {
730 $columns = static::protectedGetColumnsWithTypes($table);
731
732 if (!empty($columns)) {
733 return $columns;
734 }
735
736 if (static::isSqlite()) {
737 return [];
738 }
739
740 $columns = static::db()->get_results(
741 'SHOW COLUMNS FROM `'.static::table($table).'`'
742 );
743
744 return array_map(function ($col) {
745 return [
746 'column_name' => $col->Field,
747 'ordinal_position' => null,
748 'column_default' => $col->Default,
749 'is_nullable' => ($col->Null === 'YES' ? 'YES' : 'NO'),
750 'data_type' => strtolower(
751 preg_replace('/\(.*/', '', $col->Type)
752 ),
753 'character_maximum_length' => preg_match(
754 '/\((\d+)\)/', $col->Type, $matches
755 ) ? (int)$matches[1] : null,
756 'numeric_precision' => null,
757 'numeric_scale' => null,
758 'column_key' => $col->Key,
759 'extra' => $col->Extra,
760 ];
761 }, (array) $columns);
762 }
763
764 /**
765 * Gets a list of all columns including column information
766 *
767 * @param string $table The table name without the prefix
768 * @return array
769 */
770 public static function describeTable($table)
771 {
772 return static::getColumnsWithTypes($table);
773 }
774
775 /**
776 * Gets a list of all foreign keys from the given table name.
777 *
778 * @param string $table
779 * @return array
780 */
781 public static function getTableForeignKeys($table)
782 {
783 $table = static::table($table);
784
785 if (static::isSqlite()) {
786 // Parse foreign keys from the CREATE TABLE statement in sqlite_master
787 // since PRAGMA queries cannot go through $wpdb.
788 $row = static::db()->get_row(
789 "SELECT sql FROM sqlite_master WHERE type='table' AND name='{$table}'"
790 );
791 if (!$row || empty($row->sql)) {
792 return [];
793 }
794 $fks = [];
795 if (preg_match_all(
796 '/FOREIGN\s+KEY\s*\(\s*[`"]?(\w+)[`"]?\s*\)\s*REFERENCES\s+[`"]?(\w+)[`"]?\s*\(\s*[`"]?(\w+)[`"]?\s*\)/i',
797 $row->sql, $matches, PREG_SET_ORDER
798 )) {
799 foreach ($matches as $m) {
800 $fks[] = [
801 'column_name' => $m[1],
802 'referenced_table' => $m[2],
803 'referenced_column' => $m[3],
804 ];
805 }
806 }
807 return $fks;
808 }
809
810 // @phpstan-ignore-next-line
811 $db = static::db()->dbname;
812
813 $sql = "SELECT COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
814 FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
815 WHERE TABLE_NAME = '{$table}'
816 AND TABLE_SCHEMA = '{$db}'
817 AND REFERENCED_TABLE_NAME IS NOT NULL";
818
819 return array_map(function ($i) {
820 return [
821 'column_name' => $i->COLUMN_NAME,
822 'referenced_table' => $i->REFERENCED_TABLE_NAME,
823 'referenced_column' => $i->REFERENCED_COLUMN_NAME,
824 ];
825 }, (array) static::db()->get_results($sql));
826 }
827
828 /**
829 * Retrieves the list of all available built-in tables from
830 * the database using WordPress' $wpdb->tables native method.
831 *
832 * @param string $scope
833 * @param boolean $prefix
834 * @param integer $blogId
835 * @return string[] WP Table names. When a prefix is requested,
836 * the key is the unprefixed table name.
837 * @see https://developer.wordpress.org/reference/classes/wpdb/tables/
838 */
839 public static function tables($scope = 'all', $prefix = true, $blogId = 0)
840 {
841 return static::db()->tables($scope, $prefix, $blogId);
842 }
843
844 /**
845 * Retrieves the list of all available tables from the database.
846 *
847 * @param string $dbname optional
848 * @return array
849 */
850 public static function getTables($dbname = null)
851 {
852 return static::getTableList($dbname);
853 }
854
855 /**
856 * Retrieves the list of all available tables in the database.
857 *
858 * @param string $dbname optional
859 * @return array
860 */
861 public static function getTableList($dbname = null)
862 {
863 if (static::isSqlite()) {
864 return array_map(function ($i) {
865 return $i->name;
866 }, (array) static::db()->get_results(
867 "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
868 ));
869 }
870
871 // @phpstan-ignore-next-line
872 $dbname = $dbname ?: static::db()->dbname;
873 $sql = "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES";
874 $sql .= " WHERE TABLE_SCHEMA = '".$dbname."'";
875
876 return array_map(function($i) {
877 return $i->TABLE_NAME;
878 }, (array) static::db()->get_results($sql));
879 }
880
881 /**
882 * Retrieves the list of all available views in the database.
883 *
884 * @param string $dbname optional
885 * @return array
886 */
887 public static function getViews($dbname = null)
888 {
889 if (static::isSqlite()) {
890 return array_map(function ($i) {
891 return $i->name;
892 }, (array) static::db()->get_results(
893 "SELECT name FROM sqlite_master WHERE type='view'"
894 ));
895 }
896
897 // @phpstan-ignore-next-line
898 $dbname = $dbname ?: static::db()->dbname;
899 $sql = "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.VIEWS";
900 $sql .= " WHERE TABLE_SCHEMA = '".$dbname."'";
901
902 return array_map(function ($i) {
903 return $i->TABLE_NAME;
904 }, (array) static::db()->get_results($sql));
905 }
906
907 /**
908 * Retrieves the SQL definition of a specific view.
909 *
910 * @param string $name The view name
911 * @param string $dbname optional
912 * @return string|null
913 */
914 public static function getView($name, $dbname = null)
915 {
916 if (static::isSqlite()) {
917 $result = static::db()->get_row(
918 "SELECT sql FROM sqlite_master WHERE type='view' AND name='".$name."'"
919 );
920
921 return $result ? $result->sql : null;
922 }
923
924 // @phpstan-ignore-next-line
925 $dbname = $dbname ?: static::db()->dbname;
926 $result = static::db()->get_row(
927 "SELECT VIEW_DEFINITION FROM INFORMATION_SCHEMA.VIEWS"
928 . " WHERE TABLE_SCHEMA = '".$dbname."'"
929 . " AND TABLE_NAME = '".$name."'"
930 );
931
932 return $result ? $result->VIEW_DEFINITION : null;
933 }
934
935 /**
936 * Determine if the connected database is a sqlite database.
937 *
938 * @return bool
939 */
940 public static function isSqlite()
941 {
942 return defined('DB_ENGINE') && DB_ENGINE === 'sqlite';
943 }
944
945 /**
946 * Determine if the connected database is a mariadb database.
947 *
948 * @return bool
949 */
950 public static function isMaria()
951 {
952 if (static::isSqlite()) {
953 return false;
954 }
955
956 return str_contains(
957 static::db()->get_var('SELECT VERSION()'), 'MariaDB'
958 );
959 }
960
961 /**
962 * Retrieve the current database engine name.
963 *
964 * @param string $table
965 * @return string
966 */
967 public static function getEngine($table)
968 {
969 if (static::isSqlite()) {
970 return 'sqlite';
971 }
972
973 return static::db()->get_var(
974 'SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = "' . static::table($table) . '"'
975 );
976 }
977
978 /**
979 * The wrapper for calling dbDelta function
980 *
981 * @param string $sql
982 * @return mixed
983 */
984 public static function callDBDelta($sql)
985 {
986 if (!function_exists('dbDelta')) {
987 require (ABSPATH . 'wp-admin/includes/upgrade.php');
988 }
989
990 $result = dbDelta($sql);
991
992 if (php_sapi_name() === 'cli') {
993 $key = array_key_first($result);
994 if ($key && !str_contains($key, '.')) {
995 static::$customTempTables[] = $key;
996 }
997 }
998
999 return $result;
1000 }
1001
1002 /**
1003 * Helper to get the driver-specific JSON type.
1004 */
1005 public static function jsonType()
1006 {
1007 return static::isSqlite() ? 'longtext' : 'json';
1008 }
1009 }
1010