This is an automated email from the ASF dual-hosted git repository.

zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git


The following commit(s) were added to refs/heads/main by this push:
     new f3e41bca1 feat(exprs): accept the literal wrapper form for predicate 
values (#2191)
f3e41bca1 is described below

commit f3e41bca12fd3c7f92f87c07041a9558d9b42a68
Author: Revanth <[email protected]>
AuthorDate: Mon Oct 19 10:45:51 2026 -0500

    feat(exprs): accept the literal wrapper form for predicate values (#2191)
---
 expr_json.go      | 22 ++++++++++++++++++-
 expr_json_test.go | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 84 insertions(+), 1 deletion(-)

diff --git a/expr_json.go b/expr_json.go
index 56a6b1c86..03e9f7d5f 100644
--- a/expr_json.go
+++ b/expr_json.go
@@ -619,8 +619,28 @@ func decodeTerm(raw json.RawMessage) (UnboundTerm, error) {
        }
 }
 
-// decodeValue parses one literal value and converts it to typ.
+// decodeValue parses one literal value and converts it to typ. The value is
+// either bare or wrapped as {"type":"literal","value":...}. As in Java, the
+// wrapper's optional data-type is ignored: the field type decides.
 func decodeValue(raw json.RawMessage, typ Type) (Literal, error) {
+       if tok, err := firstToken(raw); err == nil && tok == json.Delim('{') {
+               var wrapper struct {
+                       Type  string          `json:"type"`
+                       Value json.RawMessage `json:"value"`
+               }
+               if err := json.Unmarshal(raw, &wrapper); err != nil {
+                       return nil, fmt.Errorf("%w: cannot parse literal: %s", 
ErrInvalidArgument, err)
+               }
+               if wrapper.Type != "literal" {
+                       return nil, fmt.Errorf("%w: cannot parse type %q as a 
literal", ErrInvalidArgument, wrapper.Type)
+               }
+               if len(wrapper.Value) == 0 {
+                       return nil, fmt.Errorf("%w: literal is missing a 
value", ErrInvalidArgument)
+               }
+               // A null or nested object value is rejected by literalValue 
below.
+               raw = wrapper.Value
+       }
+
        v := literalValue{typ: typ}
        if err := json.Unmarshal(raw, &v); err != nil {
                return nil, err
diff --git a/expr_json_test.go b/expr_json_test.go
index 69a4c0fed..cdf0b2b2d 100644
--- a/expr_json_test.go
+++ b/expr_json_test.go
@@ -608,6 +608,69 @@ func TestUnmarshalExpressionTransformTermUnknown(t 
*testing.T) {
        require.ErrorContains(t, err, "unknown transform in expression term")
 }
 
+// TestUnmarshalExpressionLiteralWrapper checks that a predicate value wrapped 
as
+// {"type":"literal","value":...}, as the spec's JSON grammar allows, parses 
the
+// same as the bare value.
+func TestUnmarshalExpressionLiteralWrapper(t *testing.T) {
+       schema := iceberg.NewSchema(0,
+               iceberg.NestedField{ID: 1, Name: "i", Type: 
iceberg.PrimitiveTypes.Int32},
+               iceberg.NestedField{ID: 2, Name: "d", Type: 
iceberg.PrimitiveTypes.Date},
+       )
+
+       tests := []struct {
+               name    string
+               schema  *iceberg.Schema
+               wrapped string
+               bare    string
+       }{
+               {
+                       "value takes the field type", schema,
+                       
`{"type":"eq","term":"d","value":{"type":"literal","value":"2022-08-14"}}`,
+                       `{"type":"eq","term":"d","value":"2022-08-14"}`,
+               },
+               {
+                       "data-type is ignored", schema,
+                       
`{"type":"eq","term":"d","value":{"type":"literal","value":"2022-08-14","data-type":"string"}}`,
+                       `{"type":"eq","term":"d","value":"2022-08-14"}`,
+               },
+               {
+                       "values mix wrapped and bare", schema,
+                       
`{"type":"in","term":"i","values":[{"type":"literal","value":1},2]}`,
+                       `{"type":"in","term":"i","values":[1,2]}`,
+               },
+               {
+                       "schema-less value", nil,
+                       
`{"type":"lt","term":"x","value":{"type":"literal","value":5}}`,
+                       `{"type":"lt","term":"x","value":5}`,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       want, err := iceberg.ParseExpr([]byte(tt.bare), 
tt.schema)
+                       require.NoError(t, err)
+                       got, err := iceberg.ParseExpr([]byte(tt.wrapped), 
tt.schema)
+                       require.NoError(t, err)
+                       assert.Truef(t, want.Equals(got), "want %s, got %s", 
want, got)
+               })
+       }
+}
+
+func TestUnmarshalExpressionLiteralWrapperErrors(t *testing.T) {
+       for name, value := range map[string]string{
+               "not a literal": `{"type":"reference","term":"a"}`,
+               "missing type":  `{"value":1}`,
+               "missing value": `{"type":"literal"}`,
+               "null value":    `{"type":"literal","value":null}`,
+               "nested":        
`{"type":"literal","value":{"type":"literal","value":1}}`,
+       } {
+               t.Run(name, func(t *testing.T) {
+                       _, err := 
iceberg.ParseExpr([]byte(`{"type":"eq","term":"a","value":`+value+`}`), nil)
+                       require.ErrorIs(t, err, iceberg.ErrInvalidArgument)
+               })
+       }
+}
+
 // TestUnmarshalExpressionFixedLength rejects a fixed value whose decoded 
length
 // doesn't match the column's declared length.
 func TestUnmarshalExpressionFixedLength(t *testing.T) {

Reply via email to