@@ -27,6 +27,7 @@ use arrow::{
2727 record_batch:: RecordBatch ,
2828} ;
2929use datafusion_common:: { Result , internal_err} ;
30+
3031use datafusion_expr:: ColumnarValue ;
3132
3233#[ derive( Debug , Clone , Eq ) ]
@@ -84,6 +85,42 @@ impl PhysicalExpr for UnKnownColumn {
8485 fn fmt_sql ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
8586 std:: fmt:: Display :: fmt ( self , f)
8687 }
88+
89+ #[ cfg( feature = "proto" ) ]
90+ fn try_to_proto (
91+ & self ,
92+ _ctx : & datafusion_physical_expr_common:: physical_expr:: proto_encode:: PhysicalExprEncodeCtx < ' _ > ,
93+ ) -> Result < Option < datafusion_proto_models:: protobuf:: PhysicalExprNode > > {
94+ use datafusion_proto_models:: protobuf;
95+
96+ Ok ( Some ( protobuf:: PhysicalExprNode {
97+ expr_id : None ,
98+ expr_type : Some ( protobuf:: physical_expr_node:: ExprType :: UnknownColumn (
99+ protobuf:: UnknownColumn {
100+ name : self . name . clone ( ) ,
101+ } ,
102+ ) ) ,
103+ } ) )
104+ }
105+ }
106+
107+ #[ cfg( feature = "proto" ) ]
108+ impl UnKnownColumn {
109+ /// Reconstruct an [`UnKnownColumn`] from its protobuf representation.
110+ pub fn try_from_proto (
111+ node : & datafusion_proto_models:: protobuf:: PhysicalExprNode ,
112+ _ctx : & datafusion_physical_expr_common:: physical_expr:: proto_decode:: PhysicalExprDecodeCtx < ' _ > ,
113+ ) -> Result < Arc < dyn PhysicalExpr > > {
114+ use datafusion_physical_expr_common:: expect_expr_variant;
115+ use datafusion_proto_models:: protobuf;
116+
117+ let unknown_col = expect_expr_variant ! (
118+ node,
119+ protobuf:: physical_expr_node:: ExprType :: UnknownColumn ,
120+ "UnKnownColumn" ,
121+ ) ;
122+ Ok ( Arc :: new ( UnKnownColumn :: new ( & unknown_col. name ) ) )
123+ }
87124}
88125
89126impl Hash for UnKnownColumn {
@@ -99,3 +136,103 @@ impl PartialEq for UnKnownColumn {
99136 false
100137 }
101138}
139+
140+ /// Tests for the `try_to_proto` / `try_from_proto` hooks.
141+ #[ cfg( all( test, feature = "proto" ) ) ]
142+ mod proto_tests {
143+ use super :: * ;
144+ use crate :: proto_test_util:: { StubEncoder , UnreachableDecoder , column_node} ;
145+ use arrow:: datatypes:: Schema ;
146+ use datafusion_common:: DataFusionError ;
147+ use datafusion_physical_expr_common:: physical_expr:: proto_decode:: PhysicalExprDecodeCtx ;
148+ use datafusion_physical_expr_common:: physical_expr:: proto_encode:: PhysicalExprEncodeCtx ;
149+ use datafusion_proto_models:: protobuf:: { self , physical_expr_node} ;
150+
151+ // ── try_to_proto ─────────────────────────────────────────────────────────
152+
153+ #[ test]
154+ fn try_to_proto_encodes_unknown_column ( ) {
155+ let expr = UnKnownColumn :: new ( "my_col" ) ;
156+ let encoder = StubEncoder :: ok ( ) ;
157+ let ctx = PhysicalExprEncodeCtx :: new ( & encoder) ;
158+
159+ let node = expr
160+ . try_to_proto ( & ctx)
161+ . unwrap ( )
162+ . expect ( "UnKnownColumn should encode to Some(node)" ) ;
163+
164+ // Built-in exprs never set expr_id; only dynamic filters do.
165+ assert ! ( node. expr_id. is_none( ) ) ;
166+
167+ // Verify the encoded name matches the original.
168+ let protobuf:: UnknownColumn { name } = match node. expr_type {
169+ Some ( physical_expr_node:: ExprType :: UnknownColumn ( c) ) => c,
170+ other => panic ! ( "expected UnknownColumn proto node, got {other:?}" ) ,
171+ } ;
172+ assert_eq ! ( name, "my_col" ) ;
173+ }
174+
175+ // ── try_from_proto ───────────────────────────────────────────────────────
176+
177+ #[ test]
178+ fn try_from_proto_decodes_name ( ) {
179+ let node = protobuf:: PhysicalExprNode {
180+ expr_id : None ,
181+ expr_type : Some ( physical_expr_node:: ExprType :: UnknownColumn (
182+ protobuf:: UnknownColumn {
183+ name : "my_col" . to_string ( ) ,
184+ } ,
185+ ) ) ,
186+ } ;
187+ let schema = Schema :: empty ( ) ;
188+ // UnKnownColumn has no child exprs so the decoder is never called.
189+ let decoder = UnreachableDecoder ;
190+ let ctx = PhysicalExprDecodeCtx :: new ( & schema, & decoder) ;
191+
192+ let decoded = UnKnownColumn :: try_from_proto ( & node, & ctx) . unwrap ( ) ;
193+ let col = decoded
194+ . downcast_ref :: < UnKnownColumn > ( )
195+ . expect ( "decoded expr should be an UnKnownColumn" ) ;
196+ assert_eq ! ( col. name( ) , "my_col" ) ;
197+ }
198+
199+ #[ test]
200+ fn try_from_proto_rejects_non_unknown_column_node ( ) {
201+ // column_node produces an ExprType::Column node, not UnknownColumn.
202+ let node = column_node ( "a" ) ;
203+ let schema = Schema :: empty ( ) ;
204+ let decoder = UnreachableDecoder ;
205+ let ctx = PhysicalExprDecodeCtx :: new ( & schema, & decoder) ;
206+ let err = UnKnownColumn :: try_from_proto ( & node, & ctx) . unwrap_err ( ) ;
207+ assert ! ( matches!(
208+ err,
209+ DataFusionError :: Internal ( ref msg)
210+ if msg. contains( "PhysicalExprNode is not a UnKnownColumn" )
211+ ) ) ;
212+ }
213+
214+ // ── roundtrip ────────────────────────────────────────────────────────────
215+
216+ #[ test]
217+ fn unknown_column_proto_roundtrip ( ) {
218+ let expr = UnKnownColumn :: new ( "col_b" ) ;
219+ let encoder = StubEncoder :: ok ( ) ;
220+ let enc_ctx = PhysicalExprEncodeCtx :: new ( & encoder) ;
221+
222+ let node = expr
223+ . try_to_proto ( & enc_ctx)
224+ . unwrap ( )
225+ . expect ( "UnKnownColumn should encode to Some(node)" ) ;
226+
227+ let schema = Schema :: empty ( ) ;
228+ // UnKnownColumn has no child exprs so the decoder is never called.
229+ let decoder = UnreachableDecoder ;
230+ let dec_ctx = PhysicalExprDecodeCtx :: new ( & schema, & decoder) ;
231+
232+ let decoded = UnKnownColumn :: try_from_proto ( & node, & dec_ctx) . unwrap ( ) ;
233+ let col = decoded
234+ . downcast_ref :: < UnKnownColumn > ( )
235+ . expect ( "decoded expr should be an UnKnownColumn" ) ;
236+ assert_eq ! ( col. name( ) , "col_b" ) ;
237+ }
238+ }
0 commit comments