SQL-text entry point: checks the plan cache first. `plan_fn` is called at most once (on cache miss or after a descriptor cache-miss recovery that requires re-planning). ## Two-phase cache lookup (Gap 5 fix) A `PlanCacheKey` requires a `GatewayVersionSet`, which we cannot build from SQL text alone — it requires knowing which collections the plan touches. Previously this method used a speculative
(
&self,
ctx: &QueryContext,
sql: &str,
placeholder_types: &[&str],
plan_fn: impl FnOnce() -> Result<PhysicalPlan, Error>,
)
| 146 | /// descriptor versions), and — if current — use it to build the full key |
| 147 | /// for the plan lookup. |
| 148 | pub async fn execute_sql( |
| 149 | &self, |
| 150 | ctx: &QueryContext, |
| 151 | sql: &str, |
| 152 | placeholder_types: &[&str], |
| 153 | plan_fn: impl FnOnce() -> Result<PhysicalPlan, Error>, |
| 154 | ) -> Result<Vec<Vec<u8>>, Error> { |
| 155 | let sql_hash = hash_sql(sql); |
| 156 | let ph_hash = hash_placeholder_types(placeholder_types); |
| 157 | let sql_key = SqlKey { |
| 158 | sql_text_hash: sql_hash, |
| 159 | placeholder_types_hash: ph_hash, |
| 160 | }; |
| 161 | |
| 162 | // Phase 1: check the side cache for a previously stored version set. |
| 163 | if let Some(stored_vs) = self.plan_cache.lookup_version_set(&sql_key) { |
| 164 | // Verify the stored version set is still current by cross-checking |
| 165 | // each collection's current descriptor version. |
| 166 | let current_vs = |
| 167 | self.verify_version_set(&stored_vs, ctx.tenant_id.as_u64(), ctx.database_id); |
| 168 | if current_vs == stored_vs { |
| 169 | // Version set is still current — try the full plan cache. |
| 170 | let full_key = PlanCacheKey { |
| 171 | sql_text_hash: sql_hash, |
| 172 | placeholder_types_hash: ph_hash, |
| 173 | version_set: stored_vs.clone(), |
| 174 | }; |
| 175 | if let Some(cached_plan) = self.plan_cache.get(&full_key) { |
| 176 | debug!(sql = %sql, "gateway: plan cache hit (two-phase)"); |
| 177 | return self |
| 178 | .execute_with_version_set(ctx, (*cached_plan).clone(), stored_vs) |
| 179 | .await; |
| 180 | } |
| 181 | } |
| 182 | // Stored version set is stale or plan was evicted — fall through |
| 183 | // to re-plan. The stale side-cache entry will be overwritten below. |
| 184 | } |
| 185 | |
| 186 | // Cache miss — invoke the planner. |
| 187 | let plan = plan_fn()?; |
| 188 | |
| 189 | // Compute the actual version set from the plan (contains the real |
| 190 | // collection names and their current descriptor versions). |
| 191 | let actual_vs = self.collect_version_set(&plan, ctx.tenant_id.as_u64(), ctx.database_id); |
| 192 | let actual_key = PlanCacheKey { |
| 193 | sql_text_hash: sql_hash, |
| 194 | placeholder_types_hash: ph_hash, |
| 195 | version_set: actual_vs.clone(), |
| 196 | }; |
| 197 | |
| 198 | // Populate both caches so the next call hits. |
| 199 | self.plan_cache |
| 200 | .insert_version_set(sql_key, actual_vs.clone()); |
| 201 | self.plan_cache.insert(actual_key, Arc::new(plan.clone())); |
| 202 | |
| 203 | self.execute_with_version_set(ctx, plan, actual_vs).await |
| 204 | } |
| 205 |
nothing calls this directly
no test coverage detected