cacheManager = $cacheManager; $this->initializeOptimizer(); } /** * Execute optimized query with caching and performance monitoring * * @param string $sql SQL query * @param array $params Query parameters * @param array $options Execution options * @return array Query results * @since 1.0.0 */ public function executeQuery(string $sql, array $params = [], array $options = []): array { $startTime = microtime(true); $cacheKey = $this->generateQueryCacheKey($sql, $params); // Try cache first if enabled if ($options['use_cache'] ?? true) { $cachedResult = $this->cacheManager->get( "query_{$cacheKey}", null, $this->determineCacheTTL($sql, $options) ); if ($cachedResult !== null) { $this->recordQueryMetric($sql, microtime(true) - $startTime, true); return $cachedResult; } } // Execute query with optimization $result = $this->executeOptimizedQuery($sql, $params, $options); // Cache result if appropriate if (($options['use_cache'] ?? true) && $this->shouldCacheQuery($sql, $result)) { $this->cacheManager->set( "query_{$cacheKey}", $result, $this->determineCacheTTL($sql, $options) ); } $executionTime = (microtime(true) - $startTime) * 1000; $this->recordQueryMetric($sql, $executionTime, false); return $result; } /** * Get restrictions with optimized query and intelligent caching * * @param array $filters Query filters * @param array $options Query options * @return array Restrictions data * @since 1.0.0 */ public function getRestrictions(array $filters = [], array $options = []): array { $sql = $this->buildOptimizedRestrictionsQuery($filters, $options); $params = $this->extractQueryParameters($filters); return $this->executeQuery($sql, $params, [ 'use_cache' => true, 'cache_ttl' => self::CACHE_TTL_MEDIUM, 'query_type' => 'restrictions' ]); } /** * Get doctor availability with high-performance queries * * @param int $doctorId Doctor ID * @param array $dateRange Date range filters * @param array $options Query options * @return array Availability data * @since 1.0.0 */ public function getDoctorAvailability(int $doctorId, array $dateRange = [], array $options = []): array { $cacheKey = "doctor_availability_{$doctorId}_" . md5(serialize($dateRange)); return $this->cacheManager->get( $cacheKey, function() use ($doctorId, $dateRange, $options) { return $this->executeDoctorAvailabilityQuery($doctorId, $dateRange, $options); }, self::CACHE_TTL_FAST, // Fast cache for real-time availability ['use_file_cache' => false] // Don't use file cache for real-time data ); } /** * Batch insert/update operations with transaction optimization * * @param string $table Table name * @param array $data Batch data * @param array $options Operation options * @return array Operation results * @since 1.0.0 */ public function batchOperation(string $table, array $data, array $options = []): array { global $wpdb; $startTime = microtime(true); $operation = $options['operation'] ?? 'insert'; // Start transaction for consistency $wpdb->query('START TRANSACTION'); try { $results = []; switch ($operation) { case 'insert': $results = $this->executeBatchInsert($table, $data, $options); break; case 'update': $results = $this->executeBatchUpdate($table, $data, $options); break; case 'upsert': $results = $this->executeBatchUpsert($table, $data, $options); break; default: throw new \InvalidArgumentException("Unsupported operation: {$operation}"); } $wpdb->query('COMMIT'); // Invalidate related caches $this->invalidateRelatedCaches($table, $data); $executionTime = (microtime(true) - $startTime) * 1000; $this->recordBatchMetric($operation, count($data), $executionTime); return $results; } catch (\Exception $e) { $wpdb->query('ROLLBACK'); throw $e; } } /** * Optimize database indexes and analyze query performance * * @return array Optimization results * @since 1.0.0 */ public function optimizeDatabase(): array { global $wpdb; $results = [ 'indexes_analyzed' => 0, 'recommendations' => [], 'slow_queries' => [], 'optimization_applied' => false ]; // Analyze current indexes $indexAnalysis = $this->analyzeIndexUsage(); $results['indexes_analyzed'] = count($indexAnalysis); // Check for missing indexes $missingIndexes = $this->identifyMissingIndexes(); if (!empty($missingIndexes)) { $results['recommendations'] = array_merge($results['recommendations'], $missingIndexes); } // Analyze slow queries $results['slow_queries'] = array_slice($this->slowQueries, -10); // Last 10 slow queries // Update table statistics for query optimizer $this->updateTableStatistics(); $results['optimization_applied'] = true; return $results; } /** * Monitor query performance and collect metrics * * @return array Performance metrics * @since 1.0.0 */ public function getPerformanceMetrics(): array { $totalQueries = count($this->queryMetrics); $cachedQueries = count(array_filter($this->queryMetrics, fn($m) => $m['cached'])); $executionTimes = array_column($this->queryMetrics, 'execution_time'); return [ 'total_queries' => $totalQueries, 'cached_queries' => $cachedQueries, 'cache_hit_rate' => $totalQueries > 0 ? ($cachedQueries / $totalQueries) * 100 : 0, 'average_execution_time' => !empty($executionTimes) ? array_sum($executionTimes) / count($executionTimes) : 0, 'slow_queries_count' => count($this->slowQueries), 'prepared_statements_cached' => count($this->preparedStatements), 'index_monitoring_enabled' => $this->indexMonitoringEnabled, 'connection_pool_size' => $this->getConnectionPoolSize() ]; } /** * Prepare and cache SQL statements for reuse * * @param string $sql SQL statement * @return string Prepared statement identifier * @since 1.0.0 */ public function prepareStatement(string $sql): string { $hash = md5($sql); if (!isset($this->preparedStatements[$hash])) { $this->preparedStatements[$hash] = [ 'sql' => $sql, 'usage_count' => 0, 'created_at' => time(), 'last_used' => time() ]; } $this->preparedStatements[$hash]['usage_count']++; $this->preparedStatements[$hash]['last_used'] = time(); return $hash; } /** * Execute prepared statement with cached optimization * * @param string $statementId Statement identifier * @param array $params Statement parameters * @param array $options Execution options * @return array Query results * @since 1.0.0 */ public function executePreparedStatement(string $statementId, array $params = [], array $options = []): array { if (!isset($this->preparedStatements[$statementId])) { throw new \InvalidArgumentException("Prepared statement not found: {$statementId}"); } $statement = $this->preparedStatements[$statementId]; return $this->executeQuery($statement['sql'], $params, $options); } /** * Build optimized restrictions query with proper indexing * * @param array $filters Query filters * @param array $options Query options * @return string Optimized SQL query * @since 1.0.0 */ private function buildOptimizedRestrictionsQuery(array $filters, array $options): string { global $wpdb; $table = $wpdb->prefix . 'care_booking_restrictions'; $sql = "SELECT * FROM {$table}"; $conditions = []; $orderBy = 'ORDER BY id ASC'; $limit = ''; // Build WHERE conditions with index hints if (!empty($filters['type'])) { $conditions[] = "type = %s"; // Uses type index } if (!empty($filters['target_id'])) { $conditions[] = "target_id = %d"; // Uses target_id index } if (!empty($filters['active'])) { $conditions[] = "is_active = %d"; // Uses is_active index } if (!empty($filters['date_range'])) { $conditions[] = "created_at BETWEEN %s AND %s"; // Uses created_at index } // Combine conditions if (!empty($conditions)) { $sql .= " WHERE " . implode(' AND ', $conditions); } // Optimize ORDER BY for index usage if (!empty($options['order_by'])) { $validColumns = ['id', 'type', 'target_id', 'created_at']; $orderColumn = $options['order_by']; $orderDirection = strtoupper($options['order_direction'] ?? 'ASC'); if (in_array($orderColumn, $validColumns) && in_array($orderDirection, ['ASC', 'DESC'])) { $orderBy = "ORDER BY {$orderColumn} {$orderDirection}"; } } $sql .= " {$orderBy}"; // Add LIMIT for pagination if (!empty($options['limit'])) { $limit = $wpdb->prepare(" LIMIT %d", $options['limit']); if (!empty($options['offset'])) { $limit = $wpdb->prepare(" LIMIT %d, %d", $options['offset'], $options['limit']); } $sql .= $limit; } return $sql; } /** * Execute doctor availability query with optimization * * @param int $doctorId Doctor ID * @param array $dateRange Date range * @param array $options Query options * @return array Availability data * @since 1.0.0 */ private function executeDoctorAvailabilityQuery(int $doctorId, array $dateRange, array $options): array { global $wpdb; // Use indexes: doctor_id, appointment_date $sql = " SELECT r.id, r.type, r.target_id, r.is_active, CASE WHEN r.type = 'doctor' AND r.target_id = %d AND r.is_active = 1 THEN 'blocked' ELSE 'available' END as availability_status FROM {$wpdb->prefix}care_booking_restrictions r USE INDEX (idx_type_target_active) WHERE ( (r.type = 'doctor' AND r.target_id = %d) OR (r.type = 'doctor_service' AND r.doctor_id = %d) ) AND r.is_active = 1 "; $params = [$doctorId, $doctorId, $doctorId]; // Add date range if provided if (!empty($dateRange)) { $sql .= " AND r.created_at BETWEEN %s AND %s"; $params[] = $dateRange['start'] ?? date('Y-m-d 00:00:00'); $params[] = $dateRange['end'] ?? date('Y-m-d 23:59:59'); } return $this->executeOptimizedQuery($sql, $params, $options); } /** * Execute optimized query with performance monitoring * * @param string $sql SQL query * @param array $params Query parameters * @param array $options Execution options * @return array Query results * @since 1.0.0 */ private function executeOptimizedQuery(string $sql, array $params, array $options): array { global $wpdb; $startTime = microtime(true); // Prepare query with parameters if (!empty($params)) { $sql = $wpdb->prepare($sql, ...$params); } // Add query hints for MySQL 8.0+ optimization $sql = $this->addQueryHints($sql, $options); // Execute query $results = $wpdb->get_results($sql, ARRAY_A); if ($wpdb->last_error) { throw new \RuntimeException("Database query error: " . $wpdb->last_error); } $executionTime = (microtime(true) - $startTime) * 1000; // Monitor slow queries if ($executionTime > self::SLOW_QUERY_THRESHOLD) { $this->recordSlowQuery($sql, $executionTime, $params); } // Record index usage if monitoring is enabled if ($this->indexMonitoringEnabled) { $this->recordIndexUsage($sql, $executionTime); } return $results ?: []; } /** * Add MySQL 8.0+ query hints for optimization * * @param string $sql SQL query * @param array $options Query options * @return string SQL with hints * @since 1.0.0 */ private function addQueryHints(string $sql, array $options): string { $hints = []; // Force index usage for specific queries if ($options['force_index'] ?? false) { // This would be handled in the query building phase } // Enable query cache for SELECT queries if (strpos(strtoupper(trim($sql)), 'SELECT') === 0) { $hints[] = 'SQL_CACHE'; } // Add hints to query if (!empty($hints) && strpos($sql, 'SELECT') !== false) { $sql = str_replace('SELECT', 'SELECT ' . implode(' ', $hints), $sql); } return $sql; } /** * Execute batch insert with optimization * * @param string $table Table name * @param array $data Data to insert * @param array $options Insert options * @return array Insert results * @since 1.0.0 */ private function executeBatchInsert(string $table, array $data, array $options): array { global $wpdb; if (empty($data)) { return ['inserted' => 0]; } // Build bulk insert query $columns = array_keys($data[0]); $placeholders = '(' . implode(',', array_fill(0, count($columns), '%s')) . ')'; $allPlaceholders = array_fill(0, count($data), $placeholders); $sql = "INSERT INTO {$table} (" . implode(',', $columns) . ") VALUES " . implode(',', $allPlaceholders); // Flatten data for wpdb->prepare $values = []; foreach ($data as $row) { foreach ($columns as $column) { $values[] = $row[$column] ?? null; } } $preparedSql = $wpdb->prepare($sql, ...$values); $result = $wpdb->query($preparedSql); if ($result === false) { throw new \RuntimeException("Batch insert failed: " . $wpdb->last_error); } return [ 'inserted' => $result, 'last_insert_id' => $wpdb->insert_id ]; } /** * Execute batch update with optimization * * @param string $table Table name * @param array $data Data to update * @param array $options Update options * @return array Update results * @since 1.0.0 */ private function executeBatchUpdate(string $table, array $data, array $options): array { global $wpdb; $updated = 0; $idColumn = $options['id_column'] ?? 'id'; foreach ($data as $row) { if (!isset($row[$idColumn])) { continue; } $id = $row[$idColumn]; unset($row[$idColumn]); $result = $wpdb->update($table, $row, [$idColumn => $id]); if ($result !== false) { $updated++; } } return ['updated' => $updated]; } /** * Execute batch upsert (INSERT ... ON DUPLICATE KEY UPDATE) * * @param string $table Table name * @param array $data Data to upsert * @param array $options Upsert options * @return array Upsert results * @since 1.0.0 */ private function executeBatchUpsert(string $table, array $data, array $options): array { global $wpdb; if (empty($data)) { return ['upserted' => 0]; } $columns = array_keys($data[0]); $updateColumns = $options['update_columns'] ?? array_filter($columns, fn($col) => $col !== 'id'); // Build upsert query $placeholders = '(' . implode(',', array_fill(0, count($columns), '%s')) . ')'; $allPlaceholders = array_fill(0, count($data), $placeholders); $updateClause = implode(',', array_map(fn($col) => "{$col}=VALUES({$col})", $updateColumns)); $sql = "INSERT INTO {$table} (" . implode(',', $columns) . ") VALUES " . implode(',', $allPlaceholders) . " ON DUPLICATE KEY UPDATE {$updateClause}"; // Flatten data $values = []; foreach ($data as $row) { foreach ($columns as $column) { $values[] = $row[$column] ?? null; } } $preparedSql = $wpdb->prepare($sql, ...$values); $result = $wpdb->query($preparedSql); if ($result === false) { throw new \RuntimeException("Batch upsert failed: " . $wpdb->last_error); } return ['upserted' => $result]; } /** * Generate query cache key * * @param string $sql SQL query * @param array $params Query parameters * @return string Cache key * @since 1.0.0 */ private function generateQueryCacheKey(string $sql, array $params): string { return md5($sql . serialize($params)); } /** * Determine appropriate cache TTL for query * * @param string $sql SQL query * @param array $options Query options * @return int Cache TTL in seconds * @since 1.0.0 */ private function determineCacheTTL(string $sql, array $options): int { if (isset($options['cache_ttl'])) { return $options['cache_ttl']; } // Determine TTL based on query characteristics if (strpos($sql, 'care_booking_restrictions') !== false) { return self::CACHE_TTL_MEDIUM; // Restrictions change moderately } if (strpos($sql, 'appointment') !== false) { return self::CACHE_TTL_FAST; // Appointments change frequently } return self::CACHE_TTL_SLOW; // Default for static data } /** * Check if query should be cached * * @param string $sql SQL query * @param array $result Query result * @return bool True if should cache * @since 1.0.0 */ private function shouldCacheQuery(string $sql, array $result): bool { // Don't cache empty results if (empty($result)) { return false; } // Don't cache very large result sets if (count($result) > 1000) { return false; } // Don't cache INSERT/UPDATE/DELETE queries $queryType = strtoupper(substr(trim($sql), 0, 6)); return in_array($queryType, ['SELECT']); } /** * Extract query parameters from filters * * @param array $filters Query filters * @return array Query parameters * @since 1.0.0 */ private function extractQueryParameters(array $filters): array { $params = []; if (isset($filters['type'])) { $params[] = $filters['type']; } if (isset($filters['target_id'])) { $params[] = $filters['target_id']; } if (isset($filters['active'])) { $params[] = $filters['active'] ? 1 : 0; } if (isset($filters['date_range'])) { $params[] = $filters['date_range']['start']; $params[] = $filters['date_range']['end']; } return $params; } /** * Record query performance metric * * @param string $sql SQL query * @param float $executionTime Execution time in milliseconds * @param bool $cached Whether result was cached * @return void * @since 1.0.0 */ private function recordQueryMetric(string $sql, float $executionTime, bool $cached): void { $this->queryMetrics[] = [ 'sql' => substr($sql, 0, 100) . '...', // Truncate for storage 'execution_time' => $executionTime, 'cached' => $cached, 'timestamp' => time() ]; // Keep only recent metrics to prevent memory bloat if (count($this->queryMetrics) > 1000) { $this->queryMetrics = array_slice($this->queryMetrics, -500); } } /** * Record slow query for analysis * * @param string $sql SQL query * @param float $executionTime Execution time * @param array $params Query parameters * @return void * @since 1.0.0 */ private function recordSlowQuery(string $sql, float $executionTime, array $params): void { $this->slowQueries[] = [ 'sql' => $sql, 'execution_time' => $executionTime, 'params' => $params, 'timestamp' => time() ]; // Keep only recent slow queries if (count($this->slowQueries) > 100) { $this->slowQueries = array_slice($this->slowQueries, -50); } } /** * Record batch operation metric * * @param string $operation Operation type * @param int $count Number of records * @param float $executionTime Execution time * @return void * @since 1.0.0 */ private function recordBatchMetric(string $operation, int $count, float $executionTime): void { $this->queryMetrics[] = [ 'sql' => "BATCH_{$operation}", 'execution_time' => $executionTime, 'cached' => false, 'record_count' => $count, 'timestamp' => time() ]; } /** * Record index usage for monitoring * * @param string $sql SQL query * @param float $executionTime Execution time * @return void * @since 1.0.0 */ private function recordIndexUsage(string $sql, float $executionTime): void { // This would analyze EXPLAIN output to determine index usage // Simplified implementation for now if ($executionTime < self::SLOW_QUERY_THRESHOLD) { // Likely using indexes efficiently return; } // Could analyze EXPLAIN EXTENDED results here } /** * Initialize database optimizer * * @return void * @since 1.0.0 */ private function initializeOptimizer(): void { // Register cleanup hooks add_action('care_book_ultimate_daily_cleanup', [$this, 'cleanupPreparedStatements']); // Performance monitoring add_action('shutdown', [$this, 'recordShutdownMetrics']); // Database optimization hooks add_action('care_book_ultimate_weekly_maintenance', [$this, 'optimizeDatabase']); } /** * Analyze current index usage * * @return array Index analysis results * @since 1.0.0 */ private function analyzeIndexUsage(): array { global $wpdb; $table = $wpdb->prefix . 'care_booking_restrictions'; try { $indexes = $wpdb->get_results("SHOW INDEX FROM {$table}", ARRAY_A); return $indexes ?: []; } catch (\Exception $e) { return []; } } /** * Identify missing indexes for optimization * * @return array Missing index recommendations * @since 1.0.0 */ private function identifyMissingIndexes(): array { $recommendations = []; // Analyze slow queries for missing indexes foreach ($this->slowQueries as $slowQuery) { if (strpos($slowQuery['sql'], 'WHERE') !== false) { // Simplified analysis - could be more sophisticated if (strpos($slowQuery['sql'], 'type =') !== false) { $recommendations[] = "Consider adding index on 'type' column"; } if (strpos($slowQuery['sql'], 'target_id =') !== false) { $recommendations[] = "Consider adding index on 'target_id' column"; } } } return array_unique($recommendations); } /** * Update table statistics for query optimizer * * @return void * @since 1.0.0 */ private function updateTableStatistics(): void { global $wpdb; $table = $wpdb->prefix . 'care_booking_restrictions'; try { // Update statistics for MySQL query optimizer $wpdb->query("ANALYZE TABLE {$table}"); } catch (\Exception $e) { // Log error but don't fail } } /** * Invalidate caches related to table operations * * @param string $table Table name * @param array $data Operation data * @return void * @since 1.0.0 */ private function invalidateRelatedCaches(string $table, array $data): void { // Determine which caches to invalidate based on table and data $keysToInvalidate = []; if (strpos($table, 'care_booking_restrictions') !== false) { $keysToInvalidate[] = 'restrictions'; $keysToInvalidate[] = 'doctor_availability'; $keysToInvalidate[] = 'appointment_availability'; } if (!empty($keysToInvalidate)) { $this->cacheManager->invalidate($keysToInvalidate, ['cascade' => true]); } } /** * Get connection pool size (simulated) * * @return int Pool size * @since 1.0.0 */ private function getConnectionPoolSize(): int { // WordPress uses a single connection, but we can monitor concurrent queries return 1; } /** * Clean up old prepared statements * * @return void * @since 1.0.0 */ public function cleanupPreparedStatements(): void { $cutoffTime = time() - 3600; // Remove statements not used in last hour $this->preparedStatements = array_filter( $this->preparedStatements, fn($stmt) => $stmt['last_used'] > $cutoffTime || $stmt['usage_count'] > 10 ); } /** * Record metrics on shutdown * * @return void * @since 1.0.0 */ public function recordShutdownMetrics(): void { $metrics = $this->getPerformanceMetrics(); update_option('care_book_ultimate_query_performance', $metrics, false); } }