@@ -39,6 +39,8 @@ use asap_types::{AggregationType, MonitorSpec, PolicyFingerprint};
3939use prost:: Message as _;
4040use thiserror:: Error ;
4141
42+ pub const BACKEND_COMPAT : & str = "asap-query-backend.v1" ;
43+
4244use crate :: physical:: runtime_capability:: Capability ;
4345use asap_types:: enums:: WindowKind ;
4446use planner_types:: post_asap:: {
@@ -82,6 +84,27 @@ pub enum ValidationError {
8284 IncompatibleRoute { fingerprint : u64 } ,
8385 #[ error( "stale plan generation: incoming={incoming}, active={active}" ) ]
8486 StaleGeneration { incoming : u64 , active : u64 } ,
87+ #[ error( "stale plan version for plan {plan_id}: incoming={incoming}, active={active}" ) ]
88+ StalePlanVersion {
89+ plan_id : u64 ,
90+ incoming : u64 ,
91+ active : u64 ,
92+ } ,
93+ #[ error( "plan {plan_id} version {plan_version} was reused with different content" ) ]
94+ ReusedPlanVersion { plan_id : u64 , plan_version : u64 } ,
95+ #[ error( "non-bootstrap plan must have a non-zero plan version" ) ]
96+ ZeroPlanVersion ,
97+ #[ error( "non-bootstrap plan must have an activation time" ) ]
98+ MissingActivation ,
99+ #[ error( "plan expiry {expiry} is not after activation {activation}" ) ]
100+ InvalidExpiry { activation : u64 , expiry : u64 } ,
101+ #[ error( "non-bootstrap plan must declare backend compatibility" ) ]
102+ MissingBackendCompat ,
103+ #[ error( "unsupported backend compatibility `{actual}`; expected `{expected}`" ) ]
104+ UnsupportedBackendCompat {
105+ actual : String ,
106+ expected : & ' static str ,
107+ } ,
85108}
86109
87110// ── WindowSpec ───────────────────────────────────────────────────────────────
@@ -772,6 +795,10 @@ pub struct BackendPlan {
772795 /// existing convention for the pre-`BackendPlan` wire format).
773796 pub plan_id : u64 ,
774797 pub generated_at_unix_ms : u64 ,
798+ pub plan_version : u64 ,
799+ pub activation_unix_ms : u64 ,
800+ pub expiry_unix_ms : Option < u64 > ,
801+ pub backend_compat : String ,
775802 pub materializations : HashMap < PolicyFingerprint , Materialization > ,
776803 pub routing : Vec < RoutingEntry > ,
777804 pub monitors : Vec < MonitorSpec > ,
@@ -782,6 +809,10 @@ impl From<&BackendPlan> for proto::BackendPlan {
782809 proto:: BackendPlan {
783810 plan_id : p. plan_id ,
784811 generated_at_unix_ms : p. generated_at_unix_ms ,
812+ plan_version : p. plan_version ,
813+ activation_unix_ms : p. activation_unix_ms ,
814+ expiry_unix_ms : p. expiry_unix_ms ,
815+ backend_compat : p. backend_compat . clone ( ) ,
785816 materializations : p
786817 . materializations
787818 . iter ( )
@@ -809,6 +840,10 @@ impl TryFrom<proto::BackendPlan> for BackendPlan {
809840 Ok ( BackendPlan {
810841 plan_id : p. plan_id ,
811842 generated_at_unix_ms : p. generated_at_unix_ms ,
843+ plan_version : p. plan_version ,
844+ activation_unix_ms : p. activation_unix_ms ,
845+ expiry_unix_ms : p. expiry_unix_ms ,
846+ backend_compat : p. backend_compat ,
812847 materializations,
813848 routing,
814849 monitors : p. monitors . into_iter ( ) . map ( Into :: into) . collect ( ) ,
@@ -832,6 +867,31 @@ impl BackendPlan {
832867 /// Validate cross-references and invariants required before a decoded
833868 /// plan may become visible to ingest or query readers.
834869 pub fn validate ( & self ) -> Result < ( ) , ValidationError > {
870+ if self . plan_id != 0 {
871+ if self . plan_version == 0 {
872+ return Err ( ValidationError :: ZeroPlanVersion ) ;
873+ }
874+ if self . activation_unix_ms == 0 {
875+ return Err ( ValidationError :: MissingActivation ) ;
876+ }
877+ if self . backend_compat . trim ( ) . is_empty ( ) {
878+ return Err ( ValidationError :: MissingBackendCompat ) ;
879+ }
880+ if self . backend_compat != BACKEND_COMPAT {
881+ return Err ( ValidationError :: UnsupportedBackendCompat {
882+ actual : self . backend_compat . clone ( ) ,
883+ expected : BACKEND_COMPAT ,
884+ } ) ;
885+ }
886+ if let Some ( expiry) = self . expiry_unix_ms {
887+ if expiry <= self . activation_unix_ms {
888+ return Err ( ValidationError :: InvalidExpiry {
889+ activation : self . activation_unix_ms ,
890+ expiry,
891+ } ) ;
892+ }
893+ }
894+ }
835895 for ( key, materialization) in & self . materializations {
836896 if * key != materialization. fingerprint {
837897 return Err ( ValidationError :: FingerprintMismatch {
@@ -976,6 +1036,10 @@ mod tests {
9761036 BackendPlan {
9771037 plan_id : 42 ,
9781038 generated_at_unix_ms : 1_735_000_000_000 ,
1039+ plan_version : 1 ,
1040+ activation_unix_ms : 1_735_000_000_000 ,
1041+ expiry_unix_ms : None ,
1042+ backend_compat : "asap-query-backend.v1" . into ( ) ,
9791043 materializations,
9801044 routing : vec ! [
9811045 RoutingEntry {
0 commit comments