Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 36 additions & 19 deletions inc/Cli/Commands/RetentionCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use WP_CLI;
use DataMachine\Cli\BaseCommand;
use DataMachine\Core\Database\BaseRepository;

if ( ! defined( 'ABSPATH' ) ) {
exit;
Expand Down Expand Up @@ -281,28 +282,44 @@ private function get_table_sizes(): array {

// Deduplicate tables for the query (jobs appears twice).
$unique_tables = array_unique( array_values( $tables ) );
$placeholders = implode( ',', array_fill( 0, count( $unique_tables ), '%s' ) );

// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$results = $wpdb->get_results(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->prepare(
"SELECT table_name, table_rows,
ROUND((data_length + index_length) / 1024 / 1024, 1) AS size_mb
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name IN ({$placeholders})",
...$unique_tables
)
);

$table_data = array();
if ( $results ) {
foreach ( $results as $row ) {
$table_data[ $row->table_name ] = array(
'rows' => (int) $row->table_rows,
'size_mb' => $row->size_mb,

if ( BaseRepository::is_sqlite() ) {
// SQLite: no information_schema. Count rows per table; size is unavailable.
foreach ( $unique_tables as $tbl ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$count = (int) $wpdb->get_var(
$wpdb->prepare( 'SELECT COUNT(*) FROM %i', $tbl )
);
$table_data[ $tbl ] = array(
'rows' => $count,
'size_mb' => '0.0',
);
}
} else {
$placeholders = implode( ',', array_fill( 0, count( $unique_tables ), '%s' ) );

// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$results = $wpdb->get_results(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->prepare(
"SELECT table_name, table_rows,
ROUND((data_length + index_length) / 1024 / 1024, 1) AS size_mb
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name IN ({$placeholders})",
...$unique_tables
)
);

if ( $results ) {
foreach ( $results as $row ) {
$table_data[ $row->table_name ] = array(
'rows' => (int) $row->table_rows,
'size_mb' => $row->size_mb,
);
}
}
}

Expand Down
5 changes: 1 addition & 4 deletions inc/Core/Database/Agents/Agents.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,7 @@ public static function ensure_site_scope_column(): void {

$table_name = $wpdb->base_prefix . self::TABLE_NAME;

// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$column = $wpdb->get_var( "SHOW COLUMNS FROM `{$table_name}` LIKE 'site_scope'" );

if ( ! $column ) {
if ( ! BaseRepository::column_exists( $table_name, 'site_scope', $wpdb ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query( "ALTER TABLE `{$table_name}` ADD COLUMN site_scope BIGINT(20) UNSIGNED NULL DEFAULT NULL AFTER owner_id" );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
Expand Down
92 changes: 92 additions & 0 deletions inc/Core/Database/BaseRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,98 @@ protected function count_rows( string $where = '1=1', array $prepare_args = arra
return (int) $this->wpdb->get_var( $sql );
}

/**
* Check whether WordPress is running on SQLite (e.g. WordPress Studio).
*
* The SQLite Database Integration plugin defines this constant in its
* db.php drop-in. Checking it is the canonical way to detect SQLite at
* runtime — no autoload, no option lookup, no file sniffing required.
*
* @since 0.45.0
*
* @return bool True when the active database driver is SQLite.
*/
public static function is_sqlite(): bool {
return defined( 'DATABASE_TYPE' ) && 'sqlite' === DATABASE_TYPE;
}

/**
* Check whether a column exists on a table.
*
* Uses `SHOW COLUMNS FROM <table> LIKE '<col>'` which the SQLite
* Database Integration translator already handles, avoiding the
* MySQL-only `information_schema.COLUMNS` + `DB_NAME` pattern that
* fatals on SQLite.
*
* @since 0.45.0
*
* @param string $table_name Fully-qualified (prefixed) table name.
* @param string $column Column name to check.
* @param \wpdb|null $wpdb Optional wpdb instance (defaults to global).
* @return bool
*/
public static function column_exists( string $table_name, string $column, ?\wpdb $wpdb = null ): bool {
if ( null === $wpdb ) {
global $wpdb;
}

// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$result = $wpdb->get_var(
$wpdb->prepare( 'SHOW COLUMNS FROM %i LIKE %s', $table_name, $column )
);

return null !== $result;
}

/**
* Get column metadata from information_schema (MySQL only).
*
* Returns an object-keyed result set with DATA_TYPE,
* CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE per column. On SQLite this
* returns an empty array because the schema introspection queries
* that consume this data (MODIFY COLUMN, etc.) are MySQL-only
* operations anyway.
*
* @since 0.45.0
*
* @param string $table_name Fully-qualified (prefixed) table name.
* @param string[] $columns Column names to inspect.
* @param \wpdb|null $wpdb Optional wpdb instance.
* @return array<string,object> Column name → metadata object. Empty on SQLite.
*/
public static function get_column_meta( string $table_name, array $columns, ?\wpdb $wpdb = null ): array {
if ( null === $wpdb ) {
global $wpdb;
}

// On SQLite, MODIFY COLUMN is not supported and these migrations
// are irrelevant — the columns were created with the correct types
// from the start via dbDelta / CREATE TABLE.
if ( self::is_sqlite() ) {
return array();
}

$placeholders = implode( ',', array_fill( 0, count( $columns ), '%s' ) );

// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
// phpcs:disable WordPress.DB.PreparedSQL,WordPress.DB.PreparedSQLPlaceholders
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
AND COLUMN_NAME IN ({$placeholders})",
DB_NAME,
$table_name,
...$columns
),
OBJECT_K
);
// phpcs:enable WordPress.DB.PreparedSQL,WordPress.DB.PreparedSQLPlaceholders

return is_array( $results ) ? $results : array();
}

/**
* Log a database error if one occurred on the last query.
*
Expand Down
18 changes: 4 additions & 14 deletions inc/Core/Database/Chat/Chat.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,7 @@ public static function ensure_agent_id_column(): void {

$table_name = self::get_prefixed_table_name();

// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared
$column = $wpdb->get_var( $wpdb->prepare( 'SHOW COLUMNS FROM %i LIKE %s', $table_name, 'agent_id' ) );

if ( $column ) {
if ( self::column_exists( $table_name, 'agent_id', $wpdb ) ) {
return;
}

Expand All @@ -107,12 +104,8 @@ public static function ensure_context_column(): void {

$table_name = self::get_prefixed_table_name();

// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared
$context_column = $wpdb->get_var( $wpdb->prepare( 'SHOW COLUMNS FROM %i LIKE %s', $table_name, 'context' ) );

if ( ! $context_column ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared
$legacy_agent_type = $wpdb->get_var( $wpdb->prepare( 'SHOW COLUMNS FROM %i LIKE %s', $table_name, 'agent_type' ) );
if ( ! self::column_exists( $table_name, 'context', $wpdb ) ) {
$legacy_agent_type = self::column_exists( $table_name, 'agent_type', $wpdb );

if ( $legacy_agent_type ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared
Expand Down Expand Up @@ -160,10 +153,7 @@ public static function ensure_last_read_at_column(): void {

$table_name = self::get_prefixed_table_name();

// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared
$column = $wpdb->get_var( $wpdb->prepare( 'SHOW COLUMNS FROM %i LIKE %s', $table_name, 'last_read_at' ) );

if ( $column ) {
if ( self::column_exists( $table_name, 'last_read_at', $wpdb ) ) {
return;
}

Expand Down
29 changes: 2 additions & 27 deletions inc/Core/Database/Flows/Flows.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,7 @@ public static function create_table(): void {
*/
public function migrate_columns(): void {
// Check if user_id column already exists.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
// phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix, not user input.
$column = $this->wpdb->get_var(
$this->wpdb->prepare(
"SELECT COLUMN_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'user_id'",
DB_NAME,
$this->table_name
)
);
// phpcs:enable WordPress.DB.PreparedSQL

if ( null === $column ) {
if ( ! self::column_exists( $this->table_name, 'user_id', $this->wpdb ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.SchemaChange
// phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix, not user input.
$result = $this->wpdb->query(
Expand Down Expand Up @@ -104,19 +91,7 @@ public function migrate_columns(): void {
}

// Add agent_id column for agent-first scoping (#735).
// phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix, not user input.
$agent_col = $this->wpdb->get_var(
$this->wpdb->prepare(
"SELECT COLUMN_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'agent_id'",
DB_NAME,
$this->table_name
)
);
// phpcs:enable WordPress.DB.PreparedSQL

if ( null === $agent_col ) {
if ( ! self::column_exists( $this->table_name, 'agent_id', $this->wpdb ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.SchemaChange
// phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix, not user input.
$result = $this->wpdb->query(
Expand Down
Loading
Loading