@@ -52,6 +52,11 @@ use crate::control::state::SharedState;
5252/// is infallible today (all typed functions log on failure and
5353/// return).
5454pub fn apply_post_apply_side_effects_sync ( entry : & CatalogEntry , shared : & Arc < SharedState > ) {
55+ // Gateway plan-cache invalidation: on any descriptor mutation, evict
56+ // stale cached plans that reference the changed descriptor.
57+ // This is a single, unconditional call per DDL commit — negligible overhead.
58+ invalidate_gateway_cache_for_entry ( entry, shared) ;
59+
5560 match entry {
5661 CatalogEntry :: PutCollection ( stored) => {
5762 // Owner record install is sync; Data Plane register is
@@ -189,3 +194,175 @@ pub fn spawn_post_apply_async_side_effects(entry: CatalogEntry, shared: Arc<Shar
189194 } ) ;
190195 }
191196}
197+
198+ /// Notify the gateway plan-cache invalidator after a DDL descriptor mutation.
199+ ///
200+ /// Extracts the descriptor name and new version from the entry and calls
201+ /// `PlanCacheInvalidator::invalidate`. This is best-effort: if the gateway
202+ /// has not been constructed yet (`gateway_invalidator == None`) the call is
203+ /// a no-op.
204+ ///
205+ /// ## Invalidation decision table (all 31 variants — exhaustive, no `_ => {}`)
206+ ///
207+ /// The gateway plan cache keys on `(sql_hash, ph_hash, GatewayVersionSet)`.
208+ /// A `GatewayVersionSet` lists `(collection_name, descriptor_version)` pairs
209+ /// extracted from the `PhysicalPlan` by `touched_collections`. A DDL entry
210+ /// requires invalidation only if it changes the observable plan shape for
211+ /// an already-cached plan. Verified against `planner/`, `rls_injection.rs`,
212+ /// and the `PhysicalPlan` definition.
213+ ///
214+ /// | Entry kind | Invalidate? | Reason |
215+ /// |-----------------------------------------|-------------|--------|
216+ /// | PutCollection / DeactivateCollection | ✅ yes | collection schema baked into plan |
217+ /// | PutSequence / DeleteSequence | ❌ no | sequences resolved at handler level (pgwire `transaction_cmds.rs`), not in PhysicalPlan |
218+ /// | PutSequenceState | ❌ no | runtime counter state, not plan shape |
219+ /// | PutTrigger / DeleteTrigger | ❌ no | triggers dispatched by Event Plane post-execution; no trigger fields in any PhysicalPlan variant |
220+ /// | PutFunction / DeleteFunction | ❌ no | functions looked up at eval time, not inlined |
221+ /// | PutProcedure / DeleteProcedure | ❌ no | same as functions |
222+ /// | PutSchedule / DeleteSchedule | ❌ no | scheduler runs independently |
223+ /// | PutChangeStream / DeleteChangeStream | ❌ no | CDC Event Plane concern |
224+ /// | PutUser / DeactivateUser | ❌ no | authz checked at exec time |
225+ /// | PutRole / DeleteRole | ❌ no | same |
226+ /// | PutApiKey / RevokeApiKey | ❌ no | same |
227+ /// | PutMaterializedView / DeleteMaterializedView | ❌ no | MV definition is its own catalog object; write-path `materialized_sum_sources` is set at collection-register time via PutCollection, not updated by PutMaterializedView independently |
228+ /// | PutTenant / DeleteTenant | ❌ no | tenant identity does not affect plan shape |
229+ /// | PutRlsPolicy / DeleteRlsPolicy | ❌ no | `execute_sql` is only called from CDC path (no RLS injection via `inject_rls`); per-session pgwire cache has its own DDL invalidation |
230+ /// | PutPermission / DeletePermission | ❌ no | permission checked at exec time |
231+ /// | PutOwner / DeleteOwner | ❌ no | ownership does not affect plan shape |
232+ pub ( crate ) fn invalidate_gateway_cache_for_entry ( entry : & CatalogEntry , shared : & Arc < SharedState > ) {
233+ let Some ( ref inv) = shared. gateway_invalidator else {
234+ return ;
235+ } ;
236+ match entry {
237+ // ── Collection mutations that change the plan shape ──────────────────
238+ CatalogEntry :: PutCollection ( stored) => {
239+ inv. invalidate ( & stored. name , stored. descriptor_version . max ( 1 ) ) ;
240+ }
241+ CatalogEntry :: DeactivateCollection { name, .. } => {
242+ // Treat deactivation as version 0 (collection gone — any cached
243+ // plan for it is stale).
244+ inv. invalidate ( name, 0 ) ;
245+ }
246+
247+ // ── Sequence: resolved at handler level, not baked into PhysicalPlan ─
248+ CatalogEntry :: PutSequence ( _) => {
249+ // no-op: sequences resolved in pgwire transaction_cmds.rs before
250+ // planning; StoredSequence never appears in a PhysicalPlan variant.
251+ }
252+ CatalogEntry :: DeleteSequence { .. } => {
253+ // no-op: same reason as PutSequence.
254+ }
255+ CatalogEntry :: PutSequenceState ( _) => {
256+ // no-op: runtime counter state — the planner never reads seq state.
257+ }
258+
259+ // ── Trigger: dispatched by Event Plane post-execution ────────────────
260+ CatalogEntry :: PutTrigger ( _) => {
261+ // no-op: triggers are AFTER-fire; no trigger field exists in any
262+ // PhysicalPlan variant; Event Plane reads the trigger registry
263+ // directly at fire time.
264+ }
265+ CatalogEntry :: DeleteTrigger { .. } => {
266+ // no-op: same as PutTrigger.
267+ }
268+
269+ // ── Function / Procedure: looked up at eval time, not inlined ────────
270+ CatalogEntry :: PutFunction ( _) => {
271+ // no-op: UDFs looked up in function_registry at eval time via
272+ // `wasm/` executor; never inlined into a PhysicalPlan.
273+ }
274+ CatalogEntry :: DeleteFunction { .. } => {
275+ // no-op: same as PutFunction.
276+ }
277+ CatalogEntry :: PutProcedure ( _) => {
278+ // no-op: stored procedures parsed and executed at CALL time via
279+ // `procedural/executor`; body not baked into any PhysicalPlan.
280+ }
281+ CatalogEntry :: DeleteProcedure { .. } => {
282+ // no-op: same as PutProcedure.
283+ }
284+
285+ // ── Schedule: cron runs independently of the plan cache ──────────────
286+ CatalogEntry :: PutSchedule ( _) => {
287+ // no-op: ScheduleRegistry drives the scheduler loop; no plan shape
288+ // changes result from a new/updated schedule definition.
289+ }
290+ CatalogEntry :: DeleteSchedule { .. } => {
291+ // no-op: same as PutSchedule.
292+ }
293+
294+ // ── Change stream: CDC Event Plane concern ────────────────────────────
295+ CatalogEntry :: PutChangeStream ( _) => {
296+ // no-op: CDC stream definitions route WriteEvents in the Event
297+ // Plane; they do not alter how a collection's plan is constructed.
298+ }
299+ CatalogEntry :: DeleteChangeStream { .. } => {
300+ // no-op: same as PutChangeStream.
301+ }
302+
303+ // ── User / Role / ApiKey: authz checked at exec, not baked into plan ─
304+ CatalogEntry :: PutUser ( _) => {
305+ // no-op: user identity checked in credential store at exec time.
306+ }
307+ CatalogEntry :: DeactivateUser { .. } => {
308+ // no-op: same as PutUser.
309+ }
310+ CatalogEntry :: PutRole ( _) => {
311+ // no-op: role membership checked at exec time via RoleStore.
312+ }
313+ CatalogEntry :: DeleteRole { .. } => {
314+ // no-op: same as PutRole.
315+ }
316+ CatalogEntry :: PutApiKey ( _) => {
317+ // no-op: API key checked at connection/exec time via ApiKeyStore.
318+ }
319+ CatalogEntry :: RevokeApiKey { .. } => {
320+ // no-op: same as PutApiKey.
321+ }
322+
323+ // ── Materialized view: MV definition is a separate catalog object ────
324+ CatalogEntry :: PutMaterializedView ( _) => {
325+ // no-op: MaterializedView metadata is its own catalog object and
326+ // does not directly modify any PhysicalPlan. The `materialized_sum_sources`
327+ // field in DocumentOp::Register is set at collection-register time
328+ // (driven by PutCollection), not updated independently by
329+ // PutMaterializedView. Any schema change that would affect plans
330+ // cascades through PutCollection instead.
331+ }
332+ CatalogEntry :: DeleteMaterializedView { .. } => {
333+ // no-op: same as PutMaterializedView.
334+ }
335+
336+ // ── Tenant: identity does not affect plan shape ───────────────────────
337+ CatalogEntry :: PutTenant ( _) => {
338+ // no-op: tenant identity used for quota enforcement at exec time.
339+ }
340+ CatalogEntry :: DeleteTenant { .. } => {
341+ // no-op: same as PutTenant.
342+ }
343+
344+ // ── RLS policy: execute_sql callers (CDC) do not inject RLS ──────────
345+ CatalogEntry :: PutRlsPolicy ( _) => {
346+ // no-op: the gateway execute_sql path (CDC consume_remote) calls
347+ // plan_sql without RLS injection; per-session pgwire plan cache
348+ // has its own DDL-aware invalidation that handles RLS changes.
349+ }
350+ CatalogEntry :: DeleteRlsPolicy { .. } => {
351+ // no-op: same as PutRlsPolicy.
352+ }
353+
354+ // ── Permission / Owner: not baked into plan ───────────────────────────
355+ CatalogEntry :: PutPermission ( _) => {
356+ // no-op: permission grants checked at exec time via PermissionStore.
357+ }
358+ CatalogEntry :: DeletePermission { .. } => {
359+ // no-op: same as PutPermission.
360+ }
361+ CatalogEntry :: PutOwner ( _) => {
362+ // no-op: ownership does not influence plan structure.
363+ }
364+ CatalogEntry :: DeleteOwner { .. } => {
365+ // no-op: same as PutOwner.
366+ }
367+ }
368+ }
0 commit comments