d36bb954d1
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
211 lines
6.6 KiB
PHP
211 lines
6.6 KiB
PHP
<?php
|
|
/**
|
|
* Database abstraction layer for WP Data Optimizer.
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* TMDO_DB — Database abstraction layer for MySQL / SQLite dual support.
|
|
*
|
|
* Provides unified transaction interface, SQL dialect helpers, and
|
|
* table name resolution. Absorbs and extends the FCB_DB pattern.
|
|
*/
|
|
class TMDO_DB {
|
|
|
|
/**
|
|
* Check if the current database engine is SQLite.
|
|
*/
|
|
public static function is_sqlite(): bool {
|
|
return TMDO_IS_SQLITE;
|
|
}
|
|
|
|
/**
|
|
* Check if the current database engine is MySQL.
|
|
*/
|
|
public static function is_mysql(): bool {
|
|
return TMDO_IS_MYSQL;
|
|
}
|
|
|
|
/**
|
|
* Begin a database transaction.
|
|
*
|
|
* SQLite: BEGIN IMMEDIATE (write-lock, prevents concurrent writes).
|
|
* MySQL: START TRANSACTION (row-level locking via InnoDB).
|
|
*/
|
|
public static function begin(): void {
|
|
global $wpdb;
|
|
$wpdb->query( self::is_sqlite() ? 'BEGIN IMMEDIATE' : 'START TRANSACTION' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Static SQL literals; no user input.
|
|
}
|
|
|
|
/**
|
|
* Commit the current transaction.
|
|
*/
|
|
public static function commit(): void {
|
|
global $wpdb;
|
|
$wpdb->query( 'COMMIT' );
|
|
}
|
|
|
|
/**
|
|
* Roll back the current transaction.
|
|
*/
|
|
public static function rollback(): void {
|
|
global $wpdb;
|
|
$wpdb->query( 'ROLLBACK' );
|
|
}
|
|
|
|
/**
|
|
* Get the current WordPress local time as a MySQL datetime string.
|
|
*/
|
|
public static function now(): string {
|
|
return current_time( 'mysql' );
|
|
}
|
|
|
|
/**
|
|
* Get the full table name with wpdb prefix.
|
|
*
|
|
* @param string $name Table name without prefix (e.g. 'wpdo_warm').
|
|
* @return string Fully-prefixed, sanitized table name.
|
|
*/
|
|
public static function table( string $name ): string {
|
|
global $wpdb;
|
|
return $wpdb->prefix . sanitize_key( $name );
|
|
}
|
|
|
|
/**
|
|
* Execute an INSERT IGNORE statement (MySQL) or INSERT OR IGNORE (SQLite).
|
|
*
|
|
* @param string $table Full table name.
|
|
* @param array $data Column => value pairs.
|
|
* @param array $format Optional wpdb format array ('%s', '%d', etc.).
|
|
* @return int|false Number of rows affected, or false on error.
|
|
*/
|
|
public static function insert_ignore( string $table, array $data, array $format = array() ): int|false {
|
|
global $wpdb;
|
|
|
|
$columns = array_keys( $data );
|
|
$values = array_values( $data );
|
|
$col_list = implode( ', ', array_map( fn( $c ) => '`' . sanitize_key( $c ) . '`', $columns ) );
|
|
$placeholder = implode( ', ', $format ?: array_fill( 0, count( $values ), '%s' ) );
|
|
|
|
$keyword = self::is_sqlite() ? 'INSERT OR IGNORE' : 'INSERT IGNORE';
|
|
|
|
$sql = "{$keyword} INTO `{$table}` ({$col_list}) VALUES ({$placeholder})";
|
|
|
|
return $wpdb->query( $wpdb->prepare( $sql, ...$values ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
|
}
|
|
|
|
/**
|
|
* Build a JSON_EXTRACT expression compatible with both MySQL and SQLite.
|
|
*
|
|
* Both MySQL 5.7+ and SQLite 3.38+ support json_extract().
|
|
*
|
|
* @param string $column Column name containing JSON data.
|
|
* @param string $path JSON path (e.g. '$.social_links').
|
|
* @return string SQL expression.
|
|
*/
|
|
public static function json_extract( string $column, string $path ): string {
|
|
$column = sanitize_key( $column );
|
|
// JSON path must start with '$' and contain only word chars, dots, brackets, and digits.
|
|
if ( ! preg_match( '/^\$[\w.\[\]0-9]*$/', $path ) ) {
|
|
$path = '$';
|
|
}
|
|
return "json_extract(`{$column}`, '{$path}')";
|
|
}
|
|
|
|
/**
|
|
* Build an UPSERT statement.
|
|
*
|
|
* MySQL: INSERT ... ON DUPLICATE KEY UPDATE ...
|
|
* SQLite: INSERT ... ON CONFLICT(...) DO UPDATE SET ...
|
|
*
|
|
* @param string $table Full table name.
|
|
* @param array $data Column => value pairs to insert.
|
|
* @param array $update_columns Columns to update on conflict.
|
|
* @param string|string[] $conflict_key Column name (string) or columns (array)
|
|
* for ON CONFLICT (SQLite). On MySQL it
|
|
* is informational only — ON DUPLICATE
|
|
* KEY UPDATE matches any unique key.
|
|
* @param array $format Optional wpdb format array.
|
|
* @return int|false
|
|
*/
|
|
public static function upsert( string $table, array $data, array $update_columns, string|array $conflict_key = 'post_id', array $format = array() ): int|false {
|
|
global $wpdb;
|
|
|
|
$columns = array_keys( $data );
|
|
$values = array_values( $data );
|
|
// Normalise composite vs single conflict key.
|
|
$conflict_keys = is_array( $conflict_key ) ? $conflict_key : array( $conflict_key );
|
|
$conflict_keys = array_map( 'sanitize_key', $conflict_keys );
|
|
$conflict_list = implode(
|
|
', ',
|
|
array_map( static fn( $k ) => '`' . $k . '`', $conflict_keys )
|
|
);
|
|
|
|
$col_list = implode( ', ', array_map( fn( $c ) => '`' . sanitize_key( $c ) . '`', $columns ) );
|
|
|
|
// Build per-value placeholders, using NULL literal for null values.
|
|
$prepare_values = array();
|
|
$placeholders = array();
|
|
$fmt = $format ?: array_fill( 0, count( $values ), '%s' );
|
|
foreach ( $values as $i => $v ) {
|
|
if ( null === $v ) {
|
|
$placeholders[] = 'NULL';
|
|
} else {
|
|
$placeholders[] = $fmt[ $i ] ?? '%s';
|
|
$prepare_values[] = $v;
|
|
}
|
|
}
|
|
$placeholder = implode( ', ', $placeholders );
|
|
|
|
// ON DUPLICATE KEY UPDATE: null columns use NULL literal, others use VALUES().
|
|
$null_cols = array();
|
|
foreach ( $update_columns as $c ) {
|
|
$col_index = array_search( $c, $columns, true );
|
|
if ( false !== $col_index && null === $values[ $col_index ] ) {
|
|
$null_cols[] = $c;
|
|
}
|
|
}
|
|
|
|
if ( self::is_mysql() ) {
|
|
$updates = implode(
|
|
', ',
|
|
array_map(
|
|
function ( $c ) use ( $null_cols ) {
|
|
$safe = sanitize_key( $c );
|
|
return in_array( $c, $null_cols, true )
|
|
? "`{$safe}` = NULL"
|
|
: "`{$safe}` = VALUES(`{$safe}`)";
|
|
},
|
|
$update_columns
|
|
)
|
|
);
|
|
$sql = "INSERT INTO `{$table}` ({$col_list}) VALUES ({$placeholder}) ON DUPLICATE KEY UPDATE {$updates}";
|
|
} else {
|
|
$updates = implode(
|
|
', ',
|
|
array_map(
|
|
function ( $c ) use ( $null_cols ) {
|
|
$safe = sanitize_key( $c );
|
|
return in_array( $c, $null_cols, true )
|
|
? "`{$safe}` = NULL"
|
|
: "`{$safe}` = excluded.`{$safe}`";
|
|
},
|
|
$update_columns
|
|
)
|
|
);
|
|
$sql = "INSERT INTO `{$table}` ({$col_list}) VALUES ({$placeholder}) ON CONFLICT({$conflict_list}) DO UPDATE SET {$updates}";
|
|
}
|
|
|
|
if ( empty( $prepare_values ) ) {
|
|
return $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
|
}
|
|
|
|
return $wpdb->query( $wpdb->prepare( $sql, ...$prepare_values ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
|
}
|
|
}
|