@@ -59,22 +59,34 @@ impl ScalarSubqueryExpr {
5959 }
6060 }
6161
62+ pub fn results ( & self ) -> & ScalarSubqueryResults {
63+ & self . results
64+ }
65+
66+ #[ deprecated(
67+ since = "55.0.0" ,
68+ note = "was only used for proto serialization, which no longer needs it; use `return_field` for type/nullability. It will be removed in 61.0.0 or 6 months after 55.0.0 is released, whichever is longer."
69+ ) ]
6270 pub fn data_type ( & self ) -> & DataType {
6371 & self . data_type
6472 }
6573
74+ #[ deprecated(
75+ since = "55.0.0" ,
76+ note = "was only used for proto serialization, which no longer needs it; use `return_field` for type/nullability. It will be removed in 61.0.0 or 6 months after 55.0.0 is released, whichever is longer."
77+ ) ]
6678 pub fn nullable ( & self ) -> bool {
6779 self . nullable
6880 }
6981
7082 /// Returns the index of this subquery in the shared results container.
83+ #[ deprecated(
84+ since = "55.0.0" ,
85+ note = "was only used for proto serialization, which no longer needs it. It will be removed in 61.0.0 or 6 months after 55.0.0 is released, whichever is longer."
86+ ) ]
7187 pub fn index ( & self ) -> SubqueryIndex {
7288 self . index
7389 }
74-
75- pub fn results ( & self ) -> & ScalarSubqueryResults {
76- & self . results
77- }
7890}
7991
8092impl fmt:: Display for ScalarSubqueryExpr {
@@ -139,6 +151,69 @@ impl PhysicalExpr for ScalarSubqueryExpr {
139151 fn fmt_sql ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
140152 write ! ( f, "(scalar subquery)" )
141153 }
154+
155+ #[ cfg( feature = "proto" ) ]
156+ fn try_to_proto (
157+ & self ,
158+ _ctx : & datafusion_physical_expr_common:: physical_expr:: proto_encode:: PhysicalExprEncodeCtx < ' _ > ,
159+ ) -> Result < Option < datafusion_proto_models:: protobuf:: PhysicalExprNode > > {
160+ use datafusion_proto_models:: protobuf;
161+ Ok ( Some ( protobuf:: PhysicalExprNode {
162+ expr_id : None ,
163+ expr_type : Some ( protobuf:: physical_expr_node:: ExprType :: ScalarSubquery (
164+ protobuf:: PhysicalScalarSubqueryExprNode {
165+ data_type : Some ( ( & self . data_type ) . try_into ( ) ?) ,
166+ nullable : self . nullable ,
167+ index : u32:: try_from ( self . index . as_usize ( ) ) . map_err ( |_| {
168+ internal_datafusion_err ! (
169+ "scalar subquery index {} does not fit in u32" ,
170+ self . index. as_usize( )
171+ )
172+ } ) ?,
173+ } ,
174+ ) ) ,
175+ } ) )
176+ }
177+ }
178+
179+ #[ cfg( feature = "proto" ) ]
180+ impl ScalarSubqueryExpr {
181+ /// Reconstruct a [`ScalarSubqueryExpr`] from its protobuf representation.
182+ ///
183+ /// Unlike other expressions, this takes a third argument: the shared
184+ /// [`ScalarSubqueryResults`] container. That container is a runtime-only
185+ /// `Arc` shared with the surrounding `ScalarSubqueryExec` and is not part of
186+ /// the wire format, so it cannot be reconstructed here or carried on the
187+ /// decode context (which lives in a crate that cannot depend on
188+ /// `datafusion-expr`). The match arm in `from_proto.rs` fetches it from the
189+ /// plan-level decode context and passes it in.
190+ pub fn try_from_proto (
191+ node : & datafusion_proto_models:: protobuf:: PhysicalExprNode ,
192+ _ctx : & datafusion_physical_expr_common:: physical_expr:: proto_decode:: PhysicalExprDecodeCtx < ' _ > ,
193+ results : & ScalarSubqueryResults ,
194+ ) -> Result < Arc < dyn PhysicalExpr > > {
195+ use datafusion_physical_expr_common:: expect_expr_variant;
196+ use datafusion_physical_expr_common:: physical_expr:: proto_decode:: require_proto_field;
197+ use datafusion_proto_models:: protobuf;
198+
199+ let sq = expect_expr_variant ! (
200+ node,
201+ protobuf:: physical_expr_node:: ExprType :: ScalarSubquery ,
202+ "ScalarSubqueryExpr" ,
203+ ) ;
204+ let data_type = require_proto_field (
205+ sq. data_type . as_ref ( ) ,
206+ "ScalarSubqueryExpr" ,
207+ "data_type" ,
208+ ) ?
209+ . try_into ( ) ?;
210+ Ok ( Arc :: new ( ScalarSubqueryExpr :: new (
211+ data_type,
212+ sq. nullable ,
213+ SubqueryIndex :: new ( sq. index as usize ) ,
214+ results. clone ( ) ,
215+ ) ) )
216+ }
142217}
143218
144219#[ cfg( test) ]
@@ -238,3 +313,123 @@ mod tests {
238313 assert_ne ! ( e1a, e3) ;
239314 }
240315}
316+
317+ /// Tests for the `try_to_proto` / `try_from_proto` hooks.
318+ #[ cfg( all( test, feature = "proto" ) ) ]
319+ mod proto_tests {
320+ use super :: * ;
321+ use crate :: proto_test_util:: { StubEncoder , UnreachableDecoder , column_node} ;
322+ use datafusion_common:: DataFusionError ;
323+ use datafusion_physical_expr_common:: physical_expr:: proto_decode:: PhysicalExprDecodeCtx ;
324+ use datafusion_physical_expr_common:: physical_expr:: proto_encode:: PhysicalExprEncodeCtx ;
325+ use datafusion_proto_models:: protobuf:: {
326+ PhysicalExprNode , PhysicalScalarSubqueryExprNode , physical_expr_node,
327+ } ;
328+
329+ /// Build a `ScalarSubquery` proto node directly, with control over each
330+ /// field, so the decode error paths can be exercised independently.
331+ fn proto_scalar_subquery_node (
332+ data_type : Option < datafusion_proto_models:: datafusion_common:: ArrowType > ,
333+ nullable : bool ,
334+ index : u32 ,
335+ ) -> PhysicalExprNode {
336+ PhysicalExprNode {
337+ expr_id : None ,
338+ expr_type : Some ( physical_expr_node:: ExprType :: ScalarSubquery (
339+ PhysicalScalarSubqueryExprNode {
340+ data_type,
341+ nullable,
342+ index,
343+ } ,
344+ ) ) ,
345+ }
346+ }
347+
348+ #[ test]
349+ fn round_trips_through_proto ( ) {
350+ // A three-slot results container so index 2 is meaningful.
351+ let results = ScalarSubqueryResults :: new ( 3 ) ;
352+ let expr = ScalarSubqueryExpr :: new (
353+ DataType :: Int32 ,
354+ true ,
355+ SubqueryIndex :: new ( 2 ) ,
356+ results. clone ( ) ,
357+ ) ;
358+
359+ // Encode: the expression serializes itself via try_to_proto.
360+ let encoder = StubEncoder :: ok ( ) ;
361+ let enc_ctx = PhysicalExprEncodeCtx :: new ( & encoder) ;
362+ let node = expr
363+ . try_to_proto ( & enc_ctx)
364+ . unwrap ( )
365+ . expect ( "ScalarSubqueryExpr should encode to Some(node)" ) ;
366+
367+ assert ! ( node. expr_id. is_none( ) ) ;
368+ let sq = match & node. expr_type {
369+ Some ( physical_expr_node:: ExprType :: ScalarSubquery ( sq) ) => sq,
370+ other => panic ! ( "expected a ScalarSubquery node, got {other:?}" ) ,
371+ } ;
372+ assert ! ( sq. nullable) ;
373+ assert_eq ! ( sq. index, 2 ) ;
374+ let encoded_type: DataType = sq
375+ . data_type
376+ . as_ref ( )
377+ . expect ( "data_type encoded" )
378+ . try_into ( )
379+ . unwrap ( ) ;
380+ assert_eq ! ( encoded_type, DataType :: Int32 ) ;
381+
382+ // Decode: reconstruct from the proto node, threading in the shared
383+ // results container the surrounding exec would provide.
384+ let decoder = UnreachableDecoder ;
385+ let schema = Schema :: empty ( ) ;
386+ let dec_ctx = PhysicalExprDecodeCtx :: new ( & schema, & decoder) ;
387+ let decoded =
388+ ScalarSubqueryExpr :: try_from_proto ( & node, & dec_ctx, & results) . unwrap ( ) ;
389+ let decoded = decoded
390+ . downcast_ref :: < ScalarSubqueryExpr > ( )
391+ . expect ( "decoded expr should be a ScalarSubqueryExpr" ) ;
392+
393+ // data_type + nullable survive the round-trip (observed via return_field).
394+ let field = decoded. return_field ( & Schema :: empty ( ) ) . unwrap ( ) ;
395+ assert_eq ! ( field. data_type( ) , & DataType :: Int32 ) ;
396+ assert ! ( field. is_nullable( ) ) ;
397+
398+ // Same shared container + same index → equal to the original.
399+ assert_eq ! ( decoded, & expr) ;
400+ }
401+
402+ #[ test]
403+ fn rejects_non_scalar_subquery_node ( ) {
404+ let node = column_node ( "a" ) ;
405+ let results = ScalarSubqueryResults :: new ( 1 ) ;
406+ let decoder = UnreachableDecoder ;
407+ let schema = Schema :: empty ( ) ;
408+ let dec_ctx = PhysicalExprDecodeCtx :: new ( & schema, & decoder) ;
409+
410+ let err =
411+ ScalarSubqueryExpr :: try_from_proto ( & node, & dec_ctx, & results) . unwrap_err ( ) ;
412+ assert ! ( matches!(
413+ err,
414+ DataFusionError :: Internal ( msg)
415+ if msg. contains( "PhysicalExprNode is not a ScalarSubqueryExpr" )
416+ ) ) ;
417+ }
418+
419+ #[ test]
420+ fn rejects_missing_data_type ( ) {
421+ let node = proto_scalar_subquery_node ( None , false , 0 ) ;
422+ let results = ScalarSubqueryResults :: new ( 1 ) ;
423+ let decoder = UnreachableDecoder ;
424+ let schema = Schema :: empty ( ) ;
425+ let dec_ctx = PhysicalExprDecodeCtx :: new ( & schema, & decoder) ;
426+
427+ let err =
428+ ScalarSubqueryExpr :: try_from_proto ( & node, & dec_ctx, & results) . unwrap_err ( ) ;
429+ assert ! ( matches!(
430+ err,
431+ DataFusionError :: Internal ( msg)
432+ if msg. contains( "ScalarSubqueryExpr is missing required field 'data_type'" )
433+ ) ) ;
434+ }
435+ }
0 commit comments