PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.1.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.1.0
2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 All 42 releases
← All changes | vendor/wpfluent/framework/src/WPFluent/Database/Schema.php +645 -83 1.352.1.0 View file →
@@ -1,14 +1,25 @@
1 1 <?php
2 2
3 3 namespace FluentBoards\Framework\Database;
4 4
5 +use FluentBoards\Framework\Database\Concerns\MaintainsDatabase;
6 +
5 7 class Schema
6 8 {
9 + use MaintainsDatabase;
10 +
7 11 /**
12 + * Keep track of custom tables when unit testing
13 + *
14 + * @var array
15 + */
16 + public static $customTempTables = [];
17 +
18 + /**
8 19 * Get the global $wpdb instance
9 20 *
10 - * @return global $wpdb instance
21 + * @return \wpdb The global $wpdb
11 22 */
12 23 public static function db()
13 24 {
14 25 return $GLOBALS['wpdb'];
@@ -22,17 +33,36 @@
22 33 public static function getInfo($key = null)
23 34 {
24 35 $db = static::db();
25 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 +
26 52 $info = [
27 - 'dbname' => $db->dbname,
28 - 'prefix' => $db->prefix,
29 - '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
30 59 'username' => $db->dbuser,
60 + // @phpstan-ignore-next-line
31 61 'password' => $db->dbpassword,
32 62 'charset' => $db->charset,
33 63 'collation' => $db->collate,
34 - 'tables' => static::getTableList()
64 + 'tables' => static::getTableList(),
35 65 ];
36 66
37 67 return $key ? $info[$key] : $info;
38 68 }
@@ -49,11 +79,11 @@
49 79 public static function migrate($table, $sql = null)
50 80 {
51 81 if (!$sql && is_array($table)) {
52 82 $result = [];
53 - foreach ($table as $t => $sql) {
83 + foreach ($table as $t => $s) {
54 84 $result = array_merge(
55 - $result, (array) static::createTable($t, $sql)
85 + $result, (array) static::createTable($t, $s)
56 86 );
57 87 }
58 88 return $result;
59 89 } else {
@@ -120,16 +150,18 @@
120 150 // error and then turn off the error from being shown, so
121 151 // error will be not shown if there is no temporary
122 152 // table, then restore the error state.
123 153 $isErrorSuppressed = $wpdb->suppress_errors;
124 -
154 +
125 155 $wpdb->suppress_errors = true;
126 156
127 - $result = static::query("SELECT 1 FROM %{$table}% WHERE 0");
157 + static::query("SELECT 1 FROM %{$table}% WHERE 0");
128 158
159 + $hasError = !empty($wpdb->last_error);
160 +
129 161 $wpdb->suppress_errors = $isErrorSuppressed;
130 162
131 - return $result === 0;
163 + return !$hasError;
132 164 }
133 165
134 166 /**
135 167 * Resolves the table prefix and makes the table name with prefix
@@ -149,8 +181,59 @@
149 181
150 182 return isset($wpdb->{$table}) ? $wpdb->{$table} : ($wpdb->prefix.$table);
151 183 }
152 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 +
153 236 /**
154 237 * Creates a new table using dbDelta function or alters the table if exists.
155 238 *
156 239 * @param string $table The table name without the prefix
@@ -162,18 +245,21 @@
162 245 public static function createTable($table, $sql)
163 246 {
164 247 $table = static::table($table);
165 248
166 - $sql = @file_exists($sql) ? file_get_contents($sql) : $sql;
249 + $sql = static::cleanUp($sql);
167 250
251 + if (static::isSqlite()) {
252 + $sql = preg_replace('/\bjson\b/i', 'longtext', $sql);
253 + }
254 +
168 255 $collate = static::db()->get_charset_collate();
169 256
170 - if ($sql && !str_contains(basename($sql), '.')) {
171 - return static::callDBDelta(
172 - $table,
173 - "CREATE TABLE $table (".PHP_EOL.trim(trim($sql), ',').PHP_EOL.") $collate;"
174 - );
175 - }
257 + return static::callDBDelta(
258 + "CREATE TABLE $table (
259 + ".PHP_EOL.trim(trim($sql), ',').PHP_EOL."
260 + ) $collate;"
261 + );
176 262 }
177 263
178 264 /**
179 265 * Alters an existing table if exists
@@ -192,13 +278,13 @@
192 278 }
193 279
194 280 /**
195 281 * Alters an existing table
196 - *
282 + *
197 283 * @param string $table The table name without the prefix
198 284 * @param string $sql The sql to create table or an absolute path of a
199 285 * .sql file containing the column definations for creating the new table.
200 - *
286 + *
201 287 * @return string message
202 288 */
203 289 public static function alterTable($table, $sql)
204 290 {
@@ -203,31 +289,80 @@
203 289 public static function alterTable($table, $sql)
204 290 {
205 291 $table = static::table($table);
206 292
207 - $sql = @file_exists($sql) ? file_get_contents($sql) : $sql;
293 + $sql = static::cleanUp($sql);
208 294
209 - $sql = array_map(function($i) { return trim($i);}, explode(',', $sql));
210 -
211 - $sql = "ALTER TABLE $table ".PHP_EOL.rtrim(trim(implode(','.PHP_EOL, $sql)), ';').";";
295 + if (static::isSqlite()) {
296 + $sql = preg_replace('/\bjson\b/i', 'longtext', $sql);
297 + }
212 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 +
213 314 return static::query($sql);
214 315 }
215 316
216 317 /**
217 - * Alters an existing table using dbDelta function if exists, otherwise creates it.
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.
218 362 *
219 - * Alters an existing table but takes the table's creation column defination. This is
220 - * because, the dbDelta functioin can create or update a table using the table same
221 - * creation defination. In this, case, if a table exists and columns are matched
222 - * then nothing happens but if there's any difference in the new sql then the
223 - * dbDelta alters the table using the new defination but doesn't delete any
224 - * columns. so, after the dbDelta finishes it's job, any non-existing
225 - * columns in the new defination will be deleted from the existing
226 - * table. if table is not there then the table gets created.
227 - *
228 363 * @param string $table The table name without the prefix
229 - * @param string $sql The sql to create table or an absolute path of a
364 + * @param string $sql The sql to create table or an absolute path of a
230 365 * .sql file containing the column definations for creating the new table.
231 366 *
232 367 * @return string message
233 368 */
@@ -232,47 +367,141 @@
232 367 * @return string message
233 368 */
234 369 public static function updateTable($table, $sql)
235 370 {
236 - $result = static::createTable(
237 - $table, implode(",\n", array_map('trim', explode(',', $sql)))
238 - );
371 + $sql = static::cleanUp($sql);
239 372
240 - if ($existingColumns = static::getColumns($table)) {
373 + if (static::isSqlite()) {
374 + $sql = preg_replace('/\bjson\b/i', 'longtext', $sql);
375 + }
241 376
242 - $columns = array_map(function($l) {
243 - if (preg_match('/(\S+)/', $l, $matches)) return $matches[1];
244 - }, array_map('trim', explode(',', $sql)));
377 + $columnsDefinitions = array_map('trim', static::splitAlterClauses($sql));
378 + $schemaSql = implode(",\n", $columnsDefinitions);
245 379
246 - foreach ($existingColumns as $column) {
247 - if (!in_array($column, $columns)) {
248 - $tbl = static::table($table);
249 - static::query("alter table $tbl drop column $column");
250 - $tblColumn = $tbl.'.'.$column;
251 - $result[$tblColumn] = "Dropped column {$tblColumn}";
252 - }
253 - }
254 - }
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 + }
255 386
256 - 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;
257 473 }
258 474
259 475 /**
260 - * Drops/deletes an existing table if exists
476 + * Drops/deletes an existing table.
261 477 *
262 - * @param string $table The table name without the prefix
263 - * @return bool
478 + * @param string $table The table name without the prefix
479 + * @param bool $disableForeignKeyCheck Optional.
480 + * @return bool
264 481 */
265 - public static function dropTableIfExists($table)
266 - {
267 - if (static::hasTable($table)) {
268 - return static::db()->query('DROP TABLE ' . static::table($table));
269 - }
270 - }
482 + public static function dropTable($table, $disableForeignKeyCheck = true)
483 + {
484 + return static::db()->query('DROP TABLE ' . static::table($table));
485 + }
271 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 +
272 501 /**
273 502 * Truncate a table.
274 - *
503 + *
275 504 * @param string $table
276 505 * @return bool
277 506 */
278 507 public static function truncate($table)
@@ -278,14 +507,23 @@
278 507 public static function truncate($table)
279 508 {
280 509 $table = static::table($table);
281 510
282 - 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;
283 521 }
284 522
285 523 /**
286 524 * Truncate a table if exists.
287 - *
525 + *
288 526 * @param string $table
289 527 * @return bool
290 528 */
291 529 public static function truncateTableIfExists($table)
@@ -295,39 +533,74 @@
295 533 }
296 534 }
297 535
298 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 + /**
299 568 * Makes raw query and can resolve the table name from the query
300 569 * and can form a full table name including the table prefix if
301 570 * the table name is wrapped like: %table_name% in the query.
302 571 *
303 - * @param straing $query
572 + * @param string $query
304 573 * @return mixed
305 574 */
306 575 public static function query($query)
307 576 {
308 - if (preg_match('/%.*%/', $query, $m)) {
309 - $query = str_replace(
310 - $m[0], static::table(trim($m[0], '%')), $query
311 - );
312 - }
577 + $query = preg_replace_callback('/(?<![\'"])%([a-zA-Z0-9_-]+)%(?![\'"])/', function ($matches) {
578 + return static::table($matches[1]);
579 + }, $query);
313 580
581 + if (preg_match('/^(SELECT|SHOW|DESCRIBE|EXPLAIN)\s+/i', trim($query))) {
582 + return static::db()->get_results($query, OBJECT);
583 + }
584 +
314 585 return static::db()->query($query);
315 586 }
316 587
317 588 /**
318 589 * Get a list of all columns from the given table name.
319 - *
590 + *
320 591 * @param string $table The table name without the prefix
321 - * @return array
592 + * @return array|null
322 593 */
323 594 public static function getColumns($table)
324 595 {
325 - if (static::hasTable($table)) {
326 - return static::db()->get_col(
327 - 'DESC ' . static::table($table), 0
328 - );
596 + if (!static::hasTable($table)) {
597 + return null;
329 598 }
599 +
600 + return static::db()->get_col(
601 + 'DESCRIBE ' . static::table($table), 0
602 + );
330 603 }
331 604
332 605 /**
333 606 * Gets a list of all columns including column information
@@ -334,14 +607,52 @@
334 607 *
335 608 * @param string $table The table name without the prefix
336 609 * @return array
337 610 */
338 - public static function getColumnsWithTypes($table)
611 + protected static function protectedGetColumnsWithTypes($table)
339 612 {
340 613 if (!static::hasTable($table)) return;
341 -
614 +
615 + $table = static::table($table);
616 +
617 + if (static::isSqlite()) {
618 + // INFORMATION_SCHEMA is not available in SQLite.
619 + // Use DESCRIBE which the WP SQLite plugin translates with
620 + // proper MySQL type mapping from its data type cache.
621 + $columns = (array) static::db()->get_results("DESCRIBE {$table}");
622 + return array_map(function ($col, $pos) {
623 + $extra = $col->Extra ?? '';
624 + // SQLite INTEGER PRIMARY KEY is auto_increment but DESCRIBE
625 + // doesn't report it in Extra — detect from Key + Type.
626 + if (empty($extra) && ($col->Key ?? '') === 'PRI') {
627 + $type = strtoupper($col->Type ?? '');
628 + if (in_array($type, ['INTEGER', 'BIGINT', 'BIGINT(20)', 'BIGINT(20) UNSIGNED', 'INT'])) {
629 + $extra = 'auto_increment';
630 + }
631 + }
632 +
633 + return [
634 + 'column_name' => $col->Field,
635 + 'ordinal_position' => $pos + 1,
636 + 'column_default' => $col->Default,
637 + 'is_nullable' => ($col->Null === 'YES' ? 'YES' : 'NO'),
638 + 'data_type' => strtolower(
639 + preg_replace('/\(.*/', '', $col->Type)
640 + ),
641 + 'character_maximum_length' => preg_match(
642 + '/\((\d+)\)/', $col->Type, $matches
643 + ) ? (int)$matches[1] : null,
644 + 'numeric_precision' => null,
645 + 'numeric_scale' => null,
646 + 'column_key' => $col->Key ?? '',
647 + 'extra' => $extra,
648 + ];
649 + }, $columns, array_keys($columns));
650 + }
651 +
652 + // @phpstan-ignore-next-line
342 653 $db = static::db()->dbname;
343 - $table = static::table($table);
654 +
344 655 $fields = [
345 656 'COLUMN_NAME',
346 657 'ORDINAL_POSITION',
347 658 'COLUMN_DEFAULT',
@@ -352,9 +663,9 @@
352 663 'NUMERIC_SCALE',
353 664 'COLUMN_KEY',
354 665 'EXTRA',
355 666 ];
356 -
667 +
357 668 $sql = "SELECT " . implode(',', $fields) . " FROM INFORMATION_SCHEMA.COLUMNS";
358 669 $sql .= " WHERE TABLE_NAME = '".$table."' AND TABLE_SCHEMA = '".$db."'";
359 670
360 671 return array_map(function($i) {
@@ -362,12 +673,139 @@
362 673 foreach ((array) $i as $key => $value) {
363 674 $item[strtolower($key)] = $value;
364 675 }
365 676 return $item;
366 - }, static::db()->get_results($sql));
677 + }, (array) static::db()->get_results($sql));
367 678 }
368 679
680 + public static function getColumnsWithTypes($table)
681 + {
682 + $columns = static::protectedGetColumnsWithTypes($table);
683 +
684 + if (!empty($columns)) {
685 + return $columns;
686 + }
687 +
688 + if (static::isSqlite()) {
689 + return [];
690 + }
691 +
692 + $columns = static::db()->get_results(
693 + 'SHOW COLUMNS FROM `'.static::table($table).'`'
694 + );
695 +
696 + return array_map(function ($col) {
697 + return [
698 + 'column_name' => $col->Field,
699 + 'ordinal_position' => null,
700 + 'column_default' => $col->Default,
701 + 'is_nullable' => ($col->Null === 'YES' ? 'YES' : 'NO'),
702 + 'data_type' => strtolower(
703 + preg_replace('/\(.*/', '', $col->Type)
704 + ),
705 + 'character_maximum_length' => preg_match(
706 + '/\((\d+)\)/', $col->Type, $matches
707 + ) ? (int)$matches[1] : null,
708 + 'numeric_precision' => null,
709 + 'numeric_scale' => null,
710 + 'column_key' => $col->Key,
711 + 'extra' => $col->Extra,
712 + ];
713 + }, (array) $columns);
714 + }
715 +
369 716 /**
717 + * Gets a list of all columns including column information
718 + *
719 + * @param string $table The table name without the prefix
720 + * @return array
721 + */
722 + public static function describeTable($table)
723 + {
724 + return static::getColumnsWithTypes($table);
725 + }
726 +
727 + /**
728 + * Gets a list of all foreign keys from the given table name.
729 + *
730 + * @param string $table
731 + * @return array
732 + */
733 + public static function getTableForeignKeys($table)
734 + {
735 + $table = static::table($table);
736 +
737 + if (static::isSqlite()) {
738 + // Parse foreign keys from the CREATE TABLE statement in sqlite_master
739 + // since PRAGMA queries cannot go through $wpdb.
740 + $row = static::db()->get_row(
741 + "SELECT sql FROM sqlite_master WHERE type='table' AND name='{$table}'"
742 + );
743 + if (!$row || empty($row->sql)) {
744 + return [];
745 + }
746 + $fks = [];
747 + if (preg_match_all(
748 + '/FOREIGN\s+KEY\s*\(\s*[`"]?(\w+)[`"]?\s*\)\s*REFERENCES\s+[`"]?(\w+)[`"]?\s*\(\s*[`"]?(\w+)[`"]?\s*\)/i',
749 + $row->sql, $matches, PREG_SET_ORDER
750 + )) {
751 + foreach ($matches as $m) {
752 + $fks[] = [
753 + 'column_name' => $m[1],
754 + 'referenced_table' => $m[2],
755 + 'referenced_column' => $m[3],
756 + ];
757 + }
758 + }
759 + return $fks;
760 + }
761 +
762 + // @phpstan-ignore-next-line
763 + $db = static::db()->dbname;
764 +
765 + $sql = "SELECT COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
766 + FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
767 + WHERE TABLE_NAME = '{$table}'
768 + AND TABLE_SCHEMA = '{$db}'
769 + AND REFERENCED_TABLE_NAME IS NOT NULL";
770 +
771 + return array_map(function ($i) {
772 + return [
773 + 'column_name' => $i->COLUMN_NAME,
774 + 'referenced_table' => $i->REFERENCED_TABLE_NAME,
775 + 'referenced_column' => $i->REFERENCED_COLUMN_NAME,
776 + ];
777 + }, (array) static::db()->get_results($sql));
778 + }
779 +
780 + /**
781 + * Retrieves the list of all available built-in tables from
782 + * the database using WordPress' $wpdb->tables native method.
783 + *
784 + * @param string $scope
785 + * @param boolean $prefix
786 + * @param integer $blogId
787 + * @return string[] WP Table names. When a prefix is requested,
788 + * the key is the unprefixed table name.
789 + * @see https://developer.wordpress.org/reference/classes/wpdb/tables/
790 + */
791 + public static function tables($scope = 'all', $prefix = true, $blogId = 0)
792 + {
793 + return static::db()->tables($scope, $prefix, $blogId);
794 + }
795 +
796 + /**
797 + * Retrieves the list of all available tables from the database.
798 + *
799 + * @param string $dbname optional
800 + * @return array
801 + */
802 + public static function getTables($dbname = null)
803 + {
804 + return static::getTableList($dbname);
805 + }
806 +
807 + /**
370 808 * Retrieves the list of all available tables in the database.
371 809 *
372 810 * @param string $dbname optional
373 811 * @return array
@@ -373,27 +811,151 @@
373 811 * @return array
374 812 */
375 813 public static function getTableList($dbname = null)
376 814 {
815 + if (static::isSqlite()) {
816 + return array_map(function ($i) {
817 + return $i->name;
818 + }, (array) static::db()->get_results(
819 + "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
820 + ));
821 + }
822 +
823 + // @phpstan-ignore-next-line
377 824 $dbname = $dbname ?: static::db()->dbname;
378 825 $sql = "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES";
379 826 $sql .= " WHERE TABLE_SCHEMA = '".$dbname."'";
827 +
380 828 return array_map(function($i) {
381 829 return $i->TABLE_NAME;
382 - }, static::db()->get_results($sql));
830 + }, (array) static::db()->get_results($sql));
383 831 }
384 832
385 833 /**
834 + * Retrieves the list of all available views in the database.
835 + *
836 + * @param string $dbname optional
837 + * @return array
838 + */
839 + public static function getViews($dbname = null)
840 + {
841 + if (static::isSqlite()) {
842 + return array_map(function ($i) {
843 + return $i->name;
844 + }, (array) static::db()->get_results(
845 + "SELECT name FROM sqlite_master WHERE type='view'"
846 + ));
847 + }
848 +
849 + // @phpstan-ignore-next-line
850 + $dbname = $dbname ?: static::db()->dbname;
851 + $sql = "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.VIEWS";
852 + $sql .= " WHERE TABLE_SCHEMA = '".$dbname."'";
853 +
854 + return array_map(function ($i) {
855 + return $i->TABLE_NAME;
856 + }, (array) static::db()->get_results($sql));
857 + }
858 +
859 + /**
860 + * Retrieves the SQL definition of a specific view.
861 + *
862 + * @param string $name The view name
863 + * @param string $dbname optional
864 + * @return string|null
865 + */
866 + public static function getView($name, $dbname = null)
867 + {
868 + if (static::isSqlite()) {
869 + $result = static::db()->get_row(
870 + "SELECT sql FROM sqlite_master WHERE type='view' AND name='".$name."'"
871 + );
872 +
873 + return $result ? $result->sql : null;
874 + }
875 +
876 + // @phpstan-ignore-next-line
877 + $dbname = $dbname ?: static::db()->dbname;
878 + $result = static::db()->get_row(
879 + "SELECT VIEW_DEFINITION FROM INFORMATION_SCHEMA.VIEWS"
880 + . " WHERE TABLE_SCHEMA = '".$dbname."'"
881 + . " AND TABLE_NAME = '".$name."'"
882 + );
883 +
884 + return $result ? $result->VIEW_DEFINITION : null;
885 + }
886 +
887 + /**
888 + * Determine if the connected database is a sqlite database.
889 + *
890 + * @return bool
891 + */
892 + public static function isSqlite()
893 + {
894 + return defined('DB_ENGINE') && DB_ENGINE === 'sqlite';
895 + }
896 +
897 + /**
898 + * Determine if the connected database is a mariadb database.
899 + *
900 + * @return bool
901 + */
902 + public static function isMaria()
903 + {
904 + if (static::isSqlite()) {
905 + return false;
906 + }
907 +
908 + return str_contains(
909 + static::db()->get_var('SELECT VERSION()'), 'MariaDB'
910 + );
911 + }
912 +
913 + /**
914 + * Retrieve the current database engine name.
915 + *
916 + * @param string $table
917 + * @return string
918 + */
919 + public static function getEngine($table)
920 + {
921 + if (static::isSqlite()) {
922 + return 'sqlite';
923 + }
924 +
925 + return static::db()->get_var(
926 + 'SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = "' . static::table($table) . '"'
927 + );
928 + }
929 +
930 + /**
386 931 * The wrapper for calling dbDelta function
387 932 *
388 933 * @param string $sql
389 934 * @return mixed
390 935 */
391 - protected static function callDBDelta($table, $sql)
936 + public static function callDBDelta($sql)
392 937 {
393 938 if (!function_exists('dbDelta')) {
394 939 require (ABSPATH . 'wp-admin/includes/upgrade.php');
395 940 }
396 941
397 - return dbDelta($sql);
942 + $result = dbDelta($sql);
943 +
944 + if (php_sapi_name() === 'cli') {
945 + $key = array_key_first($result);
946 + if ($key && !str_contains($key, '.')) {
947 + static::$customTempTables[] = $key;
948 + }
949 + }
950 +
951 + return $result;
398 952 }
953 +
954 + /**
955 + * Helper to get the driver-specific JSON type.
956 + */
957 + public static function jsonType()
958 + {
959 + return static::isSqlite() ? 'longtext' : 'json';
960 + }
399 961 }