• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

saygoweb / anorm / 18485561332

14 Oct 2025 04:33AM UTC coverage: 84.381% (+3.9%) from 80.483%
18485561332

Pull #42

github

web-flow
Merge 8f63030d4 into 025025bcf
Pull Request #42: Implement comprehensive join optimization system to solve N+1 query problem

846 of 962 new or added lines in 19 files covered. (87.94%)

18 existing lines in 1 file now uncovered.

1637 of 1940 relevant lines covered (84.38%)

15.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.21
/src/Relationship/Strategy/QueryStrategySelector.php
1
<?php
2

3
namespace Anorm\Relationship\Strategy;
4

5
/**
6
 * Selects optimal query strategies for relationship loading
7
 *
8
 * This class implements the decision logic for choosing between different
9
 * relationship loading strategies based on data characteristics, performance
10
 * requirements, and system constraints.
11
 */
12
class QueryStrategySelector implements QueryStrategyInterface
13
{
14
    /** @var DataSizeEstimator */
15
    private $dataEstimator;
16

17
    /** @var array Configuration options for strategy selection */
18
    private $config;
19

20
    public function __construct(DataSizeEstimator $dataEstimator = null, array $config = [])
21
    {
22
        $this->dataEstimator = $dataEstimator ?: new DataSizeEstimator();
119✔
23
        $this->config = array_merge($this->getDefaultConfig(), $config);
119✔
24
    }
25

26
    /**
27
     * Select the optimal query strategy for a relationship
28
     */
29
    public function selectStrategy($relationship, int $sourceCount, ?array $fieldSelection = null): string
30
    {
31
        // For very small datasets, individual loading might be acceptable
32
        if ($sourceCount <= $this->config['individual_loading_threshold']) {
39✔
33
            return self::STRATEGY_INDIVIDUAL_LOADING;
29✔
34
        }
35

36
        // Check if JOIN strategy is supported and beneficial
37
        if ($this->shouldUseJoinStrategy($relationship, $sourceCount, $fieldSelection)) {
11✔
38
            return self::STRATEGY_JOIN_WITH_SELECTION;
1✔
39
        }
40

41
        // For large datasets, consider data size implications
42
        if ($sourceCount > $this->config['large_dataset_threshold']) {
10✔
43
            $estimatedDataSize = $this->dataEstimator->estimateInClauseDataSize($relationship, $sourceCount);
1✔
44
            $joinDataSize = $this->dataEstimator->estimateJoinDataSize($relationship, $sourceCount, $fieldSelection);
1✔
45

46
            // If JOIN would transfer significantly less data, prefer it
47
            if ($fieldSelection && $joinDataSize < $estimatedDataSize * 0.7) {
1✔
NEW
48
                return self::STRATEGY_JOIN_WITH_SELECTION;
×
49
            }
50
        }
51

52
        // Default to IN clause batch loading
53
        return self::STRATEGY_IN_CLAUSE_BATCH;
10✔
54
    }
55

56
    /**
57
     * Determine if JOIN strategy should be used
58
     */
59
    private function shouldUseJoinStrategy($relationship, int $sourceCount, ?array $fieldSelection): bool
60
    {
61
        // JOIN strategy is only beneficial with field selection
62
        if ($fieldSelection === null || empty($fieldSelection)) {
13✔
63
            return false;
8✔
64
        }
65

66
        // Check if relationship type supports JOIN optimization
67
        $cardinality = $relationship->getCardinality();
5✔
68
        if (!$this->isJoinOptimalForCardinality($cardinality)) {
5✔
69
            return false;
1✔
70
        }
71

72
        // Compare estimated data sizes
73
        $inClauseSize = $this->dataEstimator->estimateInClauseDataSize($relationship, $sourceCount);
4✔
74
        $joinSize = $this->dataEstimator->estimateJoinDataSize($relationship, $sourceCount, $fieldSelection);
4✔
75

76
        // Use JOIN if it reduces data transfer by the configured threshold
77
        $reductionRatio = 1 - ($joinSize / max($inClauseSize, 1));
4✔
78
        return $reductionRatio >= $this->config['join_strategy_threshold'];
4✔
79
    }
80

81
    /**
82
     * Check if JOIN strategy is optimal for a given cardinality
83
     */
84
    private function isJoinOptimalForCardinality(string $cardinality): bool
85
    {
86
        switch ($cardinality) {
87
            case 'one-to-one':
6✔
88
            case 'many-to-one':
5✔
89
                return true; // No data duplication
3✔
90
            case 'one-to-many':
5✔
91
                return true; // Can be beneficial with field selection
4✔
92
            case 'many-to-many':
2✔
93
                return false; // High risk of data explosion
2✔
94
            default:
95
                return false;
1✔
96
        }
97
    }
98

99
    /**
100
     * Get metadata about a strategy selection decision
101
     */
102
    public function getStrategyMetadata(string $strategy, $relationship, int $sourceCount, ?array $fieldSelection = null): array
103
    {
104
        $metadata = [
1✔
105
            'strategy' => $strategy,
1✔
106
            'source_count' => $sourceCount,
1✔
107
            'field_selection' => $fieldSelection,
1✔
108
            'cardinality' => $relationship->getCardinality(),
1✔
109
            'estimated_queries' => $this->estimateQueryCount($strategy, $sourceCount),
1✔
110
        ];
1✔
111

112
        // Add data size estimates
113
        if ($strategy === self::STRATEGY_IN_CLAUSE_BATCH) {
1✔
114
            $metadata['estimated_data_size'] = $this->dataEstimator->estimateInClauseDataSize($relationship, $sourceCount);
1✔
NEW
115
        } elseif ($strategy === self::STRATEGY_JOIN_WITH_SELECTION) {
×
NEW
116
            $metadata['estimated_data_size'] = $this->dataEstimator->estimateJoinDataSize($relationship, $sourceCount, $fieldSelection);
×
117
        }
118

119
        // Add decision reasoning
120
        $metadata['decision_factors'] = $this->getDecisionFactors($strategy, $relationship, $sourceCount, $fieldSelection);
1✔
121

122
        return $metadata;
1✔
123
    }
124

125
    /**
126
     * Estimate number of queries for a strategy
127
     */
128
    private function estimateQueryCount(string $strategy, int $sourceCount): int
129
    {
130
        switch ($strategy) {
131
            case self::STRATEGY_INDIVIDUAL_LOADING:
2✔
132
                return $sourceCount; // N queries
1✔
133
            case self::STRATEGY_IN_CLAUSE_BATCH:
2✔
134
                return 1; // Single batch query
2✔
135
            case self::STRATEGY_JOIN_WITH_SELECTION:
1✔
136
                return 1; // Single JOIN query
1✔
137
            default:
138
                return $sourceCount;
1✔
139
        }
140
    }
141

142
    /**
143
     * Get factors that influenced the strategy decision
144
     */
145
    private function getDecisionFactors(string $strategy, $relationship, int $sourceCount, ?array $fieldSelection): array
146
    {
147
        $factors = [];
2✔
148

149
        if ($sourceCount <= $this->config['individual_loading_threshold']) {
2✔
NEW
150
            $factors[] = 'Small dataset size favors individual loading';
×
151
        }
152

153
        if ($fieldSelection !== null && !empty($fieldSelection)) {
2✔
154
            $factors[] = 'Field selection available for optimization';
2✔
155
        } else {
NEW
156
            $factors[] = 'No field selection - full records needed';
×
157
        }
158

159
        $cardinality = $relationship->getCardinality();
2✔
160
        $factors[] = "Relationship cardinality: {$cardinality}";
2✔
161

162
        if ($strategy === self::STRATEGY_JOIN_WITH_SELECTION) {
2✔
NEW
163
            $factors[] = 'JOIN strategy selected for data transfer optimization';
×
164
        } elseif ($strategy === self::STRATEGY_IN_CLAUSE_BATCH) {
2✔
165
            $factors[] = 'IN clause batch loading selected for query optimization';
2✔
166
        }
167

168
        return $factors;
2✔
169
    }
170

171
    /**
172
     * Check if a strategy is supported for a given relationship type
173
     */
174
    public function isStrategySupported(string $strategy, string $relationshipType): bool
175
    {
176
        // All strategies are supported for all relationship types
177
        // Individual implementations may have specific limitations
178
        return in_array($strategy, [
1✔
179
            self::STRATEGY_INDIVIDUAL_LOADING,
1✔
180
            self::STRATEGY_IN_CLAUSE_BATCH,
1✔
181
            self::STRATEGY_JOIN_WITH_SELECTION
1✔
182
        ]);
1✔
183
    }
184

185
    /**
186
     * Get default configuration options
187
     */
188
    private function getDefaultConfig(): array
189
    {
190
        return [
119✔
191
            'individual_loading_threshold' => 10, // Use individual loading for <= 10 models
119✔
192
            'large_dataset_threshold' => 100,     // Consider data size optimization for > 100 models
119✔
193
            'join_strategy_threshold' => 0.5,     // Use JOIN if it reduces data by 50%+
119✔
194
            'max_in_clause_size' => 1000,         // Maximum items in IN clause
119✔
195
            'enable_join_strategy' => true,       // Enable JOIN optimization
119✔
196
            'debug_mode' => false,                 // Enable debug logging
119✔
197
        ];
119✔
198
    }
199

200
    /**
201
     * Update configuration options
202
     */
203
    public function setConfig(array $config): void
204
    {
205
        $this->config = array_merge($this->config, $config);
1✔
206
    }
207

208
    /**
209
     * Get current configuration
210
     */
211
    public function getConfig(): array
212
    {
213
        return $this->config;
1✔
214
    }
215
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc