Reservations -> DB Check * * @since 2.3.4 */ class ALEXR_Database_Check { public function __construct() { add_action( 'admin_menu', [ $this, 'register_menu' ], 20 ); } /* ------------------------------------------------------------------ */ /* MENU */ /* ------------------------------------------------------------------ */ public function register_menu() { add_submenu_page( 'admin-alex-reservations', __( 'DB Check', 'alexr' ), __( 'DB Check', 'alexr' ), 'manage_options', 'alexr-database-check', [ $this, 'render_page' ] ); } /* ------------------------------------------------------------------ */ /* TABLES LIST (same as install.php) */ /* ------------------------------------------------------------------ */ private function get_expected_tables(): array { return [ 'restaurants', 'restaurant_meta', 'restaurant_setting', 'users', 'user_meta', 'customers', 'customer_meta', 'ctaggroups', 'ctags', 'customer_ctag', 'bookings', 'booking_meta', 'booking_notifications', 'booking_reviews', 'btaggroups', 'btags', 'booking_btag', 'restaurant_user', 'settings', 'notifications', 'payments', 'floors', 'areas', 'tables', 'combinations', 'combination_table', 'booking_table', 'roles', 'actions', 'daily_notifications', 'tokens', 'bookings_recurring', 'ttaggroups', 'ttags', 'table_ttag', 'layouts', 'automation_logs', 'booking_tracking', 'whatsapp_flows', 'whatsapp_conversations', 'whatsapp_messages', 'whatsapp_webhooks', 'api_keys', 'api_logs', 'api_keys_bot', 'api_logs_bot', 'url_shorteners', 'missed_call_logs', 'spaces', 'vouchers', 'voucher_redemptions' ]; } /* ------------------------------------------------------------------ */ /* EXPECTED COLUMNS */ /* Parsed from sql_create() + sql_update_columns_releases() */ /* ------------------------------------------------------------------ */ /** * Return ALL expected columns for a table: base (CREATE TABLE) plus * any columns ADDed by migrations, minus any DROPped by migrations. */ private function get_expected_columns( string $table_key ): array { $db_instance = WP_ALEXR()->{$table_key} ?? null; if ( ! $db_instance || ! method_exists( $db_instance, 'sql_create' ) ) { return []; } // 1. Base columns from sql_create() $sql = $db_instance->sql_create(); $columns = ! empty( $sql ) ? $this->parse_columns_from_create_sql( $sql ) : []; // 2. Read the source of sql_update_columns_releases() and apply ADD / DROP if ( method_exists( $db_instance, 'sql_update_columns_releases' ) ) { $source = $this->get_method_source( $db_instance, 'sql_update_columns_releases' ); if ( $source !== '' ) { $columns = $this->apply_migration_columns( $columns, $source ); } } return array_values( array_unique( $columns ) ); } /** * Parse column names from a CREATE TABLE statement. * Ignores PRIMARY KEY, KEY, CONSTRAINT, UNIQUE KEY, INDEX lines. */ private function parse_columns_from_create_sql( string $sql ): array { $columns = []; // Get everything between the first ( and the last ) if ( ! preg_match( '/CREATE TABLE[^(]+\((.+)\)\s*$/s', $sql, $m ) ) { return $columns; } $body = $m[1]; $lines = preg_split( '/\n/', $body ); foreach ( $lines as $line ) { $line = trim( $line, " \t\r\n," ); if ( $line === '' ) continue; // Skip non-column definitions $upper = strtoupper( $line ); if ( str_starts_with( $upper, 'PRIMARY KEY' ) || str_starts_with( $upper, 'KEY ' ) || str_starts_with( $upper, 'UNIQUE KEY' ) || str_starts_with( $upper, 'CONSTRAINT' ) || str_starts_with( $upper, 'INDEX' ) || str_starts_with( $upper, 'FOREIGN KEY' ) ) { continue; } // First token is the column name (may be back-ticked) if ( preg_match( '/^`?(\w+)`?\s+/', $line, $cm ) ) { $columns[] = strtolower( $cm[1] ); } } return $columns; } /** * Use Reflection to read the PHP source code of a method. */ private function get_method_source( object $instance, string $method ): string { try { $ref = new ReflectionMethod( $instance, $method ); $file = $ref->getFileName(); $start = $ref->getStartLine(); $end = $ref->getEndLine(); if ( ! $file || ! is_readable( $file ) ) { return ''; } $lines = file( $file ); if ( $lines === false ) { return ''; } // getStartLine / getEndLine are 1-based return implode( '', array_slice( $lines, $start - 1, $end - $start + 1 ) ); } catch ( \ReflectionException $e ) { return ''; } } /** * Parse ALTER TABLE ADD / DROP COLUMN from the raw PHP source of * sql_update_columns_releases() and apply them to the column list. * * Recognised patterns: * ALTER TABLE ... ADD [COLUMN] `col_name` ... * ALTER TABLE ... DROP COLUMN `col_name` */ private function apply_migration_columns( array $columns, string $source ): array { // ADD [COLUMN] `column_name` or ADD column_name if ( preg_match_all( '/ALTER\s+TABLE\s+.+?\s+ADD\s+(?:COLUMN\s+)?`?(\w+)`?\s+/i', $source, $matches ) ) { foreach ( $matches[1] as $col ) { $col = strtolower( $col ); // Skip non-column keywords that might match if ( in_array( $col, [ 'index', 'key', 'unique', 'primary', 'constraint', 'foreign' ], true ) ) { continue; } if ( ! in_array( $col, $columns, true ) ) { $columns[] = $col; } } } // DROP COLUMN `column_name` if ( preg_match_all( '/ALTER\s+TABLE\s+.+?\s+DROP\s+COLUMN\s+`?(\w+)`?/i', $source, $matches ) ) { foreach ( $matches[1] as $col ) { $col = strtolower( $col ); $columns = array_filter( $columns, fn( $c ) => $c !== $col ); } } return $columns; } /* ------------------------------------------------------------------ */ /* LIVE DATABASE INSPECTION */ /* ------------------------------------------------------------------ */ private function get_full_table_name( string $table_key ): string { $db_instance = WP_ALEXR()->{$table_key} ?? null; if ( $db_instance ) { return $db_instance->table_name; } // Fallback global $wpdb; return $wpdb->prefix . EVAVEL_DB_NAMESPACE . '_' . $table_key; } private function table_exists_in_db( string $full_name ): bool { global $wpdb; return $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $full_name ) ) === $full_name; } /** * Return associative array of column_name => column_type for a live table. */ private function get_live_columns( string $full_name ): array { global $wpdb; $rows = $wpdb->get_results( "SHOW COLUMNS FROM `{$full_name}`" ); $columns = []; foreach ( $rows as $row ) { $columns[ strtolower( $row->Field ) ] = $row->Type; } return $columns; } /** * Return charset & collation of a table. */ private function get_table_charset_info( string $full_name ): array { global $wpdb; $row = $wpdb->get_row( "SELECT TABLE_COLLATION, ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '{$full_name}'" ); if ( ! $row ) { return [ 'charset' => '?', 'collation' => '?', 'engine' => '?' ]; } $collation = $row->TABLE_COLLATION ?? '?'; if ( str_starts_with( $collation, 'utf8mb4' ) ) { $charset = 'utf8mb4'; } elseif ( str_starts_with( $collation, 'utf8' ) ) { $charset = 'utf8 (utf8mb3)'; } elseif ( str_starts_with( $collation, 'latin' ) ) { $charset = 'latin1'; } else { $charset = strtok( $collation, '_' ); } return [ 'charset' => $charset, 'collation' => $collation, 'engine' => $row->ENGINE ?? '?', ]; } /* ------------------------------------------------------------------ */ /* BUILD THE FULL REPORT */ /* ------------------------------------------------------------------ */ private function build_report(): array { $tables = $this->get_expected_tables(); $report = []; foreach ( $tables as $table_key ) { $full_name = $this->get_full_table_name( $table_key ); $exists = $this->table_exists_in_db( $full_name ); $entry = [ 'key' => $table_key, 'full_name' => $full_name, 'exists' => $exists, 'charset' => '—', 'collation' => '—', 'engine' => '—', 'missing_columns' => [], 'extra_columns' => [], 'expected_columns' => [], 'live_columns' => [], 'status' => 'missing', ]; if ( $exists ) { $charset_info = $this->get_table_charset_info( $full_name ); $entry['charset'] = $charset_info['charset']; $entry['collation'] = $charset_info['collation']; $entry['engine'] = $charset_info['engine']; $expected = $this->get_expected_columns( $table_key ); $live = $this->get_live_columns( $full_name ); $live_names = array_keys( $live ); $entry['expected_columns'] = $expected; $entry['live_columns'] = $live; $entry['missing_columns'] = array_values( array_diff( $expected, $live_names ) ); $entry['extra_columns'] = array_values( array_diff( $live_names, $expected ) ); if ( empty( $entry['missing_columns'] ) ) { $entry['status'] = 'ok'; } else { $entry['status'] = 'columns_missing'; } } $report[] = $entry; } return $report; } /* ------------------------------------------------------------------ */ /* RENDER */ /* ------------------------------------------------------------------ */ public function render_page() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( __( 'You do not have permission to access this page.', 'alexr' ) ); } $report = $this->build_report(); $total = count( $report ); $ok = count( array_filter( $report, fn( $r ) => $r['status'] === 'ok' ) ); $missing = count( array_filter( $report, fn( $r ) => $r['status'] === 'missing' ) ); $issues = $total - $ok - $missing; ?>

Alex Reservations — Database Integrity Check

Tables:  |  OK:  |  Missing tables:  |  Column issues: All tables are correct

# Table Exists Charset Engine Columns Issues

✓ Yes ✗ No
—
/ — Table does not exist Missing columns: ✓
Extra columns:

Generated at — Plugin v (Release )