-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Core: add parsers for events and unbounded expressions #4308
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
df0e467
1edc3ec
8bc441e
0c6a0bc
29aa16e
c57789f
f1cbe93
ed16ef5
da57f74
7aec26f
b969fb3
55c1505
e61ce99
0aa2f8c
b545ade
39dee63
d990df1
2bde5a8
636c9b8
73ec648
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,334 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
|
|
||
| package org.apache.iceberg.expressions; | ||
|
|
||
| import com.fasterxml.jackson.core.JsonGenerator; | ||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import java.io.IOException; | ||
| import java.io.StringWriter; | ||
| import java.io.UncheckedIOException; | ||
| import java.math.BigDecimal; | ||
| import java.nio.ByteBuffer; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.List; | ||
| import java.util.Locale; | ||
| import java.util.Set; | ||
| import java.util.UUID; | ||
| import org.apache.iceberg.exceptions.RuntimeIOException; | ||
| import org.apache.iceberg.relocated.com.google.common.collect.ImmutableSet; | ||
| import org.apache.iceberg.relocated.com.google.common.collect.Lists; | ||
| import org.apache.iceberg.relocated.com.google.common.collect.Sets; | ||
| import org.apache.iceberg.types.Conversions; | ||
| import org.apache.iceberg.types.Type; | ||
| import org.apache.iceberg.types.Types; | ||
| import org.apache.iceberg.util.JsonUtil; | ||
|
|
||
| public class ExpressionParser { | ||
| private static final String TYPE = "type"; | ||
| private static final String VALUE = "value"; | ||
| private static final String OPERATION = "operation"; | ||
| private static final String LITERALS = "literals"; | ||
| private static final String TERM = "term"; | ||
| private static final String LEFT_OPERAND = "left-operand"; | ||
| private static final String RIGHT_OPERAND = "right-operand"; | ||
| private static final String OPERAND = "operand"; | ||
| private static final String AND = "and"; | ||
| private static final String OR = "or"; | ||
| private static final String NOT = "not"; | ||
| private static final String TRUE = "true"; | ||
| private static final String FALSE = "false"; | ||
| private static final String UNBOUNDED_PREDICATE = "unbounded-predicate"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The term is "unbound", meaning that the expression has not been bound to a particular incoming struct/row type, not "unbounded". |
||
| private static final String BOUNDED_LITERAL_PREDICATE = "bounded-literal-predicate"; | ||
| private static final String BOUNDED_SET_PREDICATE = "bounded-set-predicate"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to serialize bound predicates? |
||
| private static final String BOUNDED_UNARY_PREDICATE = "bounded-unary-predicate"; | ||
| private static final Set<String> PREDICATE_TYPES = Sets.newHashSet( | ||
| UNBOUNDED_PREDICATE, | ||
| BOUNDED_LITERAL_PREDICATE, | ||
| BOUNDED_SET_PREDICATE, | ||
| BOUNDED_UNARY_PREDICATE); | ||
| private static final String NAMED_REFERENCE = "named-reference"; | ||
| private static final String BOUND_REFERENCE = "bound-reference"; | ||
|
|
||
| private static final Set<Expression.Operation> ONE_INPUTS = ImmutableSet.of( | ||
| Expression.Operation.IS_NULL, | ||
| Expression.Operation.NOT_NULL, | ||
| Expression.Operation.IS_NAN, | ||
| Expression.Operation.NOT_NAN); | ||
|
|
||
| private ExpressionParser() { | ||
| } | ||
|
|
||
| public static String toJson(Expression expression, boolean pretty) { | ||
| try { | ||
| StringWriter writer = new StringWriter(); | ||
| JsonGenerator generator = JsonUtil.factory().createGenerator(writer); | ||
| if (pretty) { | ||
| generator.useDefaultPrettyPrinter(); | ||
| } | ||
| toJson(expression, generator); | ||
| generator.flush(); | ||
| return writer.toString(); | ||
|
|
||
| } catch (IOException e) { | ||
| throw new UncheckedIOException("Failed to write json", e); | ||
| } | ||
| } | ||
|
|
||
| public static void toJson(Expression expression, JsonGenerator generator) throws IOException { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Iceberg separates the recursive logic to process a tree structure from the logic to do something by using visitors. I think that this should use an in-order expression visitor to produce a JSON representation. |
||
| if (expression instanceof And) { | ||
| toJson((And) expression, generator); | ||
| } else if (expression instanceof Or) { | ||
| toJson((Or) expression, generator); | ||
| } else if (expression instanceof Not) { | ||
| toJson((Not) expression, generator); | ||
| } else if (expression instanceof True) { | ||
| toJson((True) expression, generator); | ||
| } else if (expression instanceof False) { | ||
| toJson((False) expression, generator); | ||
| } else if (expression instanceof Predicate) { | ||
| toJson((Predicate<?, ?>) expression, generator); | ||
| } else { | ||
| throw new IllegalArgumentException("Invalid Operation Type"); | ||
| } | ||
| } | ||
|
|
||
| public static void toJson(And expression, JsonGenerator generator) throws IOException { | ||
| generator.writeStartObject(); | ||
| generator.writeStringField(TYPE, AND); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here can we reuse the word "operation"? Because phrases like "and" and "or" are operations |
||
| generator.writeFieldName(LEFT_OPERAND); | ||
| toJson(expression.left(), generator); | ||
| generator.writeFieldName(RIGHT_OPERAND); | ||
| toJson(expression.right(), generator); | ||
| generator.writeEndObject(); | ||
| } | ||
|
|
||
| public static void toJson(Or expression, JsonGenerator generator) throws IOException { | ||
| generator.writeStartObject(); | ||
| generator.writeStringField(TYPE, OR); | ||
| generator.writeFieldName(LEFT_OPERAND); | ||
| toJson(expression.left(), generator); | ||
| generator.writeFieldName(RIGHT_OPERAND); | ||
| toJson(expression.right(), generator); | ||
| generator.writeEndObject(); | ||
| } | ||
|
|
||
| public static void toJson(Not expression, JsonGenerator generator) throws IOException { | ||
| generator.writeStartObject(); | ||
| generator.writeStringField(TYPE, NOT); | ||
| generator.writeFieldName(OPERAND); | ||
| toJson(expression.child(), generator); | ||
| generator.writeEndObject(); | ||
| } | ||
|
|
||
| public static void toJson(True expression, JsonGenerator generator) throws IOException { | ||
| generator.writeStartObject(); | ||
| generator.writeStringField(TYPE, TRUE); | ||
| generator.writeEndObject(); | ||
| } | ||
|
|
||
| public static void toJson(False expression, JsonGenerator generator) throws IOException { | ||
| generator.writeStartObject(); | ||
| generator.writeStringField(TYPE, FALSE); | ||
| generator.writeEndObject(); | ||
| } | ||
|
|
||
| public static void toJson(Predicate<?, ?> predicate, JsonGenerator generator) throws IOException { | ||
| if (predicate instanceof UnboundPredicate) { | ||
| toJson((UnboundPredicate<?>) predicate, generator); | ||
| } else { | ||
| throw new IllegalArgumentException("Cannot convert predicate " + predicate); | ||
| } | ||
| } | ||
|
|
||
| public static void toJson(UnboundPredicate<?> predicate, JsonGenerator generator) throws IOException { | ||
| generator.writeStartObject(); | ||
| generator.writeStringField(TYPE, UNBOUNDED_PREDICATE); | ||
| generator.writeStringField(OPERATION, predicate.op().name().toLowerCase()); | ||
| generator.writeFieldName(TERM); | ||
| toJson(predicate.term(), generator); | ||
| if (!(ONE_INPUTS.contains(predicate.op()))) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. redundant brackets after ! |
||
| generator.writeFieldName(LITERALS); | ||
| generator.writeStartArray(); | ||
| for (Literal<?> literal : predicate.literals()) { | ||
| toJson(literal, generator); | ||
| } | ||
| generator.writeEndArray(); | ||
| } | ||
|
|
||
| generator.writeEndObject(); | ||
| } | ||
|
|
||
| public static void toJson(Term term, JsonGenerator generator) throws IOException { | ||
| if (term instanceof NamedReference) { | ||
| toJson((NamedReference<?>) term, generator); | ||
| } else { | ||
| throw new IllegalArgumentException("Cannot convert term " + term); | ||
| } | ||
| } | ||
|
|
||
| public static void toJson(NamedReference<?> term, JsonGenerator generator) throws IOException { | ||
| generator.writeStartObject(); | ||
| generator.writeStringField(TYPE, NAMED_REFERENCE); | ||
| generator.writeStringField(VALUE, term.name()); | ||
| generator.writeEndObject(); | ||
| } | ||
|
|
||
| public static void toJson(List<Literal<?>> literals, JsonGenerator generator) throws IOException { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this method seems unnecessary, can be combined to parent method, also can use enhanced for loop |
||
| generator.writeStartArray(); | ||
| for (int i = 0; i < literals.size(); i++) { | ||
| toJson(literals.get(i), generator); | ||
| } | ||
| generator.writeEndArray(); | ||
| } | ||
|
|
||
| public static void toJson(Literal<?> literal, JsonGenerator generator) throws IOException { | ||
| generator.writeStartObject(); | ||
|
|
||
| Object value = literal.value(); | ||
| Type type; | ||
| if (value instanceof Boolean) { | ||
| type = Types.BooleanType.get(); | ||
| } else if (value instanceof Integer) { | ||
| type = Types.IntegerType.get(); | ||
| } else if (value instanceof Long) { | ||
| type = Types.LongType.get(); | ||
| } else if (value instanceof Float) { | ||
| type = Types.FloatType.get(); | ||
| } else if (value instanceof Double) { | ||
| type = Types.DoubleType.get(); | ||
| } else if (value instanceof CharSequence) { | ||
| type = Types.StringType.get(); | ||
| } else if (value instanceof UUID) { | ||
| type = Types.UUIDType.get(); | ||
| } else if (value instanceof byte[]) { | ||
| type = Types.FixedType.ofLength(((byte[]) value).length); | ||
| } else if (value instanceof ByteBuffer) { | ||
| if (literal instanceof Literals.FixedLiteral) { | ||
| type = Types.FixedType.ofLength(((ByteBuffer) value).remaining()); | ||
| } else { | ||
| type = Types.BinaryType.get(); | ||
| } | ||
| } else if (value instanceof BigDecimal) { | ||
| BigDecimal decimal = (BigDecimal) value; | ||
| type = Types.DecimalType.of(decimal.precision(), decimal.scale()); | ||
| } else { | ||
| throw new IllegalArgumentException("Cannot find literal type for value class " + value.getClass().getName()); | ||
| } | ||
|
|
||
| generator.writeStringField(TYPE, type.toString()); | ||
| generator.writeStringField(VALUE, StandardCharsets.UTF_8.decode(literal.toByteBuffer()).toString()); | ||
| generator.writeEndObject(); | ||
| } | ||
|
|
||
| public static Expression fromJson(String json) { | ||
| try { | ||
| ObjectMapper mapper = new ObjectMapper(); | ||
| return fromJson(mapper.readTree(json)); | ||
| } catch (IOException e) { | ||
| throw new RuntimeIOException(e); | ||
| } | ||
| } | ||
|
|
||
| public static Expression fromJson(JsonNode json) { | ||
| String expressionType = JsonUtil.getString(TYPE, json); | ||
|
|
||
| if (AND.equals(expressionType)) { | ||
| return new And(fromJson(json.get(LEFT_OPERAND)), fromJson(json.get(RIGHT_OPERAND))); | ||
| } else if (OR.equals(expressionType)) { | ||
| return new Or(fromJson(json.get(LEFT_OPERAND)), fromJson(json.get(RIGHT_OPERAND))); | ||
| } else if (NOT.equals(expressionType)) { | ||
| return new Not(fromJson(json.get(OPERAND))); | ||
| } else if (TRUE.equals(expressionType)) { | ||
| return True.INSTANCE; | ||
| } else if (FALSE.equals(expressionType)) { | ||
| return False.INSTANCE; | ||
| } else if (PREDICATE_TYPES.contains(expressionType)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we don't need a set of predicate types, we just need to have a single else block, delegating all the rest of the conditions to |
||
| return fromJsonToPredicate(json, expressionType); | ||
| } else { | ||
| throw new IllegalArgumentException("Invalid Operation Type"); | ||
| } | ||
| } | ||
|
|
||
| public static Predicate<?, ?> fromJsonToPredicate(JsonNode json, String predicateType) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please keep all methods private except for the main one for Expression |
||
| if (UNBOUNDED_PREDICATE.equals(predicateType)) { | ||
| return fromJsonUnboundPredicate(json); | ||
| } else if (BOUNDED_LITERAL_PREDICATE.equals(predicateType)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since we are only targeting unbounded, we can just use a generic else block saying we don't support bounded expression yet |
||
| throw new UnsupportedOperationException( | ||
| "Serialization of Predicate type BoundLiteralPredicate is not currently supported."); | ||
| } else if (BOUNDED_SET_PREDICATE.equals(predicateType)) { | ||
| throw new UnsupportedOperationException( | ||
| "Serialization of Predicate type BoundSetPredicate is not currently supported."); | ||
| } else if (BOUNDED_UNARY_PREDICATE.equals(predicateType)) { | ||
| throw new UnsupportedOperationException( | ||
| "Serialization of Predicate type BoundUnaryPredicate is not currently supported."); | ||
| } else { | ||
| throw new IllegalArgumentException("Invalid Predicate Type"); | ||
| } | ||
| } | ||
|
|
||
| @SuppressWarnings("CyclomaticComplexity") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is not needed? |
||
| public static UnboundPredicate<?> fromJsonUnboundPredicate(JsonNode json) { | ||
| Expression.Operation operation = Expression.Operation.valueOf( | ||
| JsonUtil.getString(OPERATION, json).toUpperCase(Locale.ENGLISH)); | ||
|
|
||
| if (ONE_INPUTS.contains(operation)) { | ||
| return new UnboundPredicate<>(operation, fromJsonToTerm(json.get(TERM))); | ||
| } else { | ||
| return new UnboundPredicate( | ||
| operation, | ||
| fromJsonToTerm(json.get(TERM)), | ||
| fromJsonToLiteralValues(json.get(LITERALS))); | ||
| } | ||
| } | ||
|
|
||
| public static UnboundTerm<?> fromJsonToTerm(JsonNode json) { | ||
| String referenceType = json.get(TYPE).textValue(); | ||
|
|
||
| if (referenceType.equals(NAMED_REFERENCE)) { | ||
| return new NamedReference<>(json.get(VALUE).textValue()); | ||
| } else if (referenceType.equals(BOUND_REFERENCE)) { | ||
| throw new UnsupportedOperationException( | ||
| "Serialization of Predicate type BoundReference is not currently supported."); | ||
| } else { | ||
| throw new IllegalArgumentException("Invalid Term Reference Type"); | ||
| } | ||
| } | ||
|
|
||
| public static List<Object> fromJsonToLiteralValues(JsonNode json) { | ||
| List<Object> literals = Lists.newArrayList(); | ||
| for (int i = 0; i < json.size(); i++) { | ||
| String literalType = json.get(i).get(TYPE).textValue(); | ||
| Type primitiveType = Types.fromPrimitiveString(literalType); | ||
| Object value = Conversions.fromByteBuffer( | ||
| primitiveType, | ||
| StandardCharsets.UTF_8.encode(json.get(i).get(VALUE).textValue())); | ||
| if (primitiveType.typeId() == Type.TypeID.FIXED) { | ||
| byte[] valueByteArray = new byte[((ByteBuffer) value).remaining()]; | ||
| ((ByteBuffer) value).get(valueByteArray); | ||
| value = valueByteArray; | ||
| } | ||
|
|
||
| literals.add(value); | ||
| } | ||
|
|
||
| return literals; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these can just use operation.name().toLowerCase(), no need to define extra variables