@@ -520,6 +520,12 @@ impl QueryPlanEntry {
520520 ) ) ;
521521 }
522522 } else {
523+ if compiled. row_source ( compiled. roots ( ) [ 0 ] ) . is_some ( ) {
524+ return Err ( QueryPlanError :: Invalid (
525+ "row-preserving physical output requires its input identity binding"
526+ . into ( ) ,
527+ ) ) ;
528+ }
523529 let output = compiled
524530 . output_contract ( compiled. roots ( ) [ 0 ] )
525531 . map_err ( |e| QueryPlanError :: Invalid ( e. to_string ( ) ) ) ?;
@@ -957,6 +963,77 @@ mod contract_tests {
957963
958964#[ cfg( test) ]
959965mod retired_plan_tests {
966+ // Row-preserving operators cannot opt out of the original vector identity.
967+ #[ test]
968+ fn row_preserving_graph_requires_its_identity_binding ( ) {
969+ use super :: * ;
970+ use asap_physical_operators:: {
971+ operators:: Operator ,
972+ physical_planner:: { CompiledPhysicalDag , InputContract } ,
973+ } ;
974+ use planner_types:: {
975+ post_asap:: { SummaryFamilyType , SummaryField , SummarySchema } ,
976+ pre_asap:: DataType ,
977+ } ;
978+ let schema = std:: sync:: Arc :: new ( SummarySchema {
979+ fields : vec ! [ SummaryField {
980+ name: "value" . into( ) ,
981+ dtype: SummaryFamilyType :: Plain ( DataType :: Float64 ) ,
982+ nullable: false ,
983+ } ] ,
984+ time_index : None ,
985+ } ) ;
986+ let program = CompiledPhysicalDag :: from_operators (
987+ [ ( 0 , InputContract :: bounded ( schema. clone ( ) ) ) ] . into ( ) ,
988+ [ ( 1 , ( vec ! [ 0 ] , Operator :: limit ( schema, 1 , 0 , vec ! [ ] ) . unwrap ( ) ) ) ] . into ( ) ,
989+ vec ! [ 1 ] ,
990+ )
991+ . unwrap ( ) ;
992+ let mut entry = QueryPlanEntry {
993+ language : QueryLanguage :: PromQl ,
994+ query_id : "identity" . into ( ) ,
995+ canonical_query : "m" . into ( ) ,
996+ fixed_evaluation : None ,
997+ physical_dag : None ,
998+ root : QueryNodeId ( 1 ) ,
999+ nodes : [
1000+ (
1001+ QueryNodeId ( 0 ) ,
1002+ QueryPlanNode :: ExactFallback {
1003+ reason : "bound vector" . into ( ) ,
1004+ } ,
1005+ ) ,
1006+ (
1007+ QueryNodeId ( 1 ) ,
1008+ QueryPlanNode :: PhysicalFragment {
1009+ inputs : vec ! [ QueryNodeId ( 0 ) ] ,
1010+ dag : program. encode ( ) . unwrap ( ) ,
1011+ row_input : None ,
1012+ pruning : None ,
1013+ } ,
1014+ ) ,
1015+ ]
1016+ . into ( ) ,
1017+ instant : InstantExecution {
1018+ lookback_ms : 0 ,
1019+ full_history : false ,
1020+ cumulative_readout : false ,
1021+ } ,
1022+ fallback : FallbackPolicy :: Reject ,
1023+ } ;
1024+ assert ! ( entry
1025+ . validate( & BTreeSet :: new( ) )
1026+ . unwrap_err( )
1027+ . to_string( )
1028+ . contains( "identity binding" ) ) ;
1029+ if let QueryPlanNode :: PhysicalFragment { row_input, .. } =
1030+ entry. nodes . get_mut ( & QueryNodeId ( 1 ) ) . unwrap ( )
1031+ {
1032+ * row_input = Some ( 0 ) ;
1033+ }
1034+ entry. validate ( & BTreeSet :: new ( ) ) . unwrap ( ) ;
1035+ }
1036+
9601037 #[ test]
9611038 fn uncompiled_relation_variants_are_not_accepted ( ) {
9621039 for kind in [ "relational" , "relational_join" ] {
0 commit comments