# thinkrank/1.0.1/includes/database/class-database-schema.php

ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console &amp; Local SEO, version 1.0.1. 1,245 lines.

- Page: https://pluginprobe.com/plugins/thinkrank/1.0.1/code/includes/database/class-database-schema.php
- Raw: https://pluginprobe.com/plugins/thinkrank/1.0.1/raw/includes/database/class-database-schema.php
- Modified: 2025-08-10T07:35:04+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/thinkrank/1.0.1/code/includes/database/class-database-schema.php#L10-L20`.

```php
<?php
/**
 * Database Schema Manager Class
 *
 * Comprehensive database schema implementation for ThinkRank SEO plugin.
 * Creates and manages all 11 ThinkRank tables with proper indexes, constraints,
 * and WordPress-compliant database operations following 2025 best practices.
 *
 * Tables managed:
 * - SEO Tables (7): Settings, Analysis, Keywords, Schema, Social, Performance, Local
 * - AI/Core Tables (4): AI Cache, AI Usage, Content Briefs, SEO Scores
 *
 * @package ThinkRank
 * @subpackage Database
 * @since 1.0.0
 */

declare(strict_types=1);

namespace ThinkRank\Database;

/**
 * Database Schema Manager Class
 *
 * Handles creation, management, and optimization of all SEO database tables.
 * Implements WordPress database standards with proper indexing and constraints.
 *
 * @since 1.0.0
 */
class Database_Schema {

    /**
     * WordPress database instance
     *
     * @since 1.0.0
     * @var \wpdb
     */
    private \wpdb $wpdb;

    /**
     * Database version for schema tracking
     *
     * @since 1.0.0
     * @var string
     */
    private string $db_version = '1.0.0';

    /**
     * Database table definitions with specifications
     *
     * Consolidated table definitions for all ThinkRank tables (11 total):
     * - SEO Tables (7): Core SEO functionality with context-aware structure
     * - AI/Core Tables (4): AI caching, usage tracking, content briefs, and scoring
     *
     * @since 1.0.0
     * @var array
     */
    private array $table_definitions = [
        // === SEO TABLES (7) ===
        'seo_settings' => [
            'description' => 'Universal SEO settings storage with context-aware structure',
            'primary_key' => 'setting_id',
            'indexes' => ['context_type', 'context_id', 'setting_category', 'is_active'],
            'foreign_keys' => []
        ],
        'seo_analysis' => [
            'description' => 'SEO analysis results and scoring data',
            'primary_key' => 'analysis_id',
            'indexes' => ['context_type', 'context_id', 'analysis_type', 'created_at'],
            'composite_indexes' => [
                'context_analysis_date' => ['context_type', 'analysis_type', 'created_at'],
                'context_recent' => ['context_type', 'context_id', 'created_at']
            ],
            'foreign_keys' => []
        ],
        'seo_keywords' => [
            'description' => 'Keyword tracking and optimization data',
            'primary_key' => 'keyword_id',
            'indexes' => ['context_type', 'context_id', 'keyword_type', 'keyword_hash'],
            'foreign_keys' => []
        ],
        'seo_schema' => [
            'description' => 'Schema markup storage and validation',
            'primary_key' => 'schema_id',
            'indexes' => ['context_type', 'context_id', 'schema_type', 'is_active'],
            'foreign_keys' => []
        ],
        'seo_social' => [
            'description' => 'Social media meta and optimization data',
            'primary_key' => 'social_id',
            'indexes' => ['context_type', 'context_id', 'platform', 'is_active'],
            'foreign_keys' => []
        ],
        'seo_performance' => [
            'description' => 'Performance metrics and Core Web Vitals data',
            'primary_key' => 'performance_id',
            'indexes' => ['context_type', 'context_id', 'metric_type', 'measured_at'],
            'foreign_keys' => []
        ],
        'seo_local' => [
            'description' => 'Local SEO and business data storage',
            'primary_key' => 'local_id',
            'indexes' => ['context_type', 'context_id', 'business_type', 'is_active'],
            'foreign_keys' => []
        ],

        // === AI/CORE TABLES (4) ===
        'ai_cache' => [
            'description' => 'AI response caching for performance optimization',
            'primary_key' => 'id',
            'indexes' => ['cache_key', 'expires_at', 'created_at'],
            'composite_indexes' => [
                'cache_lookup' => ['cache_key', 'expires_at'],
                'cleanup_expired' => ['expires_at', 'created_at']
            ],
            'foreign_keys' => []
        ],
        'ai_usage' => [
            'description' => 'AI usage tracking and token consumption monitoring',
            'primary_key' => 'id',
            'indexes' => ['user_id', 'action', 'provider', 'created_at'],
            'composite_indexes' => [
                'user_analytics' => ['user_id', 'created_at', 'provider'],
                'provider_action' => ['provider', 'action', 'created_at'],
                'user_provider_date' => ['user_id', 'provider', 'created_at']
            ],
            'foreign_keys' => []
        ],
        'content_briefs' => [
            'description' => 'Generated content briefs storage and management',
            'primary_key' => 'id',
            'indexes' => ['user_id', 'content_type', 'created_at'],
            'composite_indexes' => [
                'user_content_date' => ['user_id', 'content_type', 'created_at'],
                'user_recent' => ['user_id', 'created_at']
            ],
            'foreign_keys' => []
        ],
        'seo_scores' => [
            'description' => 'SEO score calculations and historical tracking',
            'primary_key' => 'id',
            'indexes' => ['post_id', 'user_id', 'overall_score', 'grade', 'calculated_at', 'created_at'],
            'composite_indexes' => [
                'post_user_date' => ['post_id', 'user_id', 'created_at'],
                'user_score_date' => ['user_id', 'overall_score', 'created_at'],
                'post_latest' => ['post_id', 'calculated_at']
            ],
            'foreign_keys' => []
        ]
    ];

    /**
     * WordPress database charset and collation
     *
     * @since 1.0.0
     * @var array
     */
    private array $db_config;

    /**
     * Table categories for better organization and maintenance
     *
     * @since 1.0.0
     * @var array
     */
    private array $table_categories = [
        'seo' => ['seo_settings', 'seo_analysis', 'seo_keywords', 'seo_schema', 'seo_social', 'seo_performance', 'seo_local'],
        'ai' => ['ai_cache', 'ai_usage'],
        'content' => ['content_briefs'],
        'scoring' => ['seo_scores']
    ];

    /**
     * Constructor
     *
     * @since 1.0.0
     */
    public function __construct() {
        global $wpdb;
        $this->wpdb = $wpdb;
        
        // Set database configuration
        $this->db_config = [
            'charset' => $wpdb->charset ?: 'utf8mb4',
            'collate' => $wpdb->collate ?: 'utf8mb4_unicode_ci'
        ];
    }

    /**
     * Create all database tables
     *
     * @since 1.0.0
     *
     * @return array Creation results with success/failure status
     */
    public function create_tables(): array {
        $results = [
            'success' => true,
            'tables_created' => [],
            'tables_failed' => [],
            'errors' => [],
            'total_tables' => count($this->table_definitions)
        ];

        // Require WordPress upgrade functions
        if (!function_exists('dbDelta')) {
            require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        }

        foreach ($this->table_definitions as $table_name => $definition) {
            try {
                $full_table_name = $this->get_table_name($table_name);
                $sql = $this->get_table_sql($table_name);
                
                // Create table using dbDelta for WordPress compatibility
                $result = dbDelta($sql);
                
                // Verify table creation
                if ($this->table_exists($full_table_name)) {
                    $results['tables_created'][] = $full_table_name;
                    
                    // Create indexes
                    $this->create_table_indexes($table_name);
                    
                    // Add constraints if needed
                    $this->add_table_constraints($table_name);
                } else {
                    $results['tables_failed'][] = $full_table_name;
                    $results['errors'][] = "Failed to create table: {$full_table_name}";
                    $results['success'] = false;
                }
            } catch (\Exception $e) {
                $results['tables_failed'][] = $this->get_table_name($table_name);
                $results['errors'][] = "Error creating {$table_name}: " . $e->getMessage();
                $results['success'] = false;
            }
        }

        // Update database version
        if ($results['success']) {
            update_option('thinkrank_seo_db_version', $this->db_version);
            update_option('thinkrank_seo_db_created', current_time('mysql'));
        }

        return $results;
    }

    /**
     * Drop all database tables
     *
     * @since 1.0.0
     *
     * @return array Deletion results
     */
    public function drop_tables(): array {
        $results = [
            'success' => true,
            'tables_dropped' => [],
            'tables_failed' => [],
            'errors' => []
        ];

        foreach (array_keys($this->table_definitions) as $table_name) {
            try {
                $full_table_name = $this->get_table_name($table_name);
                
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Plugin deactivation requires direct schema changes, DDL cannot be prepared, table name is validated
                $result = $this->wpdb->query("DROP TABLE IF EXISTS `{$full_table_name}`");
                
                if ($result !== false) {
                    $results['tables_dropped'][] = $full_table_name;
                } else {
                    $results['tables_failed'][] = $full_table_name;
                    $results['errors'][] = "Failed to drop table: {$full_table_name}";
                    $results['success'] = false;
                }
            } catch (\Exception $e) {
                $results['tables_failed'][] = $this->get_table_name($table_name);
                $results['errors'][] = "Error dropping {$table_name}: " . $e->getMessage();
                $results['success'] = false;
            }
        }

        // Clean up options
        if ($results['success']) {
            delete_option('thinkrank_seo_db_version');
            delete_option('thinkrank_seo_db_created');
        }

        return $results;
    }

    /**
     * Check if database schema needs updates
     *
     * @since 1.0.0
     *
     * @return bool True if update needed
     */
    public function needs_update(): bool {
        $current_version = get_option('thinkrank_seo_db_version', '0.0.0');
        return version_compare($current_version, $this->db_version, '<');
    }

    /**
     * Get database status and information
     *
     * @since 1.0.0
     *
     * @return array Database status information
     */
    public function get_database_status(): array {
        $status = [
            'version' => get_option('thinkrank_seo_db_version', 'Not installed'),
            'created_at' => get_option('thinkrank_seo_db_created', 'Unknown'),
            'tables' => [],
            'total_records' => 0,
            'database_size' => 0,
            'needs_update' => $this->needs_update()
        ];

        foreach (array_keys($this->table_definitions) as $table_name) {
            $full_table_name = $this->get_table_name($table_name);
            $table_info = $this->get_table_info($full_table_name);
            
            $status['tables'][$table_name] = $table_info;
            $status['total_records'] += $table_info['row_count'];
            $status['database_size'] += $table_info['data_size'];
        }

        return $status;
    }

    /**
     * Optimize all database tables
     *
     * @since 1.0.0
     *
     * @return array Optimization results
     */
    public function optimize_tables(): array {
        $results = [
            'success' => true,
            'tables_optimized' => [],
            'tables_failed' => [],
            'space_saved' => 0,
            'errors' => []
        ];

        foreach (array_keys($this->table_definitions) as $table_name) {
            try {
                $full_table_name = $this->get_table_name($table_name);
                
                // Get table size before optimization
                $size_before = $this->get_table_size($full_table_name);
                
                // Optimize table
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table optimization requires direct database access, DDL cannot be prepared, table name is validated
                $result = $this->wpdb->query("OPTIMIZE TABLE `{$full_table_name}`");
                
                if ($result !== false) {
                    $size_after = $this->get_table_size($full_table_name);
                    $space_saved = $size_before - $size_after;
                    
                    $results['tables_optimized'][] = [
                        'table' => $full_table_name,
                        'space_saved' => $space_saved
                    ];
                    $results['space_saved'] += $space_saved;
                } else {
                    $results['tables_failed'][] = $full_table_name;
                    $results['errors'][] = "Failed to optimize table: {$full_table_name}";
                    $results['success'] = false;
                }
            } catch (\Exception $e) {
                $results['tables_failed'][] = $this->get_table_name($table_name);
                $results['errors'][] = "Error optimizing {$table_name}: " . $e->getMessage();
                $results['success'] = false;
            }
        }

        return $results;
    }

    /**
     * Get full table name with WordPress prefix
     *
     * @since 1.0.0
     *
     * @param string $table_name Base table name
     * @return string Full table name with prefix
     */
    private function get_table_name(string $table_name): string {
        return $this->wpdb->prefix . 'thinkrank_' . $table_name;
    }

    /**
     * Check if table exists
     *
     * @since 1.0.0
     *
     * @param string $table_name Full table name
     * @return bool True if table exists
     */
    private function table_exists(string $table_name): bool {
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema validation requires direct database access
        $result = $this->wpdb->get_var(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL is properly prepared with placeholders
            $this->wpdb->prepare("SHOW TABLES LIKE %s", $table_name)
        );

        return $result === $table_name;
    }

    /**
     * Get SQL for creating a specific table
     *
     * @since 1.0.0
     *
     * @param string $table_name Table name
     * @return string SQL for table creation
     */
    private function get_table_sql(string $table_name): string {
        $full_table_name = $this->get_table_name($table_name);
        $charset_collate = "DEFAULT CHARACTER SET {$this->db_config['charset']} COLLATE {$this->db_config['collate']}";

        switch ($table_name) {
            // SEO Tables
            case 'seo_settings':
                return $this->get_seo_settings_table_sql($full_table_name, $charset_collate);
            case 'seo_analysis':
                return $this->get_seo_analysis_table_sql($full_table_name, $charset_collate);
            case 'seo_keywords':
                return $this->get_seo_keywords_table_sql($full_table_name, $charset_collate);
            case 'seo_schema':
                return $this->get_seo_schema_table_sql($full_table_name, $charset_collate);
            case 'seo_social':
                return $this->get_seo_social_table_sql($full_table_name, $charset_collate);
            case 'seo_performance':
                return $this->get_seo_performance_table_sql($full_table_name, $charset_collate);
            case 'seo_local':
                return $this->get_seo_local_table_sql($full_table_name, $charset_collate);

            // AI/Core Tables
            case 'ai_cache':
                return $this->get_ai_cache_table_sql($full_table_name, $charset_collate);
            case 'ai_usage':
                return $this->get_ai_usage_table_sql($full_table_name, $charset_collate);
            case 'content_briefs':
                return $this->get_content_briefs_table_sql($full_table_name, $charset_collate);
            case 'seo_scores':
                return $this->get_seo_scores_table_sql($full_table_name, $charset_collate);

            default:
                throw new \InvalidArgumentException('Unknown table: ' . esc_html($table_name));
        }
    }

    /**
     * Get SQL for SEO Settings table
     *
     * @since 1.0.0
     *
     * @param string $table_name    Full table name
     * @param string $charset_collate Charset and collation
     * @return string SQL for table creation
     */
    private function get_seo_settings_table_sql(string $table_name, string $charset_collate): string {
        return "CREATE TABLE `{$table_name}` (
            setting_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            context_type varchar(50) NOT NULL DEFAULT 'site',
            context_id bigint(20) unsigned NULL,
            setting_category varchar(100) NOT NULL DEFAULT 'general',
            setting_key varchar(255) NOT NULL,
            setting_value longtext NULL,
            setting_type varchar(50) NOT NULL DEFAULT 'string',
            is_active tinyint(1) NOT NULL DEFAULT 1,
            priority int(11) NOT NULL DEFAULT 0,
            created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            created_by bigint(20) unsigned NULL,
            updated_by bigint(20) unsigned NULL,
            PRIMARY KEY (setting_id),
            UNIQUE KEY unique_setting (context_type, context_id, setting_category, setting_key),
            KEY idx_context (context_type, context_id),
            KEY idx_category (setting_category),
            KEY idx_active (is_active),
            KEY idx_created (created_at),
            KEY idx_updated (updated_at),
            KEY idx_context_cat_active (context_type, context_id, setting_category, is_active)
        ) {$charset_collate};";
    }

    /**
     * Get SQL for SEO Analysis table
     *
     * @since 1.0.0
     *
     * @param string $table_name    Full table name
     * @param string $charset_collate Charset and collation
     * @return string SQL for table creation
     */
    private function get_seo_analysis_table_sql(string $table_name, string $charset_collate): string {
        return "CREATE TABLE `{$table_name}` (
            analysis_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            context_type varchar(50) NOT NULL DEFAULT 'site',
            context_id bigint(20) unsigned NULL,
            analysis_type varchar(100) NOT NULL,
            analysis_data longtext NULL,
            score int(11) NOT NULL DEFAULT 0,
            status varchar(50) NOT NULL DEFAULT 'pending',
            ai_confidence decimal(3,2) NULL,
            recommendations longtext NULL,
            validation_errors longtext NULL,
            created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            analyzed_by bigint(20) unsigned NULL,
            PRIMARY KEY (analysis_id),
            KEY idx_context (context_type, context_id),
            KEY idx_type (analysis_type),
            KEY idx_status (status),
            KEY idx_score (score),
            KEY idx_created (created_at),
            KEY idx_confidence (ai_confidence)
        ) {$charset_collate};";
    }

    /**
     * Get SQL for SEO Keywords table
     *
     * @since 1.0.0
     *
     * @param string $table_name    Full table name
     * @param string $charset_collate Charset and collation
     * @return string SQL for table creation
     */
    private function get_seo_keywords_table_sql(string $table_name, string $charset_collate): string {
        return "CREATE TABLE `{$table_name}` (
            keyword_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            context_type varchar(50) NOT NULL DEFAULT 'site',
            context_id bigint(20) unsigned NULL,
            keyword_text varchar(500) NOT NULL,
            keyword_hash varchar(64) NOT NULL,
            keyword_type varchar(50) NOT NULL DEFAULT 'primary',
            search_volume int(11) NULL,
            competition_score decimal(3,2) NULL,
            difficulty_score decimal(3,2) NULL,
            density decimal(5,2) NULL,
            position int(11) NULL,
            ranking_url varchar(2048) NULL,
            is_tracking tinyint(1) NOT NULL DEFAULT 0,
            is_active tinyint(1) NOT NULL DEFAULT 1,
            created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            tracked_by bigint(20) unsigned NULL,
            PRIMARY KEY (keyword_id),
            UNIQUE KEY unique_keyword (context_type, context_id, keyword_hash),
            KEY idx_context (context_type, context_id),
            KEY idx_type (keyword_type),
            KEY idx_hash (keyword_hash),
            KEY idx_tracking (is_tracking),
            KEY idx_active (is_active),
            KEY idx_position (position),
            KEY idx_created (created_at),
            FULLTEXT KEY ft_keyword (keyword_text)
        ) {$charset_collate};";
    }

    /**
     * Get SQL for SEO Schema table (Optimized Version 2.0)
     *
     * @since 1.0.0
     * @updated 2.0.0 - Optimized structure with fewer columns and better indexes
     *
     * @param string $table_name    Full table name
     * @param string $charset_collate Charset and collation
     * @return string SQL for table creation
     */
    private function get_seo_schema_table_sql(string $table_name, string $charset_collate): string {
        // Check MySQL version for JSON column support with caching
        $schema_data_type = $this->get_mysql_json_support() ? 'JSON' : 'longtext';

        return "CREATE TABLE `{$table_name}` (
            schema_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            context_type varchar(50) NOT NULL DEFAULT 'site',
            context_id bigint(20) unsigned NULL,
            schema_type varchar(100) NOT NULL,
            schema_data {$schema_data_type} NOT NULL,
            validation_status varchar(50) NOT NULL DEFAULT 'pending',
            is_active tinyint(1) NOT NULL DEFAULT 1,
            created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            PRIMARY KEY (schema_id),
            KEY idx_context_active (context_type, context_id, is_active),
            KEY idx_type_active (schema_type, is_active),
            KEY idx_created (created_at),
            KEY idx_context_schema_active (context_type, schema_type, is_active, created_at DESC),
            KEY idx_validation_active (validation_status, is_active)
        ) {$charset_collate};";
    }

    /**
     * Get SQL for SEO Social table
     *
     * @since 1.0.0
     *
     * @param string $table_name    Full table name
     * @param string $charset_collate Charset and collation
     * @return string SQL for table creation
     */
    private function get_seo_social_table_sql(string $table_name, string $charset_collate): string {
        return "CREATE TABLE `{$table_name}` (
            social_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            context_type varchar(50) NOT NULL DEFAULT 'site',
            context_id bigint(20) unsigned NULL,
            platform varchar(50) NOT NULL,
            meta_type varchar(100) NOT NULL,
            meta_key varchar(255) NOT NULL,
            meta_value longtext NULL,
            image_url varchar(2048) NULL,
            image_width int(11) NULL,
            image_height int(11) NULL,
            is_optimized tinyint(1) NOT NULL DEFAULT 0,
            is_active tinyint(1) NOT NULL DEFAULT 1,
            created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            created_by bigint(20) unsigned NULL,
            PRIMARY KEY (social_id),
            UNIQUE KEY unique_social_meta (context_type, context_id, platform, meta_key),
            KEY idx_context (context_type, context_id),
            KEY idx_platform (platform),
            KEY idx_type (meta_type),
            KEY idx_optimized (is_optimized),
            KEY idx_active (is_active),
            KEY idx_created (created_at)
        ) {$charset_collate};";
    }

    /**
     * Get SQL for SEO Performance table
     *
     * @since 1.0.0
     *
     * @param string $table_name    Full table name
     * @param string $charset_collate Charset and collation
     * @return string SQL for table creation
     */
    private function get_seo_performance_table_sql(string $table_name, string $charset_collate): string {
        return "CREATE TABLE `{$table_name}` (
            performance_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            context_type varchar(50) NOT NULL DEFAULT 'site',
            context_id bigint(20) unsigned NULL,
            metric_type varchar(100) NOT NULL,
            metric_value decimal(10,4) NOT NULL,
            metric_unit varchar(50) NOT NULL DEFAULT 'score',
            threshold_good decimal(10,4) NULL,
            threshold_poor decimal(10,4) NULL,
            status varchar(50) NOT NULL DEFAULT 'unknown',
            device_type varchar(20) NOT NULL DEFAULT 'desktop',
            connection_type varchar(50) NULL,
            measured_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
            created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
            measured_by varchar(100) NULL,
            PRIMARY KEY (performance_id),
            KEY idx_context (context_type, context_id),
            KEY idx_metric (metric_type),
            KEY idx_status (status),
            KEY idx_device (device_type),
            KEY idx_measured (measured_at),
            KEY idx_created (created_at),
            KEY idx_value (metric_value)
        ) {$charset_collate};";
    }

    /**
     * Get SQL for SEO Local table
     *
     * @since 1.0.0
     *
     * @param string $table_name    Full table name
     * @param string $charset_collate Charset and collation
     * @return string SQL for table creation
     */
    private function get_seo_local_table_sql(string $table_name, string $charset_collate): string {
        return "CREATE TABLE `{$table_name}` (
            local_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            context_type varchar(50) NOT NULL DEFAULT 'site',
            context_id bigint(20) unsigned NULL,
            business_type varchar(100) NOT NULL DEFAULT 'LocalBusiness',
            business_name varchar(255) NOT NULL,
            business_address longtext NULL,
            business_phone varchar(50) NULL,
            business_email varchar(255) NULL,
            business_website varchar(2048) NULL,
            latitude decimal(10,8) NULL,
            longitude decimal(11,8) NULL,
            google_place_id varchar(255) NULL,
            google_my_business_url varchar(2048) NULL,
            business_hours longtext NULL,
            nap_consistency_score int(11) NOT NULL DEFAULT 0,
            local_seo_score int(11) NOT NULL DEFAULT 0,
            is_verified tinyint(1) NOT NULL DEFAULT 0,
            is_active tinyint(1) NOT NULL DEFAULT 1,
            created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            created_by bigint(20) unsigned NULL,
            PRIMARY KEY (local_id),
            UNIQUE KEY unique_business (context_type, context_id),
            KEY idx_context (context_type, context_id),
            KEY idx_type (business_type),
            KEY idx_location (latitude, longitude),
            KEY idx_verified (is_verified),
            KEY idx_active (is_active),
            KEY idx_nap_score (nap_consistency_score),
            KEY idx_local_score (local_seo_score),
            KEY idx_created (created_at)
        ) {$charset_collate};";
    }

    /**
     * Get SQL for AI Cache table
     *
     * @since 1.0.0
     *
     * @param string $table_name    Full table name
     * @param string $charset_collate Charset and collation
     * @return string SQL for table creation
     */
    private function get_ai_cache_table_sql(string $table_name, string $charset_collate): string {
        return "CREATE TABLE `{$table_name}` (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            cache_key varchar(255) NOT NULL,
            cache_data longtext NOT NULL,
            expires_at bigint(20) unsigned NOT NULL,
            created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (id),
            UNIQUE KEY cache_key (cache_key),
            KEY expires_at_idx (expires_at),
            KEY created_at_idx (created_at)
        ) {$charset_collate};";
    }

    /**
     * Get SQL for AI Usage table
     *
     * @since 1.0.0
     *
     * @param string $table_name    Full table name
     * @param string $charset_collate Charset and collation
     * @return string SQL for table creation
     */
    private function get_ai_usage_table_sql(string $table_name, string $charset_collate): string {
        return "CREATE TABLE `{$table_name}` (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            user_id bigint(20) unsigned NOT NULL,
            action varchar(100) NOT NULL,
            tokens_used int(11) NOT NULL DEFAULT 0,
            provider varchar(50) NOT NULL,
            post_id bigint(20) unsigned NULL,
            metadata longtext NULL,
            created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (id),
            KEY user_id_idx (user_id),
            KEY action_idx (action),
            KEY created_at_idx (created_at),
            KEY provider_idx (provider)
        ) {$charset_collate};";
    }

    /**
     * Get SQL for Content Briefs table
     *
     * @since 1.0.0
     *
     * @param string $table_name    Full table name
     * @param string $charset_collate Charset and collation
     * @return string SQL for table creation
     */
    private function get_content_briefs_table_sql(string $table_name, string $charset_collate): string {
        return "CREATE TABLE `{$table_name}` (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            user_id bigint(20) unsigned NOT NULL,
            title varchar(255) NOT NULL,
            target_keywords text NOT NULL,
            content_type varchar(50) NOT NULL DEFAULT 'blog_post',
            brief_data longtext NOT NULL,
            parsing_status varchar(50) NOT NULL DEFAULT 'success',
            created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            PRIMARY KEY (id),
            KEY user_id_idx (user_id),
            KEY created_at_idx (created_at),
            KEY content_type_idx (content_type),
            KEY parsing_status_idx (parsing_status)
        ) {$charset_collate};";
    }

    /**
     * Get SQL for SEO Scores table
     *
     * @since 1.0.0
     *
     * @param string $table_name    Full table name
     * @param string $charset_collate Charset and collation
     * @return string SQL for table creation
     */
    private function get_seo_scores_table_sql(string $table_name, string $charset_collate): string {
        return "CREATE TABLE `{$table_name}` (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            post_id bigint(20) unsigned NOT NULL,
            user_id bigint(20) unsigned NOT NULL,
            overall_score int(11) NOT NULL,
            score_breakdown longtext NOT NULL,
            suggestions longtext NOT NULL,
            grade varchar(2) NOT NULL,
            algorithm_version varchar(20) NOT NULL DEFAULT '2024.1',
            calculated_at datetime NOT NULL,
            created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (id),
            KEY post_id_idx (post_id),
            KEY user_id_idx (user_id),
            KEY created_at_idx (created_at),
            KEY overall_score_idx (overall_score)
        ) {$charset_collate};";
    }

    /**
     * Create indexes for a specific table
     *
     * @since 1.0.0
     *
     * @param string $table_name Table name
     * @return bool Success status
     */
    private function create_table_indexes(string $table_name): bool {
        $full_table_name = $this->get_table_name($table_name);
        $definition = $this->table_definitions[$table_name] ?? [];

        $success = true;

        // Create single column indexes
        if (!empty($definition['indexes'])) {
            foreach ($definition['indexes'] as $index_name) {
                try {
                    // Check if index already exists
                    if ($this->index_exists($full_table_name, $index_name)) {
                        continue;
                    }

                    $index_sql = $this->get_index_sql($full_table_name, $index_name);
                    if ($index_sql) {
                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.NotPrepared -- Index creation requires direct schema changes, DDL cannot be prepared
                        $result = $this->wpdb->query($index_sql);
                        if (false === $result) {
                            $success = false;
                        }
                    }
                } catch (\Exception $e) {
                    $success = false;
                }
            }
        }

        // Create composite indexes for performance optimization
        if (!empty($definition['composite_indexes'])) {
            foreach ($definition['composite_indexes'] as $index_name => $columns) {
                try {
                    // Check if index already exists
                    if ($this->index_exists($full_table_name, "idx_{$index_name}")) {
                        continue;
                    }

                    $index_sql = $this->get_composite_index_sql($full_table_name, $index_name, $columns);
                    if ($index_sql) {
                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.NotPrepared -- Composite index creation requires direct schema changes, DDL cannot be prepared
                        $result = $this->wpdb->query($index_sql);
                        if (false === $result) {
                            $success = false;
                        }
                    }
                } catch (\Exception $e) {
                    $success = false;
                }
            }
        }

        return $success;
    }

    /**
     * Add constraints for a specific table
     *
     * @since 1.0.0
     *
     * @param string $table_name Table name
     * @return bool Success status
     */
    private function add_table_constraints(string $table_name): bool {
        $full_table_name = $this->get_table_name($table_name);
        $definition = $this->table_definitions[$table_name] ?? [];

        if (empty($definition['foreign_keys'])) {
            return true;
        }

        $success = true;
        foreach ($definition['foreign_keys'] as $constraint) {
            try {
                $constraint_sql = $this->get_constraint_sql($full_table_name, $constraint);
                if ($constraint_sql) {
                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.NotPrepared -- Constraint creation requires direct schema changes, DDL cannot be prepared
                    $result = $this->wpdb->query($constraint_sql);
                    if (false === $result) {
                        $success = false;
                    }
                }
            } catch (\Exception $e) {
                $success = false;
            }
        }

        return $success;
    }

    /**
     * Get index SQL for a table
     *
     * @since 1.0.0
     *
     * @param string $table_name Full table name
     * @param string $index_name Index name
     * @return string Index SQL
     */
    private function get_index_sql(string $table_name, string $index_name): string {
        // Most indexes are already created in the table definition
        // This method is for additional indexes if needed
        return '';
    }

    /**
     * Get composite index SQL for performance optimization
     *
     * @since 1.0.0
     *
     * @param string $table_name Full table name
     * @param string $index_name Index name
     * @param array $columns Column names for composite index
     * @return string Composite index SQL
     */
    private function get_composite_index_sql(string $table_name, string $index_name, array $columns): string {
        if (empty($columns)) {
            return '';
        }

        // Escape column names
        $escaped_columns = array_map(function($column) {
            return "`{$column}`";
        }, $columns);

        $columns_sql = implode(', ', $escaped_columns);
        $index_name_escaped = esc_sql($index_name);

        return "CREATE INDEX `idx_{$index_name_escaped}` ON `{$table_name}` ({$columns_sql})";
    }

    /**
     * Get constraint SQL for a table
     *
     * @since 1.0.0
     *
     * @param string $table_name Full table name
     * @param array  $constraint Constraint definition
     * @return string Constraint SQL
     */
    private function get_constraint_sql(string $table_name, array $constraint): string {
        // Foreign key constraints would be defined here
        // Currently not implemented as tables are designed to be independent
        return '';
    }

    /**
     * Get table information
     *
     * @since 1.0.0
     *
     * @param string $table_name Full table name
     * @return array Table information
     */
    private function get_table_info(string $table_name): array {
        $info = [
            'exists' => false,
            'row_count' => 0,
            'data_size' => 0,
            'index_size' => 0,
            'total_size' => 0,
            'created' => null,
            'updated' => null
        ];

        if (!$this->table_exists($table_name)) {
            return $info;
        }

        $info['exists'] = true;

        // Get row count
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table statistics require direct database access, table name cannot be prepared, table name is validated
        $row_count = $this->wpdb->get_var("SELECT COUNT(*) FROM `{$table_name}`");
        $info['row_count'] = (int) $row_count;

        // Get table size information
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Table size information requires direct database access
        $size_info = $this->wpdb->get_row(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL is properly prepared with placeholders
            $this->wpdb->prepare(
                "SELECT
                    data_length as data_size,
                    index_length as index_size,
                    (data_length + index_length) as total_size,
                    create_time as created,
                    update_time as updated
                FROM information_schema.TABLES
                WHERE table_schema = %s AND table_name = %s",
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- DB_NAME is a WordPress constant, safe to use
                DB_NAME,
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $table_name is validated and used as parameter
                $table_name
            ),
            ARRAY_A
        );

        if ($size_info) {
            $info['data_size'] = (int) $size_info['data_size'];
            $info['index_size'] = (int) $size_info['index_size'];
            $info['total_size'] = (int) $size_info['total_size'];
            $info['created'] = $size_info['created'];
            $info['updated'] = $size_info['updated'];
        }

        return $info;
    }

    /**
     * Get table size in bytes
     *
     * @since 1.0.0
     *
     * @param string $table_name Full table name
     * @return int Table size in bytes
     */
    private function get_table_size(string $table_name): int {
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Table size calculation requires direct database access
        $size = $this->wpdb->get_var(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL is properly prepared with placeholders
            $this->wpdb->prepare(
                "SELECT (data_length + index_length) as total_size
                FROM information_schema.TABLES
                WHERE table_schema = %s AND table_name = %s",
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- DB_NAME is a WordPress constant, safe to use
                DB_NAME,
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $table_name is validated and used as parameter
                $table_name
            )
        );

        return (int) $size;
    }

    /**
     * Get tables by category for better organization
     *
     * @since 1.0.0
     *
     * @param string $category Category name (seo, ai, content, scoring)
     * @return array Table names in the category
     */
    public function get_tables_by_category(string $category): array {
        return $this->table_categories[$category] ?? [];
    }

    /**
     * Get all table categories
     *
     * @since 1.0.0
     *
     * @return array All table categories with their tables
     */
    public function get_table_categories(): array {
        return $this->table_categories;
    }

    /**
     * Get table count by category
     *
     * @since 1.0.0
     *
     * @return array Table counts per category
     */
    public function get_table_count_by_category(): array {
        $counts = [];
        foreach ($this->table_categories as $category => $tables) {
            $counts[$category] = count($tables);
        }
        $counts['total'] = count($this->table_definitions);
        return $counts;
    }

    /**
     * Validate table definition structure
     *
     * @since 1.0.0
     *
     * @param string $table_name Table name to validate
     * @return array Validation results
     */
    public function validate_table_definition(string $table_name): array {
        $definition = $this->table_definitions[$table_name] ?? null;

        if (!$definition) {
            return [
                'valid' => false,
                'errors' => ["Table definition not found: {$table_name}"]
            ];
        }

        $errors = [];
        $required_keys = ['description', 'primary_key', 'indexes', 'foreign_keys'];

        foreach ($required_keys as $key) {
            if (!isset($definition[$key])) {
                $errors[] = "Missing required key '{$key}' in table definition for {$table_name}";
            }
        }

        return [
            'valid' => empty($errors),
            'errors' => $errors
        ];
    }

    /**
     * Add composite indexes to existing tables for performance optimization
     *
     * @since 1.0.0
     *
     * @return bool Success status
     */
    public function add_performance_indexes(): bool {
        $success = true;

        foreach ($this->table_definitions as $table_name => $definition) {
            if (!empty($definition['composite_indexes'])) {
                $full_table_name = $this->get_table_name($table_name);

                // Check if table exists before adding indexes
                if (!$this->table_exists($full_table_name)) {
                    continue;
                }

                foreach ($definition['composite_indexes'] as $index_name => $columns) {
                    try {
                        // Check if index already exists
                        if ($this->index_exists($full_table_name, "idx_{$index_name}")) {
                            continue;
                        }

                        $index_sql = $this->get_composite_index_sql($full_table_name, $index_name, $columns);
                        if ($index_sql) {
                            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.NotPrepared -- Performance index creation requires direct schema changes, DDL cannot be prepared
                            $result = $this->wpdb->query($index_sql);
                            if (false === $result) {
                                $success = false;
                                // Index creation failed - logged in database operations
                            }
                        }
                    } catch (\Exception $e) {
                        $success = false;
                        // Exception during index creation - logged in database operations
                    }
                }
            }
        }

        return $success;
    }

    /**
     * Check if an index exists on a table
     *
     * @since 1.0.0
     *
     * @param string $table_name Full table name
     * @param string $index_name Index name
     * @return bool Whether index exists
     */
    private function index_exists(string $table_name, string $index_name): bool {
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Index existence check requires direct database access
        $result = $this->wpdb->get_var(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL is properly prepared with placeholders
            $this->wpdb->prepare(
                "SELECT COUNT(*) FROM information_schema.statistics
                 WHERE table_schema = %s AND table_name = %s AND index_name = %s",
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- DB_NAME is a WordPress constant, safe to use
                DB_NAME,
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $table_name is validated and used as parameter
                $table_name,
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $index_name is validated and used as parameter
                $index_name
            )
        );

        return (int) $result > 0;
    }

    /**
     * Check if MySQL supports JSON column type with caching
     *
     * Uses WordPress's built-in database version detection and caches the result
     * to avoid repeated database queries during schema creation.
     *
     * @since 1.0.0
     * @return bool True if MySQL 5.7+ supports JSON columns
     */
    private function get_mysql_json_support(): bool {
        // Check if we have cached result
        static $json_support = null;

        if ($json_support !== null) {
            return $json_support;
        }

        // Use WordPress's built-in database version method
        global $wpdb;

        // Get MySQL version using WordPress method (safer than direct query)
        $mysql_version = $wpdb->db_version();

        // Cache the result for subsequent calls
        $json_support = version_compare($mysql_version, '5.7.0', '>=');

        return $json_support;
    }

}

```
