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) {