PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.11.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.11.0
2.11.0 2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 All 78 releases
← All changes | vendor/wpfluent/framework/src/WPFluent/Database/Schema.php +573 -63 1.0.932.11.0 View file →
@@ -8,11 +8,18 @@
8 8 {
9 9 use MaintainsDatabase;
10 10
11 11 /**
12 + * Keep track of custom tables when unit testing
13 + *
14 + * @var array
15 + */
16 + public static $customTempTables = [];
17 +
18 + /**
12 19 * Get the global $wpdb instance
13 20 *
14 - * @return global $wpdb instance
21 + * @return \wpdb The global $wpdb
15 22 */
16 23 public static function db()
17 24 {
18 25 return $GLOBALS['wpdb'];
@@ -26,17 +33,36 @@
26 33 public static function getInfo($key = null)
27 34 {
28 35 $db = static::db();
29 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 +
30 52 $info = [
31 - 'dbname' => $db->dbname,
32 - 'prefix' => $db->prefix,
33 - 'dbhost' => $db->dbhost,
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
34 59 'username' => $db->dbuser,
60 + // @phpstan-ignore-next-line
35 61 'password' => $db->dbpassword,
36 62 'charset' => $db->charset,
37 63 'collation' => $db->collate,
38 - 'tables' => static::getTableList()
64 + 'tables' => static::getTableList(),
39 65 ];
40 66
41 67 return $key ? $info[$key] : $info;
42 68 }
@@ -53,11 +79,11 @@
53 79 public static function migrate($table, $sql = null)
54 80 {
55 81 if (!$sql && is_array($table)) {
56 82 $result = [];
57 - foreach ($table as $t => $sql) {
83 + foreach ($table as $t => $s) {
58 84 $result = array_merge(
59 - $result, (array) static::createTable($t, $sql)
85 + $result, (array) static::createTable($t, $s)
60 86 );
61 87 }
62 88 return $result;
63 89 } else {
@@ -124,16 +150,18 @@
124 150 // error and then turn off the error from being shown, so
125 151 // error will be not shown if there is no temporary
126 152 // table, then restore the error state.
127 153 $isErrorSuppressed = $wpdb->suppress_errors;
128 -
154 +
129 155 $wpdb->suppress_errors = true;
130 156
131 - $result = static::query("SELECT 1 FROM %{$table}% WHERE 0");
157 + static::query("SELECT 1 FROM %{$table}% WHERE 0");
132 158
159 + $hasError = !empty($wpdb->last_error);
160 +
133 161 $wpdb->suppress_errors = $isErrorSuppressed;
134 162
135 - return $result === 0;
163 + return !$hasError;
136 164 }
137 165
138 166 /**
139 167 * Resolves the table prefix and makes the table name with prefix
@@ -176,8 +204,36 @@
176 204
177 205 return $sql;
178 206 }
179 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 +
180 236 /**
181 237 * Creates a new table using dbDelta function or alters the table if exists.
182 238 *
183 239 * @param string $table The table name without the prefix
@@ -189,10 +245,14 @@
189 245 public static function createTable($table, $sql)
190 246 {
191 247 $table = static::table($table);
192 248
193 - $sql = static::sql($sql);
249 + $sql = static::cleanUp($sql);
194 250
251 + if (static::isSqlite()) {
252 + $sql = preg_replace('/\bjson\b/i', 'longtext', $sql);
253 + }
254 +
195 255 $collate = static::db()->get_charset_collate();
196 256
197 257 return static::callDBDelta(
198 258 "CREATE TABLE $table (
@@ -218,13 +278,13 @@
218 278 }
219 279
220 280 /**
221 281 * Alters an existing table
222 - *
282 + *
223 283 * @param string $table The table name without the prefix
224 284 * @param string $sql The sql to create table or an absolute path of a
225 285 * .sql file containing the column definations for creating the new table.
226 - *
286 + *
227 287 * @return string message
228 288 */
229 289 public static function alterTable($table, $sql)
230 290 {
@@ -229,14 +289,27 @@
229 289 public static function alterTable($table, $sql)
230 290 {
231 291 $table = static::table($table);
232 292
233 - $sql = static::sql($sql);
293 + $sql = static::cleanUp($sql);
234 294
235 - $sql = array_map(function($i) { return trim($i);}, explode(',', $sql));
236 -
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 +
237 310 $sql = "ALTER TABLE $table ".PHP_EOL.rtrim(
238 - trim(implode(','.PHP_EOL, $sql)), ';'
311 + trim(implode(','.PHP_EOL, $parts)), ';'
239 312 ).";";
240 313
241 314 return static::query($sql);
242 315 }
@@ -241,8 +314,42 @@
241 314 return static::query($sql);
242 315 }
243 316
244 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 + /**
245 352 * Alters an existing table using dbDelta function if exists, otherwise creates
246 353 * it. Alters an existing table but takes the table creation column defination.
247 354 * This is because, the dbDelta functioin can create or update a table using
248 355 * the table creation defination. In this, case, if a table exists and the
@@ -260,52 +367,141 @@
260 367 * @return string message
261 368 */
262 369 public static function updateTable($table, $sql)
263 370 {
264 - $result = static::createTable(
265 - $table, implode(",\n", array_map('trim', explode(',', $sql)))
266 - );
371 + $sql = static::cleanUp($sql);
267 372
268 - if ($existingColumns = static::getColumns($table)) {
373 + if (static::isSqlite()) {
374 + $sql = preg_replace('/\bjson\b/i', 'longtext', $sql);
375 + }
269 376
270 - $columns = array_map(function($l) {
271 - if (preg_match('/(\S+)/', $l, $matches)) return $matches[1];
272 - }, array_map('trim', explode(',', $sql)));
377 + $columnsDefinitions = array_map('trim', static::splitAlterClauses($sql));
378 + $schemaSql = implode(",\n", $columnsDefinitions);
273 379
274 - foreach ($existingColumns as $column) {
275 - if (!in_array($column, $columns)) {
276 - $tbl = static::table($table);
277 - static::query("alter table $tbl drop column $column");
278 - $tblColumn = $tbl.'.'.$column;
279 - $result[$tblColumn] = "Dropped column {$tblColumn}";
280 - }
281 - }
282 - }
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 + }
283 386
284 - return $result;
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;
285 473 }
286 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 +
287 487 /**
288 488 * Drops/deletes an existing table if exists
289 489 *
290 490 * @param string $table The table name without the prefix
291 - * @param bool $disableForeignKeyCheck Optional. Whether to disable foreign key checks before dropping the table. Default is true. *
491 + * @param bool $disableForeignKeyCheck Optional.
292 492 * @return bool
293 493 */
294 -
295 494 public static function dropTableIfExists($table, $disableForeignKeyCheck = true)
296 495 {
297 496 if (static::hasTable($table)) {
298 - if ($disableForeignKeyCheck) {
299 - static::db()->query("ALTER TABLE " . static::table($table) . " DISABLE KEYS;");
300 - }
301 - return static::db()->query('DROP TABLE ' . static::table($table));
497 + return static::dropTable($table, $disableForeignKeyCheck);
302 498 }
303 499 }
304 500
305 501 /**
306 502 * Truncate a table.
307 - *
503 + *
308 504 * @param string $table
309 505 * @return bool
310 506 */
311 507 public static function truncate($table)
@@ -311,14 +507,23 @@
311 507 public static function truncate($table)
312 508 {
313 509 $table = static::table($table);
314 510
315 - return static::db()->query("TRUNCATE TABLE $table");
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;
316 521 }
317 522
318 523 /**
319 524 * Truncate a table if exists.
320 - *
525 + *
321 526 * @param string $table
322 527 * @return bool
323 528 */
324 529 public static function truncateTableIfExists($table)
@@ -350,43 +555,100 @@
350 555 * @see https://developer.wordpress.org/reference/functions/drop_index
351 556 */
352 557 public static function dropIndex($table, $index)
353 558 {
559 + if (static::isSqlite()) {
560 + $tbl = static::table($table);
561 + return static::db()->query("ALTER TABLE {$tbl} DROP INDEX {$index}");
562 + }
563 +
354 564 return drop_index(static::table($table), $index);
355 565 }
356 566
357 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 + /**
358 616 * Makes raw query and can resolve the table name from the query
359 617 * and can form a full table name including the table prefix if
360 618 * the table name is wrapped like: %table_name% in the query.
361 619 *
362 - * @param straing $query
620 + * @param string $query
363 621 * @return mixed
364 622 */
365 623 public static function query($query)
366 624 {
367 - if (preg_match('/%.*%/', $query, $m)) {
368 - $query = str_replace(
369 - $m[0], static::table(trim($m[0], '%')), $query
370 - );
371 - }
625 + $query = preg_replace_callback('/(?<![\'"])%([a-zA-Z0-9_-]+)%(?![\'"])/', function ($matches) {
626 + return static::table($matches[1]);
627 + }, $query);
372 628
629 + if (preg_match('/^(SELECT|SHOW|DESCRIBE|EXPLAIN)\s+/i', trim($query))) {
630 + return static::db()->get_results($query, OBJECT);
631 + }
632 +
373 633 return static::db()->query($query);
374 634 }
375 635
376 636 /**
377 637 * Get a list of all columns from the given table name.
378 - *
638 + *
379 639 * @param string $table The table name without the prefix
380 - * @return array
640 + * @return array|null
381 641 */
382 642 public static function getColumns($table)
383 643 {
384 - if (static::hasTable($table)) {
385 - return static::db()->get_col(
386 - 'DESC ' . static::table($table), 0
387 - );
644 + if (!static::hasTable($table)) {
645 + return null;
388 646 }
647 +
648 + return static::db()->get_col(
649 + 'DESCRIBE ' . static::table($table), 0
650 + );
389 651 }
390 652
391 653 /**
392 654 * Gets a list of all columns including column information
@@ -393,14 +655,52 @@
393 655 *
394 656 * @param string $table The table name without the prefix
395 657 * @return array
396 658 */
397 - public static function getColumnsWithTypes($table)
659 + protected static function protectedGetColumnsWithTypes($table)
398 660 {
399 661 if (!static::hasTable($table)) return;
400 -
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
401 701 $db = static::db()->dbname;
402 - $table = static::table($table);
702 +
403 703 $fields = [
404 704 'COLUMN_NAME',
405 705 'ORDINAL_POSITION',
406 706 'COLUMN_DEFAULT',
@@ -411,9 +711,9 @@
411 711 'NUMERIC_SCALE',
412 712 'COLUMN_KEY',
413 713 'EXTRA',
414 714 ];
415 -
715 +
416 716 $sql = "SELECT " . implode(',', $fields) . " FROM INFORMATION_SCHEMA.COLUMNS";
417 717 $sql .= " WHERE TABLE_NAME = '".$table."' AND TABLE_SCHEMA = '".$db."'";
418 718
419 719 return array_map(function($i) {
@@ -421,12 +721,112 @@
421 721 foreach ((array) $i as $key => $value) {
422 722 $item[strtolower($key)] = $value;
423 723 }
424 724 return $item;
425 - }, static::db()->get_results($sql));
725 + }, (array) static::db()->get_results($sql));
426 726 }
427 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 +
428 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 + /**
429 829 * Retrieves the list of all available built-in tables from
430 830 * the database using WordPress' $wpdb->tables native method.
431 831 *
432 832 * @param string $scope
@@ -459,18 +859,107 @@
459 859 * @return array
460 860 */
461 861 public static function getTableList($dbname = null)
462 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
463 872 $dbname = $dbname ?: static::db()->dbname;
464 873 $sql = "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES";
465 874 $sql .= " WHERE TABLE_SCHEMA = '".$dbname."'";
466 -
875 +
467 876 return array_map(function($i) {
468 877 return $i->TABLE_NAME;
469 - }, static::db()->get_results($sql));
878 + }, (array) static::db()->get_results($sql));
470 879 }
471 880
472 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 + /**
473 962 * Retrieve the current database engine name.
474 963 *
475 964 * @param string $table
476 965 * @return string
@@ -476,8 +965,12 @@
476 965 * @return string
477 966 */
478 967 public static function getEngine($table)
479 968 {
969 + if (static::isSqlite()) {
970 + return 'sqlite';
971 + }
972 +
480 973 return static::db()->get_var(
481 974 'SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = "' . static::table($table) . '"'
482 975 );
483 976 }
@@ -493,7 +986,24 @@
493 986 if (!function_exists('dbDelta')) {
494 987 require (ABSPATH . 'wp-admin/includes/upgrade.php');
495 988 }
496 989
497 - return dbDelta($sql);
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;
498 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 + }
499 1009 }