chore: initial snapshot of 2meet-data-optimizer v0.1.0
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
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
/**
|
||||
* Zone D (Archive) handler for historical data archival.
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zone D (Archive) handler — historical data archival with optional compression.
|
||||
*
|
||||
* Single table: wpdo_archive
|
||||
* Stores old/infrequently accessed postmeta entries with optional gzip compression.
|
||||
*
|
||||
* Archive flow:
|
||||
* 1. Identify cold/stale postmeta entries (by age, post_status, or manual selection)
|
||||
* 2. Copy to wpdo_archive with original_meta_id reference
|
||||
* 3. Optionally compress meta_value with gzencode()
|
||||
* 4. Delete from wp_postmeta (only during cleanup phase)
|
||||
*
|
||||
* Retrieval: decompress on read, restore to postmeta if needed.
|
||||
*/
|
||||
class TMDO_Zone_Archive {
|
||||
|
||||
/**
|
||||
* Get the archive table name.
|
||||
*/
|
||||
public static function table(): string {
|
||||
return TMDO_DB::table( 'wpdo_archive' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive a single postmeta entry.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $post_type Post type.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $meta_value Meta value.
|
||||
* @param int $meta_id Original meta_id from wp_postmeta.
|
||||
* @param bool $compress Whether to gzip-compress the value.
|
||||
*/
|
||||
public static function archive( int $post_id, string $post_type, string $meta_key, string $meta_value, int $meta_id = 0, bool $compress = false ): void {
|
||||
global $wpdb;
|
||||
$table = self::table();
|
||||
|
||||
$compressed = 0;
|
||||
if ( $compress && function_exists( 'gzencode' ) ) {
|
||||
$meta_value = base64_encode( gzencode( $meta_value, 6 ) );
|
||||
$compressed = 1;
|
||||
}
|
||||
|
||||
$wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'post_id' => $post_id,
|
||||
'post_type' => $post_type,
|
||||
'meta_key' => $meta_key,
|
||||
'meta_value' => $meta_value,
|
||||
'compressed' => $compressed,
|
||||
'archived_at' => TMDO_DB::now(),
|
||||
'original_meta_id' => $meta_id,
|
||||
),
|
||||
array( '%d', '%s', '%s', '%s', '%d', '%s', '%d' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive multiple postmeta entries in batch.
|
||||
*
|
||||
* @param array $entries Array of [post_id, post_type, meta_key, meta_value, meta_id].
|
||||
* @param bool $compress Whether to compress values.
|
||||
* @return void
|
||||
* @throws \Throwable When a batch insert fails and the transaction is rolled back.
|
||||
*/
|
||||
public static function archive_batch( array $entries, bool $compress = false ): void {
|
||||
TMDO_DB::begin();
|
||||
try {
|
||||
foreach ( $entries as $entry ) {
|
||||
self::archive(
|
||||
(int) $entry['post_id'],
|
||||
$entry['post_type'],
|
||||
$entry['meta_key'],
|
||||
$entry['meta_value'],
|
||||
(int) ( $entry['meta_id'] ?? 0 ),
|
||||
$compress
|
||||
);
|
||||
}
|
||||
TMDO_DB::commit();
|
||||
} catch ( \Throwable $e ) {
|
||||
TMDO_DB::rollback();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve archived values for a post.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string|null $meta_key Optional specific meta_key filter.
|
||||
* @return array Array of [meta_key, meta_value, archived_at, compressed].
|
||||
*/
|
||||
public static function get( int $post_id, ?string $meta_key = null ): array {
|
||||
global $wpdb;
|
||||
$table = self::table();
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from TMDO_Zone_Archive::table() via TMDO_DB::table().
|
||||
if ( $meta_key ) {
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT meta_key, meta_value, compressed, archived_at FROM `{$table}` WHERE post_id = %d AND meta_key = %s ORDER BY archived_at DESC",
|
||||
$post_id,
|
||||
$meta_key
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
} else {
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT meta_key, meta_value, compressed, archived_at FROM `{$table}` WHERE post_id = %d ORDER BY archived_at DESC",
|
||||
$post_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
|
||||
// Decompress where needed.
|
||||
// Note: must iterate $rows directly (not $rows ?: []) to allow &$row to modify the original array.
|
||||
foreach ( $rows as &$row ) {
|
||||
if ( (int) $row['compressed'] && function_exists( 'gzdecode' ) ) {
|
||||
$decoded = base64_decode( $row['meta_value'] );
|
||||
if ( false !== $decoded ) {
|
||||
$decompressed = gzdecode( $decoded );
|
||||
if ( false !== $decompressed ) {
|
||||
$row['meta_value'] = $decompressed;
|
||||
}
|
||||
}
|
||||
}
|
||||
unset( $row['compressed'] );
|
||||
}
|
||||
|
||||
return $rows ?: array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore archived entries back to wp_postmeta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string|null $meta_key Optional meta_key filter (null = restore all).
|
||||
* @return int Number of entries restored.
|
||||
*/
|
||||
public static function restore( int $post_id, ?string $meta_key = null ): int {
|
||||
$entries = self::get( $post_id, $meta_key );
|
||||
$count = 0;
|
||||
|
||||
foreach ( $entries as $entry ) {
|
||||
update_post_meta( $post_id, $entry['meta_key'], $entry['meta_value'] );
|
||||
++$count;
|
||||
}
|
||||
|
||||
// Delete restored entries from archive.
|
||||
if ( $count > 0 ) {
|
||||
global $wpdb;
|
||||
$table = self::table();
|
||||
|
||||
if ( $meta_key ) {
|
||||
$wpdb->delete(
|
||||
$table,
|
||||
array(
|
||||
'post_id' => $post_id,
|
||||
'meta_key' => $meta_key,
|
||||
),
|
||||
array( '%d', '%s' )
|
||||
);
|
||||
} else {
|
||||
$wpdb->delete( $table, array( 'post_id' => $post_id ), array( '%d' ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all archived entries for a post.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function delete( int $post_id ): void {
|
||||
global $wpdb;
|
||||
$wpdb->delete( self::table(), array( 'post_id' => $post_id ), array( '%d' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweep: archive stale postmeta entries by age.
|
||||
*
|
||||
* Finds postmeta for trashed/deleted posts older than $days and archives them.
|
||||
*
|
||||
* @param int $days Minimum age in days.
|
||||
* @param bool $compress Whether to compress.
|
||||
* @param int $limit Maximum rows per sweep.
|
||||
* @return int Number of entries archived.
|
||||
*/
|
||||
public static function sweep( int $days = 90, bool $compress = true, int $limit = 500 ): int {
|
||||
global $wpdb;
|
||||
|
||||
$cutoff = gmdate( 'Y-m-d H:i:s', time() - ( $days * DAY_IN_SECONDS ) );
|
||||
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT pm.meta_id, pm.post_id, pm.meta_key, pm.meta_value, p.post_type
|
||||
FROM {$wpdb->postmeta} pm
|
||||
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
|
||||
WHERE p.post_status = 'trash'
|
||||
AND p.post_modified_gmt < %s
|
||||
LIMIT %d",
|
||||
$cutoff,
|
||||
$limit
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if ( ! $rows ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$entries = array();
|
||||
foreach ( $rows as $row ) {
|
||||
$entries[] = array(
|
||||
'post_id' => $row['post_id'],
|
||||
'post_type' => $row['post_type'],
|
||||
'meta_key' => $row['meta_key'],
|
||||
'meta_value' => $row['meta_value'],
|
||||
'meta_id' => $row['meta_id'],
|
||||
);
|
||||
}
|
||||
|
||||
self::archive_batch( $entries, $compress );
|
||||
|
||||
return count( $entries );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get archive statistics.
|
||||
*
|
||||
* @return array{total_rows: int, compressed_rows: int, post_types: array}
|
||||
*/
|
||||
public static function stats(): array {
|
||||
global $wpdb;
|
||||
$table = self::table();
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal self::table()
|
||||
$total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" );
|
||||
$compressed = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}` WHERE compressed = 1" );
|
||||
|
||||
$types = $wpdb->get_results(
|
||||
"SELECT post_type, COUNT(*) as cnt FROM `{$table}` GROUP BY post_type ORDER BY cnt DESC",
|
||||
ARRAY_A
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
|
||||
return array(
|
||||
'total_rows' => $total,
|
||||
'compressed_rows' => $compressed,
|
||||
'post_types' => $types ?: array(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user