diff options
author | Jon Bratseth <bratseth@gmail.com> | 2023-01-20 13:54:28 +0100 |
---|---|---|
committer | Jon Bratseth <bratseth@gmail.com> | 2023-01-20 13:54:28 +0100 |
commit | 97707f8b011edf555776b6a6bf36d35458f583ae (patch) | |
tree | aec9c68dd32e39241c34fdc1bfa270cdf19d4aa2 /indexinglanguage | |
parent | 1e29c4da6f91d13a6889d1b71058a09fcbb4eb43 (diff) |
Support choice expressions
Diffstat (limited to 'indexinglanguage')
14 files changed, 205 insertions, 65 deletions
diff --git a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/ChoiceExpression.java b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/ChoiceExpression.java new file mode 100644 index 00000000000..5d92b726ce4 --- /dev/null +++ b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/ChoiceExpression.java @@ -0,0 +1,87 @@ +package com.yahoo.vespa.indexinglanguage.expressions; + +import com.yahoo.document.DataType; +import com.yahoo.document.datatypes.FieldValue; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * An expression which returns the value of the first of a list of subexpressions which + * returns a non-null value. + * + * Syntax: sub-expression1 || sub-expression2 || ... + * + * @author bratseth + */ +public class ChoiceExpression extends ExpressionList<Expression> { + + public ChoiceExpression() { + this(List.of()); + } + + public ChoiceExpression(Expression... choices) { + this(Arrays.asList(choices)); + } + + public ChoiceExpression(Collection<? extends Expression> choices) { + super(choices, resolveInputType(choices)); + } + + @Override + protected void doExecute(ExecutionContext context) { + FieldValue input = context.getValue(); + for (Expression expression : this) { + context.setValue(input).execute(expression); + if (context.getValue() != null) + break; // value found + } + } + + @Override + protected void doVerify(VerificationContext context) { + DataType input = context.getValueType(); + for (Expression exp : this) + context.setValueType(input).execute(exp); + context.setValueType(input); + } + + private static DataType resolveInputType(Collection<? extends Expression> list) { + DataType previousInput = null; + DataType previousOutput = null; + for (Expression choice : list) { + DataType thisInput = choice.requiredInputType(); + if (previousInput == null) + previousInput = thisInput; + else if (thisInput != null && !previousInput.isAssignableFrom(thisInput)) + throw new VerificationException(ScriptExpression.class, "Choice expressions require conflicting input types, " + + previousInput.getName() + " vs " + thisInput.getName() + "."); + + DataType thisOutput = choice.createdOutputType(); + if (previousOutput == null) + previousOutput = thisOutput; + else if (thisOutput != null && !previousOutput.isAssignableFrom(thisOutput)) + throw new VerificationException(ScriptExpression.class, "Choice expressions produce conflicting output types, " + + previousOutput.getName() + " vs " + thisOutput.getName() + "."); + } + return previousInput; + } + + @Override + public DataType createdOutputType() { + return UnresolvedDataType.INSTANCE; + } + + @Override + public String toString() { + return asList().stream().map(Object::toString).collect(Collectors.joining(" || ")); + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj) && obj instanceof ChoiceExpression; + } + +} diff --git a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/CompositeExpression.java b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/CompositeExpression.java index 64043eddab8..27e5524f4ad 100644 --- a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/CompositeExpression.java +++ b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/CompositeExpression.java @@ -11,6 +11,7 @@ public abstract class CompositeExpression extends Expression { protected CompositeExpression(DataType inputType) { super(inputType); } + protected static String toScriptBlock(Expression exp) { if (exp instanceof ScriptExpression) { return exp.toString(); diff --git a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/ExecutionContext.java b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/ExecutionContext.java index 389259cc811..d463bee647e 100644 --- a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/ExecutionContext.java +++ b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/ExecutionContext.java @@ -31,9 +31,8 @@ public class ExecutionContext implements FieldTypeAdapter, FieldValueAdapter, Cl } public ExecutionContext execute(Expression exp) { - if (exp != null) { + if (exp != null) exp.execute(this); - } return this; } @@ -65,9 +64,8 @@ public class ExecutionContext implements FieldTypeAdapter, FieldValueAdapter, Cl @Override public ExecutionContext setOutputValue(Expression exp, String fieldName, FieldValue fieldValue) { - if (adapter == null) { + if (adapter == null) throw new IllegalStateException("Can not set field '" + fieldName + "' because adapter is null."); - } adapter.setOutputValue(exp, fieldName, fieldValue); return this; } diff --git a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/Expression.java b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/Expression.java index cabf4f631c8..bf8201ee7ee 100644 --- a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/Expression.java +++ b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/Expression.java @@ -206,18 +206,13 @@ public abstract class Expression extends Selectable { protected static boolean equals(Object lhs, Object rhs) { if (lhs == null) { - if (rhs != null) { - return false; - } + return rhs == null; } else { if (rhs == null) { return false; } - if (!lhs.equals(rhs)) { - return false; - } + return lhs.equals(rhs); } - return true; } // Convenience For testing diff --git a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/ExpressionList.java b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/ExpressionList.java index ff0a325b23f..e2ff1de7126 100644 --- a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/ExpressionList.java +++ b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/ExpressionList.java @@ -56,12 +56,8 @@ public abstract class ExpressionList<T extends Expression> extends CompositeExpr @Override @SuppressWarnings("rawtypes") public boolean equals(Object obj) { - if (!(obj instanceof ExpressionList rhs)) { - return false; - } - if (!expressions.equals(rhs.expressions)) { - return false; - } + if (!(obj instanceof ExpressionList rhs)) return false; + if (!expressions.equals(rhs.expressions)) return false; return true; } diff --git a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/IfThenExpression.java b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/IfThenExpression.java index 72cd65e43ed..8a29c8e8645 100644 --- a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/IfThenExpression.java +++ b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/IfThenExpression.java @@ -191,7 +191,7 @@ public final class IfThenExpression extends CompositeExpression { } if (!prev.equals(next)) { throw new VerificationException(IfThenExpression.class, "Operands require conflicting input types, " + - prev.getName() + " vs " + next.getName() + "."); + prev.getName() + " vs " + next.getName() + "."); } return prev; } diff --git a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/InputExpression.java b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/InputExpression.java index de347904a21..30c824d410d 100644 --- a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/InputExpression.java +++ b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/InputExpression.java @@ -9,6 +9,7 @@ import com.yahoo.vespa.objects.ObjectPredicate; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * @author Simon Thoresen Hult @@ -20,7 +21,7 @@ public final class InputExpression extends Expression { public InputExpression(String fieldName) { super(null); - this.fieldName = fieldName; + this.fieldName = Objects.requireNonNull(fieldName); } public String getFieldName() { @@ -29,19 +30,17 @@ public final class InputExpression extends Expression { @Override protected void doExecute(ExecutionContext context) { - if (fieldPath != null) { + if (fieldPath != null) context.setValue(context.getInputValue(fieldPath)); - } else { + else context.setValue(context.getInputValue(fieldName)); - } } @Override protected void doVerify(VerificationContext context) { DataType val = context.getInputType(this, fieldName); - if (val == null) { + if (val == null) throw new VerificationException(this, "Field '" + fieldName + "' not found."); - } context.setValueType(val); } @@ -92,8 +91,6 @@ public final class InputExpression extends Expression { private final List<String> inputFieldNames = new ArrayList<>(1); - public List<String> getInputFieldNames() { return inputFieldNames; } - @Override public void execute(Object obj) { inputFieldNames.add(((InputExpression) obj).getFieldName()); @@ -104,6 +101,12 @@ public final class InputExpression extends Expression { return obj instanceof InputExpression; } + public static List<String> runOn(Expression expression) { + InputExpression.InputFieldNameExtractor inputFieldNameExtractor = new InputExpression.InputFieldNameExtractor(); + expression.select(inputFieldNameExtractor, inputFieldNameExtractor); + return inputFieldNameExtractor.inputFieldNames; + } + } } diff --git a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/ScriptExpression.java b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/ScriptExpression.java index d2f807b99ae..8287252b268 100644 --- a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/ScriptExpression.java +++ b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/ScriptExpression.java @@ -15,6 +15,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; +import java.util.List; import java.util.Map; /** @@ -37,11 +38,20 @@ public final class ScriptExpression extends ExpressionList<StatementExpression> @Override protected void doExecute(ExecutionContext context) { FieldValue input = context.getValue(); - for (Expression expression : this) - context.setValue(input).execute(expression); + for (StatementExpression statement : this) { + if (statement.getInputFields().isEmpty() || containsAtLeastOneInputFrom(statement.getInputFields(), context)) + context.setValue(input).execute(statement); + } context.setValue(input); } + private boolean containsAtLeastOneInputFrom(List<String> inputFields, ExecutionContext context) { + for (String inputField : inputFields) + if (context.getInputValue(inputField) != null) + return true; + return false; + } + @Override protected void doVerify(VerificationContext context) { DataType input = context.getValueType(); @@ -59,7 +69,7 @@ public final class ScriptExpression extends ExpressionList<StatementExpression> prev = next; } else if (next != null && !prev.isAssignableFrom(next)) { throw new VerificationException(ScriptExpression.class, "Statements require conflicting input types, " + - prev.getName() + " vs " + next.getName() + "."); + prev.getName() + " vs " + next.getName() + "."); } } return prev; diff --git a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/StatementExpression.java b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/StatementExpression.java index 1a43d2fb36c..8516ddb883d 100644 --- a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/StatementExpression.java +++ b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/expressions/StatementExpression.java @@ -15,13 +15,17 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * @author Simon Thoresen Hult */ public final class StatementExpression extends ExpressionList<Expression> { - /** The name of the (last) output field tthis statement will write to, or null if none */ + /** The names of the fields consumed by this. */ + private final List<String> inputFields; + + /** The name of the (last) output field this statement will write to, or null if none */ private String outputField; public StatementExpression(Expression... lst) { @@ -34,12 +38,17 @@ public final class StatementExpression extends ExpressionList<Expression> { private StatementExpression(Iterable<Expression> list, Object unused) { super(list, resolveInputType(list)); + inputFields = List.copyOf(InputExpression.InputFieldNameExtractor.runOn(this)); } + /** Returns the input fields which are (perhaps optionally) consumed by some expression in this statement. */ + public List<String> getInputFields() { return inputFields; } + @Override protected void doExecute(ExecutionContext context) { - for (Expression expression : this) + for (Expression expression : this) { context.execute(expression); + } } @Override @@ -57,14 +66,10 @@ public final class StatementExpression extends ExpressionList<Expression> { private static DataType resolveInputType(Iterable<Expression> lst) { for (Expression exp : lst) { DataType type = exp.requiredInputType(); - if (type != null) { - return type; - } + if (type != null) return type; type = exp.createdOutputType(); - if (type != null) { - return null; - } + if (type != null) return null; } return null; } @@ -73,23 +78,14 @@ public final class StatementExpression extends ExpressionList<Expression> { public DataType createdOutputType() { for (int i = size(); --i >= 0; ) { DataType type = get(i).createdOutputType(); - if (type != null) { - return type; - } + if (type != null) return type; } return null; } @Override public String toString() { - StringBuilder ret = new StringBuilder(); - for (Iterator<Expression> it = iterator(); it.hasNext();) { - ret.append(it.next()); - if (it.hasNext()) { - ret.append(" | "); - } - } - return ret.toString(); + return asList().stream().map(Expression::toString).collect(Collectors.joining(" | ")); } @Override diff --git a/indexinglanguage/src/main/javacc/IndexingParser.jj b/indexinglanguage/src/main/javacc/IndexingParser.jj index 51bb9be1f8a..9f0b4733119 100644 --- a/indexinglanguage/src/main/javacc/IndexingParser.jj +++ b/indexinglanguage/src/main/javacc/IndexingParser.jj @@ -8,7 +8,6 @@ options { CACHE_TOKENS = false; DEBUG_PARSER = false; ERROR_REPORTING = true; - STATIC = false; USER_CHAR_STREAM = true; } @@ -24,7 +23,7 @@ package com.yahoo.vespa.indexinglanguage.parser; import java.math.BigDecimal; import java.math.BigInteger; import java.util.List; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.Map; import java.util.LinkedHashMap; @@ -137,6 +136,7 @@ TOKEN : <LE: "<="> | <GT: ">"> | <GE: ">="> | + <CHOICE: "||"> | <PIPE: "|"> | <LCURLY: "{"> | <RCURLY: "}"> | @@ -229,7 +229,7 @@ Expression root() : ScriptExpression script() : { StatementExpression exp; - List<StatementExpression> lst = new LinkedList<StatementExpression>(); + List<StatementExpression> lst = new ArrayList<StatementExpression>(); } { ( <LCURLY> nl() exp = statement() { lst.add(exp); } nl() @@ -240,7 +240,7 @@ ScriptExpression script() : StatementExpression statement() : { Expression exp; - List<Expression> lst = new LinkedList<Expression>(); + List<Expression> lst = new ArrayList<Expression>(); } { ( exp = expression() { lst.add(exp); } ( <PIPE> nl() exp = expression() { lst.add(exp); } )* ) @@ -249,8 +249,19 @@ StatementExpression statement() : Expression expression() : { + Expression choice; + List<Expression> choices = new ArrayList<Expression>(); +} +{ + choice = nonChoiceExpression() { choices.add(choice); } + ( <CHOICE> choice = nonChoiceExpression() { choices.add(choice); } )* + { return choices.size() == 1 ? choices.get(0) : new ChoiceExpression(choices); } +} + +Expression nonChoiceExpression() : +{ Expression exp; - List<Expression> lst = new LinkedList<Expression>(); + List<Expression> lst = new ArrayList<Expression>(); } { ( exp = math() { lst.add(exp); } ( <DOT> exp = math() { lst.add(exp); } )* ) @@ -274,6 +285,8 @@ Expression math() : { return math.resolve(); } } + + Expression value() : { Expression val; @@ -553,7 +566,7 @@ Expression randomExp() : Expression selectInputExp() : { - List<Pair<String, Expression>> cases = new LinkedList<Pair<String, Expression>>(); + List<Pair<String, Expression>> cases = new ArrayList<Pair<String, Expression>>(); Expression exp; String str; } diff --git a/indexinglanguage/src/test/java/com/yahoo/vespa/indexinglanguage/expressions/ChoiceTestCase.java b/indexinglanguage/src/test/java/com/yahoo/vespa/indexinglanguage/expressions/ChoiceTestCase.java new file mode 100644 index 00000000000..6e3964e644e --- /dev/null +++ b/indexinglanguage/src/test/java/com/yahoo/vespa/indexinglanguage/expressions/ChoiceTestCase.java @@ -0,0 +1,46 @@ +package com.yahoo.vespa.indexinglanguage.expressions; + +import com.yahoo.document.DataType; +import com.yahoo.document.Field; +import com.yahoo.document.datatypes.StringFieldValue; +import com.yahoo.vespa.indexinglanguage.SimpleTestAdapter; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author bratseth + */ +public class ChoiceTestCase { + + @Test + public void testChoiceExecution() { + var choice = new ChoiceExpression(new InputExpression("foo"), new InputExpression("bar")); + + { // foo only + var adapter = new SimpleTestAdapter(new Field("foo", DataType.STRING)); + adapter.setValue("foo", new StringFieldValue("foo1")); + ExecutionContext context = new ExecutionContext(adapter); + choice.execute(context); + assertEquals("foo1", context.getValue().getWrappedValue()); + } + + { // bar only + var adapter = new SimpleTestAdapter(new Field("bar", DataType.STRING)); + adapter.setValue("bar", new StringFieldValue("bar1")); + ExecutionContext context = new ExecutionContext(adapter); + choice.execute(context); + assertEquals("bar1", context.getValue().getWrappedValue()); + } + + { // both foo and bar + var adapter = new SimpleTestAdapter(new Field("foo", DataType.STRING), new Field("bar", DataType.STRING)); + adapter.setValue("foo", new StringFieldValue("foo1")); + adapter.setValue("bar", new StringFieldValue("bar1")); + ExecutionContext context = new ExecutionContext(adapter); + choice.execute(context); + assertEquals("foo1", context.getValue().getWrappedValue()); + } + } + +} diff --git a/indexinglanguage/src/test/java/com/yahoo/vespa/indexinglanguage/expressions/ExactTestCase.java b/indexinglanguage/src/test/java/com/yahoo/vespa/indexinglanguage/expressions/ExactTestCase.java index 68661c7b341..e993f6cbdee 100644 --- a/indexinglanguage/src/test/java/com/yahoo/vespa/indexinglanguage/expressions/ExactTestCase.java +++ b/indexinglanguage/src/test/java/com/yahoo/vespa/indexinglanguage/expressions/ExactTestCase.java @@ -21,7 +21,7 @@ public class ExactTestCase { @Test public void requireThatHashCodeAndEqualsAreImplemented() { Expression exp = new ExactExpression(); - assertFalse(exp.equals(new Object())); + assertNotEquals(exp, new Object()); assertEquals(exp, new ExactExpression()); assertEquals(exp.hashCode(), new ExactExpression().hashCode()); } diff --git a/indexinglanguage/src/test/java/com/yahoo/vespa/indexinglanguage/expressions/InputTestCase.java b/indexinglanguage/src/test/java/com/yahoo/vespa/indexinglanguage/expressions/InputTestCase.java index 5752c373001..fe16e8d10ab 100644 --- a/indexinglanguage/src/test/java/com/yahoo/vespa/indexinglanguage/expressions/InputTestCase.java +++ b/indexinglanguage/src/test/java/com/yahoo/vespa/indexinglanguage/expressions/InputTestCase.java @@ -24,15 +24,9 @@ public class InputTestCase { @Test public void requireThatHashCodeAndEqualsAreImplemented() { - Expression exp = new InputExpression(null); - assertFalse(exp.equals(new Object())); - assertFalse(exp.equals(new InputExpression("foo"))); - assertEquals(exp, new InputExpression(null)); - assertEquals(exp.hashCode(), new InputExpression(null).hashCode()); - - exp = new InputExpression("foo"); - assertFalse(exp.equals(new Object())); - assertFalse(exp.equals(new InputExpression("bar"))); + Expression exp = new InputExpression("foo"); + assertNotEquals(exp, new Object()); + assertNotEquals(exp, new InputExpression("bar")); assertEquals(exp, new InputExpression("foo")); assertEquals(exp.hashCode(), new InputExpression("foo").hashCode()); } diff --git a/indexinglanguage/src/test/java/com/yahoo/vespa/indexinglanguage/parser/ExpressionTestCase.java b/indexinglanguage/src/test/java/com/yahoo/vespa/indexinglanguage/parser/ExpressionTestCase.java index 7db026d43ee..551f770a01e 100644 --- a/indexinglanguage/src/test/java/com/yahoo/vespa/indexinglanguage/parser/ExpressionTestCase.java +++ b/indexinglanguage/src/test/java/com/yahoo/vespa/indexinglanguage/parser/ExpressionTestCase.java @@ -37,7 +37,7 @@ public class ExpressionTestCase { assertExpression(IfThenExpression.class, "if (1 < 2) { 3 }"); assertExpression(IfThenExpression.class, "if (1 < 2) { 3 } else { 4 }"); assertExpression(IndexExpression.class, "index"); - assertExpression(InputExpression.class, "input"); + assertExpression(InputExpression.class, "input foo"); assertExpression(InputExpression.class, "input field1"); assertExpression(JoinExpression.class, "join '1'"); assertExpression(LowerCaseExpression.class, "lowercase"); @@ -81,6 +81,7 @@ public class ExpressionTestCase { assertExpression(ToWsetExpression.class, "to_wset remove_if_zero create_if_non_existent"); assertExpression(TrimExpression.class, "trim"); assertExpression(ZCurveExpression.class, "zcurve"); + assertExpression(ChoiceExpression.class, "input foo || \"\""); } private static void assertExpression(Class expectedClass, String str) throws ParseException { |