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 } }