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 +664 -83 1.212.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 {
@@ -77,8 +107,27 @@
77 107 }
78 108 }
79 109
80 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 + /**
81 130 * Checks if a table exists
82 131 *
83 132 * @param string $table The table name without prefix
84 133 * @return boolean
@@ -101,16 +150,18 @@
101 150 // error and then turn off the error from being shown, so
102 151 // error will be not shown if there is no temporary
103 152 // table, then restore the error state.
104 153 $isErrorSuppressed = $wpdb->suppress_errors;
105 -
154 +
106 155 $wpdb->suppress_errors = true;
107 156
108 - $result = static::query("SELECT 1 FROM %{$table}% WHERE 0");
157 + static::query("SELECT 1 FROM %{$table}% WHERE 0");
109 158
159 + $hasError = !empty($wpdb->last_error);
160 +
110 161 $wpdb->suppress_errors = $isErrorSuppressed;
111 162
112 - return $result === 0;
163 + return !$hasError;
113 164 }
114 165
115 166 /**
116 167 * Resolves the table prefix and makes the table name with prefix
@@ -130,8 +181,59 @@
130 181
131 182 return isset($wpdb->{$table}) ? $wpdb->{$table} : ($wpdb->prefix.$table);
132 183 }
133 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 +
134 236 /**
135 237 * Creates a new table using dbDelta function or alters the table if exists.
136 238 *
137 239 * @param string $table The table name without the prefix
@@ -143,18 +245,21 @@
143 245 public static function createTable($table, $sql)
144 246 {
145 247 $table = static::table($table);
146 248
147 - $sql = @file_exists($sql) ? file_get_contents($sql) : $sql;
249 + $sql = static::cleanUp($sql);
148 250
251 + if (static::isSqlite()) {
252 + $sql = preg_replace('/\bjson\b/i', 'longtext', $sql);
253 + }
254 +
149 255 $collate = static::db()->get_charset_collate();
150 256
151 - if ($sql && !str_contains(basename($sql), '.')) {
152 - return static::callDBDelta(
153 - $table,
154 - "CREATE TABLE $table (".PHP_EOL.trim(trim($sql), ',').PHP_EOL.") $collate;"
155 - );
156 - }
257 + return static::callDBDelta(
258 + "CREATE TABLE $table (
259 + ".PHP_EOL.trim(trim($sql), ',').PHP_EOL."
260 + ) $collate;"
261 + );
157 262 }
158 263
159 264 /**
160 265 * Alters an existing table if exists
@@ -173,13 +278,13 @@
173 278 }
174 279
175 280 /**
176 281 * Alters an existing table
177 - *
282 + *
178 283 * @param string $table The table name without the prefix
179 284 * @param string $sql The sql to create table or an absolute path of a
180 285 * .sql file containing the column definations for creating the new table.
181 - *
286 + *
182 287 * @return string message
183 288 */
184 289 public static function alterTable($table, $sql)
185 290 {
@@ -184,31 +289,80 @@
184 289 public static function alterTable($table, $sql)
185 290 {
186 291 $table = static::table($table);
187 292
188 - $sql = @file_exists($sql) ? file_get_contents($sql) : $sql;
293 + $sql = static::cleanUp($sql);
189 294
190 - $sql = array_map(function($i) { return trim($i);}, explode(',', $sql));
191 -
192 - $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 + }
193 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 +
194 314 return static::query($sql);
195 315 }
196 316
197 317 /**
198 - * 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.
199 362 *
200 - * Alters an existing table but takes the table's creation column defination. This is
201 - * because, the dbDelta functioin can create or update a table using the table same
202 - * creation defination. In this, case, if a table exists and columns are matched
203 - * then nothing happens but if there's any difference in the new sql then the
204 - * dbDelta alters the table using the new defination but doesn't delete any
205 - * columns. so, after the dbDelta finishes it's job, any non-existing
206 - * columns in the new defination will be deleted from the existing
207 - * table. if table is not there then the table gets created.
208 - *
209 363 * @param string $table The table name without the prefix
210 - * @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
211 365 * .sql file containing the column definations for creating the new table.
212 366 *
213 367 * @return string message
214 368 */
@@ -213,47 +367,141 @@
213 367 * @return string message
214 368 */
215 369 public static function updateTable($table, $sql)
216 370 {
217 - $result = static::createTable(
218 - $table, implode(",\n", array_map('trim', explode(',', $sql)))
219 - );
371 + $sql = static::cleanUp($sql);
220 372
221 - if ($existingColumns = static::getColumns($table)) {
373 + if (static::isSqlite()) {
374 + $sql = preg_replace('/\bjson\b/i', 'longtext', $sql);
375 + }
222 376
223 - $columns = array_map(function($l) {
224 - if (preg_match('/(\S+)/', $l, $matches)) return $matches[1];
225 - }, array_map('trim', explode(',', $sql)));
377 + $columnsDefinitions = array_map('trim', static::splitAlterClauses($sql));
378 + $schemaSql = implode(",\n", $columnsDefinitions);
226 379
227 - foreach ($existingColumns as $column) {
228 - if (!in_array($column, $columns)) {
229 - $tbl = static::table($table);
230 - static::query("alter table $tbl drop column $column");
231 - $tblColumn = $tbl.'.'.$column;
232 - $result[$tblColumn] = "Dropped column {$tblColumn}";
233 - }
234 - }
235 - }
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 + }
236 386
237 - 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;
238 473 }
239 474
240 475 /**
241 - * Drops/deletes an existing table if exists
476 + * Drops/deletes an existing table.
242 477 *
243 - * @param string $table The table name without the prefix
244 - * @return bool
478 + * @param string $table The table name without the prefix
479 + * @param bool $disableForeignKeyCheck Optional.
480 + * @return bool
245 481 */
246 - public static function dropTableIfExists($table)
247 - {
248 - if (static::hasTable($table)) {
249 - return static::db()->query('DROP TABLE ' . static::table($table));
250 - }
251 - }
482 + public static function dropTable($table, $disableForeignKeyCheck = true)
483 + {
484 + return static::db()->query('DROP TABLE ' . static::table($table));
485 + }
252 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 +
253 501 /**
254 502 * Truncate a table.
255 - *
503 + *
256 504 * @param string $table
257 505 * @return bool
258 506 */
259 507 public static function truncate($table)
@@ -259,14 +507,23 @@
259 507 public static function truncate($table)
260 508 {
261 509 $table = static::table($table);
262 510
263 - 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;
264 521 }
265 522
266 523 /**
267 524 * Truncate a table if exists.
268 - *
525 + *
269 526 * @param string $table
270 527 * @return bool
271 528 */
272 529 public static function truncateTableIfExists($table)
@@ -276,39 +533,74 @@
276 533 }
277 534 }
278 535
279 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 + /**
280 568 * Makes raw query and can resolve the table name from the query
281 569 * and can form a full table name including the table prefix if
282 570 * the table name is wrapped like: %table_name% in the query.
283 571 *
284 - * @param straing $query
572 + * @param string $query
285 573 * @return mixed
286 574 */
287 575 public static function query($query)
288 576 {
289 - if (preg_match('/%.*%/', $query, $m)) {
290 - $query = str_replace(
291 - $m[0], static::table(trim($m[0], '%')), $query
292 - );
293 - }
577 + $query = preg_replace_callback('/(?<![\'"])%([a-zA-Z0-9_-]+)%(?![\'"])/', function ($matches) {
578 + return static::table($matches[1]);
579 + }, $query);
294 580
581 + if (preg_match('/^(SELECT|SHOW|DESCRIBE|EXPLAIN)\s+/i', trim($query))) {
582 + return static::db()->get_results($query, OBJECT);
583 + }
584 +
295 585 return static::db()->query($query);
296 586 }
297 587
298 588 /**
299 589 * Get a list of all columns from the given table name.
300 - *
590 + *
301 591 * @param string $table The table name without the prefix
302 - * @return array
592 + * @return array|null
303 593 */
304 594 public static function getColumns($table)
305 595 {
306 - if (static::hasTable($table)) {
307 - return static::db()->get_col(
308 - 'DESC ' . static::table($table), 0
309 - );
596 + if (!static::hasTable($table)) {
597 + return null;
310 598 }
599 +
600 + return static::db()->get_col(
601 + 'DESCRIBE ' . static::table($table), 0
602 + );
311 603 }
312 604
313 605 /**
314 606 * Gets a list of all columns including column information
@@ -315,14 +607,52 @@
315 607 *
316 608 * @param string $table The table name without the prefix
317 609 * @return array
318 610 */
319 - public static function getColumnsWithTypes($table)
611 + protected static function protectedGetColumnsWithTypes($table)
320 612 {
321 613 if (!static::hasTable($table)) return;
322 -
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
323 653 $db = static::db()->dbname;
324 - $table = static::table($table);
654 +
325 655 $fields = [
326 656 'COLUMN_NAME',
327 657 'ORDINAL_POSITION',
328 658 'COLUMN_DEFAULT',
@@ -333,9 +663,9 @@
333 663 'NUMERIC_SCALE',
334 664 'COLUMN_KEY',
335 665 'EXTRA',
336 666 ];
337 -
667 +
338 668 $sql = "SELECT " . implode(',', $fields) . " FROM INFORMATION_SCHEMA.COLUMNS";
339 669 $sql .= " WHERE TABLE_NAME = '".$table."' AND TABLE_SCHEMA = '".$db."'";
340 670
341 671 return array_map(function($i) {
@@ -343,12 +673,139 @@
343 673 foreach ((array) $i as $key => $value) {
344 674 $item[strtolower($key)] = $value;
345 675 }
346 676 return $item;
347 - }, static::db()->get_results($sql));
677 + }, (array) static::db()->get_results($sql));
348 678 }
349 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 +
350 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 + /**
351 808 * Retrieves the list of all available tables in the database.
352 809 *
353 810 * @param string $dbname optional
354 811 * @return array
@@ -354,27 +811,151 @@
354 811 * @return array
355 812 */
356 813 public static function getTableList($dbname = null)
357 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
358 824 $dbname = $dbname ?: static::db()->dbname;
359 825 $sql = "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES";
360 826 $sql .= " WHERE TABLE_SCHEMA = '".$dbname."'";
827 +
361 828 return array_map(function($i) {
362 829 return $i->TABLE_NAME;
363 - }, static::db()->get_results($sql));
830 + }, (array) static::db()->get_results($sql));
364 831 }
365 832
366 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 + /**
367 931 * The wrapper for calling dbDelta function
368 932 *
369 933 * @param string $sql
370 934 * @return mixed
371 935 */
372 - protected static function callDBDelta($table, $sql)
936 + public static function callDBDelta($sql)
373 937 {
374 938 if (!function_exists('dbDelta')) {
375 939 require (ABSPATH . 'wp-admin/includes/upgrade.php');
376 940 }
377 941
378 - 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;
379 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 + }
380 961 }