From 7994462805a16f4665e52a0b9f9770d3c7563556 Mon Sep 17 00:00:00 2001 From: Jon Bratseth Date: Thu, 20 Sep 2018 09:13:16 -0700 Subject: Resolve types of all argument-less functions --- .../com/yahoo/searchdefinition/RankProfile.java | 23 +- .../java/com/yahoo/searchdefinition/Search.java | 14 -- .../com/yahoo/searchdefinition/SearchBuilder.java | 7 +- .../derived/DerivedConfiguration.java | 10 - .../searchdefinition/derived/RawRankProfile.java | 32 +-- .../searchdefinition/processing/Processing.java | 28 ++- .../processing/RankingExpressionTypeResolver.java | 103 +++++++++ .../processing/RankingExpressionTypeValidator.java | 84 -------- .../RankProfileTypeSettingsProcessor.java | 2 + .../main/java/com/yahoo/vespa/model/Service.java | 2 +- .../java/com/yahoo/vespa/model/VespaModel.java | 23 +- config-model/src/test/derived/gemini2/gemini.sd | 6 + .../RankingExpressionLoopDetectionTestCase.java | 6 +- .../processing/IntegerIndex2AttributeTestCase.java | 1 - .../RankingExpressionTypeResolverTestCase.java | 238 +++++++++++++++++++++ .../RankingExpressionTypeValidatorTestCase.java | 238 --------------------- .../SummaryFieldsMustHaveValidSourceTestCase.java | 5 - .../processing/TensorTransformTestCase.java | 4 +- 18 files changed, 428 insertions(+), 398 deletions(-) create mode 100644 config-model/src/main/java/com/yahoo/searchdefinition/processing/RankingExpressionTypeResolver.java delete mode 100644 config-model/src/main/java/com/yahoo/searchdefinition/processing/RankingExpressionTypeValidator.java create mode 100644 config-model/src/test/java/com/yahoo/searchdefinition/processing/RankingExpressionTypeResolverTestCase.java delete mode 100644 config-model/src/test/java/com/yahoo/searchdefinition/processing/RankingExpressionTypeValidatorTestCase.java (limited to 'config-model') diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/RankProfile.java b/config-model/src/main/java/com/yahoo/searchdefinition/RankProfile.java index 16e494c2db1..a06cf36c7bb 100644 --- a/config-model/src/main/java/com/yahoo/searchdefinition/RankProfile.java +++ b/config-model/src/main/java/com/yahoo/searchdefinition/RankProfile.java @@ -449,7 +449,7 @@ public class RankProfile implements Serializable, Cloneable { addRankProperty(new RankProperty(name, parameter)); } - public void addRankProperty(RankProperty rankProperty) { + private void addRankProperty(RankProperty rankProperty) { // Just the usual multimap semantics here List properties = rankProperties.get(rankProperty.getName()); if (properties == null) { @@ -534,22 +534,21 @@ public class RankProfile implements Serializable, Cloneable { /** Adds a function and returns it */ public RankingExpressionFunction addFunction(ExpressionFunction function, boolean inline) { - RankingExpressionFunction rankingExpressionFunction = new RankingExpressionFunction(function, inline); + RankingExpressionFunction rankingExpressionFunction = new RankingExpressionFunction(function, inline, Optional.empty()); functions.put(function.getName(), rankingExpressionFunction); return rankingExpressionFunction; } /** Returns an unmodifiable view of the functions in this */ public Map getFunctions() { - if (functions.size() == 0 && getInherited() == null) return Collections.emptyMap(); - if (functions.size() == 0) return getInherited().getFunctions(); + if (functions.isEmpty() && getInherited() == null) return Collections.emptyMap(); + if (functions.isEmpty()) return getInherited().getFunctions(); if (getInherited() == null) return Collections.unmodifiableMap(functions); // Neither is null Map allFunctions = new LinkedHashMap<>(getInherited().getFunctions()); allFunctions.putAll(functions); return Collections.unmodifiableMap(allFunctions); - } public int getKeepRankCount() { @@ -903,9 +902,16 @@ public class RankProfile implements Serializable, Cloneable { /** True if this should be inlined into calling expressions. Useful for very cheap functions. */ private final boolean inline; - public RankingExpressionFunction(ExpressionFunction function, boolean inline) { + private Optional type = Optional.empty(); + + public RankingExpressionFunction(ExpressionFunction function, boolean inline, Optional type) { this.function = function; this.inline = inline; + this.type = type; + } + + public void setType(TensorType type) { + this.type = Optional.of(type); } public ExpressionFunction function() { return function; } @@ -914,8 +920,11 @@ public class RankProfile implements Serializable, Cloneable { return inline && function.arguments().isEmpty(); // only inline no-arg functions; } + /** Returns the type this produces, or empty if not resolved */ + public Optional type() { return type; } + public RankingExpressionFunction withBody(RankingExpression expression) { - return new RankingExpressionFunction(function.withBody(expression), inline); + return new RankingExpressionFunction(function.withBody(expression), inline, type); } @Override diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/Search.java b/config-model/src/main/java/com/yahoo/searchdefinition/Search.java index f42d5de21e8..a988da9664e 100644 --- a/config-model/src/main/java/com/yahoo/searchdefinition/Search.java +++ b/config-model/src/main/java/com/yahoo/searchdefinition/Search.java @@ -59,9 +59,6 @@ public class Search implements Serializable, ImmutableSearch { // Field sets private FieldSets fieldSets = new FieldSets(); - // Whether or not this object has been processed. - private boolean processed; - // The unique name of this search definition. private String name; @@ -585,17 +582,6 @@ public class Search implements Serializable, ImmutableSearch { return false; } - public void process() { - if (processed) { - throw new IllegalStateException("Search '" + getName() + "' already processed."); - } - processed = true; - } - - public boolean isProcessed() { - return processed; - } - /** * The field set settings for this search * diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/SearchBuilder.java b/config-model/src/main/java/com/yahoo/searchdefinition/SearchBuilder.java index 3c2ebc058ac..151ad02a3fa 100644 --- a/config-model/src/main/java/com/yahoo/searchdefinition/SearchBuilder.java +++ b/config-model/src/main/java/com/yahoo/searchdefinition/SearchBuilder.java @@ -188,10 +188,6 @@ public class SearchBuilder { throw new IllegalArgumentException("Search has no name."); } String rawName = rawSearch.getName(); - if (rawSearch.isProcessed()) { - throw new IllegalArgumentException("A search definition with a search section called '" + rawName + - "' has already been processed."); - } for (Search search : searchList) { if (rawName.equals(search.getName())) { throw new IllegalArgumentException("A search definition with a search section called '" + rawName + @@ -247,8 +243,7 @@ public class SearchBuilder { DocumentModelBuilder builder = new DocumentModelBuilder(model); for (Search search : new SearchOrderer().order(searchList)) { - new FieldOperationApplierForSearch().process(search); - // These two needed for a couple of old unit tests, ideally these are just read from app + new FieldOperationApplierForSearch().process(search); // TODO: Why is this not in the regular list? process(search, deployLogger, new QueryProfiles(queryProfileRegistry), validate); built.add(search); } diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/DerivedConfiguration.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/DerivedConfiguration.java index 9a00ee5bbd0..7c2d9a3b0ad 100644 --- a/config-model/src/main/java/com/yahoo/searchdefinition/derived/DerivedConfiguration.java +++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/DerivedConfiguration.java @@ -74,21 +74,11 @@ public class DerivedConfiguration { QueryProfileRegistry queryProfiles, ImportedModels importedModels) { Validator.ensureNotNull("Search definition", search); - if ( ! search.isProcessed()) { - throw new IllegalArgumentException("Search '" + search.getName() + "' not processed."); - } this.search = search; if ( ! search.isDocumentsOnly()) { streamingFields = new VsmFields(search); streamingSummary = new VsmSummary(search); } - if (abstractSearchList != null) { - for (Search abstractSearch : abstractSearchList) { - if (!abstractSearch.isProcessed()) { - throw new IllegalArgumentException("Search '" + search.getName() + "' not processed."); - } - } - } if ( ! search.isDocumentsOnly()) { attributeFields = new AttributeFields(search); summaries = new Summaries(search, deployLogger); diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/RawRankProfile.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/RawRankProfile.java index c041d5c6a89..c3a2673ef2b 100644 --- a/config-model/src/main/java/com/yahoo/searchdefinition/derived/RawRankProfile.java +++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/RawRankProfile.java @@ -23,6 +23,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; /** * A rank profile derived from a search definition, containing exactly the features available natively in the server @@ -180,32 +181,37 @@ public class RawRankProfile implements RankProfilesConfig.Producer { private void derivePropertiesAndSummaryFeaturesFromFunctions(Map functions) { if (functions.isEmpty()) return; - Map expressionFunctions = new LinkedHashMap<>(); - for (Map.Entry function : functions.entrySet()) { - expressionFunctions.put(function.getKey(), function.getValue().function()); - } + + List functionExpressions = functions.values().stream().map(f -> f.function()).collect(Collectors.toList()); Map functionProperties = new LinkedHashMap<>(); - functionProperties.putAll(deriveFunctionProperties(expressionFunctions)); + functionProperties.putAll(deriveFunctionProperties(functions, functionExpressions)); + if (firstPhaseRanking != null) { - functionProperties.putAll(firstPhaseRanking.getRankProperties(new ArrayList<>(expressionFunctions.values()))); + functionProperties.putAll(firstPhaseRanking.getRankProperties(functionExpressions)); } if (secondPhaseRanking != null) { - functionProperties.putAll(secondPhaseRanking.getRankProperties(new ArrayList<>(expressionFunctions.values()))); + functionProperties.putAll(secondPhaseRanking.getRankProperties(functionExpressions)); } for (Map.Entry e : functionProperties.entrySet()) { rankProperties.add(new RankProfile.RankProperty(e.getKey(), e.getValue())); } - SerializationContext context = new SerializationContext(expressionFunctions.values(), null, functionProperties); + SerializationContext context = new SerializationContext(functionExpressions, null, functionProperties); replaceFunctionSummaryFeatures(context); } - private Map deriveFunctionProperties(Map functions) { - SerializationContext context = new SerializationContext(functions); - for (Map.Entry e : functions.entrySet()) { - String expression = e.getValue().getBody().getRoot().toString(new StringBuilder(), context, null, null).toString(); - context.addFunctionSerialization(RankingExpression.propertyName(e.getKey()), expression); + private Map deriveFunctionProperties(Map functions, + List functionExpressions) { + SerializationContext context = new SerializationContext(functionExpressions); + for (Map.Entry e : functions.entrySet()) { + String expressionString = e.getValue().function().getBody().getRoot().toString(new StringBuilder(), context, null, null).toString(); + context.addFunctionSerialization(RankingExpression.propertyName(e.getKey()), expressionString); + + if (e.getValue().type().isPresent()) + context.addFunctionTypeSerialization(RankingExpression.propertyTypeName(e.getKey()), e.getValue().type().get().toString()); + else if (e.getValue().function().arguments().isEmpty()) + throw new IllegalStateException("Type of function '" + e.getKey() + "' is not resolved"); } return context.serializedFunctions(); } diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/Processing.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/Processing.java index 8c8c32389e2..15d295736c1 100644 --- a/config-model/src/main/java/com/yahoo/searchdefinition/processing/Processing.java +++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/Processing.java @@ -7,10 +7,8 @@ import com.yahoo.searchdefinition.Search; import com.yahoo.searchdefinition.processing.multifieldresolver.RankProfileTypeSettingsProcessor; import com.yahoo.vespa.model.container.search.QueryProfiles; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.List; /** * Executor of processors. This defines the right order of processor execution. @@ -75,12 +73,20 @@ public class Processing { ReferenceFieldsProcessor::new, FastAccessValidator::new, ReservedFunctionNames::new, - RankingExpressionTypeValidator::new, + RankingExpressionTypeResolver::new, // These should be last: IndexingValidation::new, IndexingValues::new); } + /** Processors of rank profiles only (those who tolerate and so something useful when the search field is null) */ + private Collection rankProfileProcessors() { + return Arrays.asList( + RankProfileTypeSettingsProcessor::new, + ReservedFunctionNames::new, + RankingExpressionTypeResolver::new); + } + /** * Runs all search processors on the given {@link Search} object. These will modify the search object, possibly * exchanging it with another, as well as its document types. @@ -93,12 +99,26 @@ public class Processing { public void process(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles, boolean validate, boolean documentsOnly) { Collection factories = processors(); - search.process(); factories.stream() .map(factory -> factory.create(search, deployLogger, rankProfileRegistry, queryProfiles)) .forEach(processor -> processor.process(validate, documentsOnly)); } + /** + * Runs rank profiles processors only. + * + * @param deployLogger The log to log messages and warnings for application deployment to + * @param rankProfileRegistry a {@link com.yahoo.searchdefinition.RankProfileRegistry} + * @param queryProfiles The query profiles contained in the application this search is part of. + */ + public void processRankProfiles(DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, + QueryProfiles queryProfiles, boolean validate, boolean documentsOnly) { + Collection factories = rankProfileProcessors(); + factories.stream() + .map(factory -> factory.create(null, deployLogger, rankProfileRegistry, queryProfiles)) + .forEach(processor -> processor.process(validate, documentsOnly)); + } + @FunctionalInterface public interface ProcessorFactory { Processor create(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles); diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/RankingExpressionTypeResolver.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/RankingExpressionTypeResolver.java new file mode 100644 index 00000000000..f502af922e9 --- /dev/null +++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/RankingExpressionTypeResolver.java @@ -0,0 +1,103 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.searchdefinition.processing; + +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.search.query.profile.QueryProfileRegistry; +import com.yahoo.searchdefinition.RankProfile; +import com.yahoo.searchdefinition.RankProfileRegistry; +import com.yahoo.searchdefinition.Search; +import com.yahoo.searchlib.rankingexpression.RankingExpression; +import com.yahoo.searchlib.rankingexpression.Reference; +import com.yahoo.searchlib.rankingexpression.rule.ExpressionNode; +import com.yahoo.tensor.TensorType; +import com.yahoo.tensor.evaluation.TypeContext; +import com.yahoo.vespa.model.container.search.QueryProfiles; + +import java.util.Map; + +/** + * Resolves and assigns types to all functions in a ranking expression, and + * validates the types of all ranking expressions under a search instance: + * Some operators constrain the types of inputs, and first-and second-phase expressions + * must return scalar values. + * + * In addition, the existence of all referred attribute, query and constant + * features is ensured. + * + * @author bratseth + */ +public class RankingExpressionTypeResolver extends Processor { + + private final QueryProfileRegistry queryProfiles; + + public RankingExpressionTypeResolver(Search search, + DeployLogger deployLogger, + RankProfileRegistry rankProfileRegistry, + QueryProfiles queryProfiles) { + super(search, deployLogger, rankProfileRegistry, queryProfiles); + this.queryProfiles = queryProfiles.getRegistry(); + } + + @Override + public void process(boolean validate, boolean documentsOnly) { + if (documentsOnly) return; + + for (RankProfile profile : rankProfileRegistry.rankProfilesOf(search)) { + try { + resolveTypesIn(profile, validate); + } + catch (IllegalArgumentException e) { + throw new IllegalArgumentException("In " + search + ", " + profile, e); + } + } + } + + /** + * Resolves the types of all functions in the given profile + * + * @throws IllegalArgumentException if validate is true and the given rank profile does not produce valid types + */ + private void resolveTypesIn(RankProfile profile, boolean validate) { + TypeContext context = profile.typeContext(queryProfiles); + for (Map.Entry function : profile.getFunctions().entrySet()) { + if ( ! function.getValue().function().arguments().isEmpty()) continue; + TensorType type = resolveType(function.getValue().function().getBody(), + "function '" + function.getKey() + "'", + context); + function.getValue().setType(type); + } + + if (validate) { + profile.getSummaryFeatures().forEach(f -> resolveType(f, "summary feature " + f, context)); + ensureValidDouble(profile.getFirstPhaseRanking(), "first-phase expression", context); + ensureValidDouble(profile.getSecondPhaseRanking(), "second-phase expression", context); + } + } + + private TensorType resolveType(RankingExpression expression, String expressionDescription, TypeContext context) { + if (expression == null) return null; + return resolveType(expression.getRoot(), expressionDescription, context); + } + + private TensorType resolveType(ExpressionNode expression, String expressionDescription, TypeContext context) { + TensorType type; + try { + type = expression.type(context); + } + catch (IllegalArgumentException e) { + throw new IllegalArgumentException("The " + expressionDescription + " is invalid", e); + } + if (type == null) // Not expected to happen + throw new IllegalStateException("Could not determine the type produced by " + expressionDescription); + return type; + } + + private void ensureValidDouble(RankingExpression expression, String expressionDescription, TypeContext context) { + if (expression == null) return; + TensorType type = resolveType(expression, expressionDescription, context); + if ( ! type.equals(TensorType.empty)) + throw new IllegalArgumentException("The " + expressionDescription + " must produce a double " + + "(a tensor with no dimensions), but produces " + type); + } + +} diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/RankingExpressionTypeValidator.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/RankingExpressionTypeValidator.java deleted file mode 100644 index 102d1910360..00000000000 --- a/config-model/src/main/java/com/yahoo/searchdefinition/processing/RankingExpressionTypeValidator.java +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.searchdefinition.processing; - -import com.yahoo.config.application.api.DeployLogger; -import com.yahoo.search.query.profile.QueryProfileRegistry; -import com.yahoo.searchdefinition.RankProfile; -import com.yahoo.searchdefinition.RankProfileRegistry; -import com.yahoo.searchdefinition.Search; -import com.yahoo.searchlib.rankingexpression.RankingExpression; -import com.yahoo.searchlib.rankingexpression.rule.ExpressionNode; -import com.yahoo.tensor.TensorType; -import com.yahoo.tensor.evaluation.TypeContext; -import com.yahoo.vespa.model.container.search.QueryProfiles; - -/** - * Validates the types of all ranking expressions under a search instance: - * Some operators constrain the types of inputs, and first-and second-phase expressions - * must return scalar values. In addition, the existence of all referred attribute, query and constant - * features is ensured. - * - * @author bratseth - */ -public class RankingExpressionTypeValidator extends Processor { - - private final QueryProfileRegistry queryProfiles; - - public RankingExpressionTypeValidator(Search search, - DeployLogger deployLogger, - RankProfileRegistry rankProfileRegistry, - QueryProfiles queryProfiles) { - super(search, deployLogger, rankProfileRegistry, queryProfiles); - this.queryProfiles = queryProfiles.getRegistry(); - } - - @Override - public void process(boolean validate, boolean documentsOnly) { - if ( ! validate) return; - if (documentsOnly) return; - - for (RankProfile profile : rankProfileRegistry.rankProfilesOf(search)) { - try { - validate(profile); - } - catch (IllegalArgumentException e) { - throw new IllegalArgumentException("In " + search + ", " + profile, e); - } - } - } - - /** Throws an IllegalArgumentException if the given rank profile does not produce valid type */ - private void validate(RankProfile profile) { - TypeContext context = profile.typeContext(queryProfiles); - profile.getSummaryFeatures().forEach(f -> ensureValid(f, "summary feature " + f, context)); - ensureValidDouble(profile.getFirstPhaseRanking(), "first-phase expression", context); - ensureValidDouble(profile.getSecondPhaseRanking(), "second-phase expression", context); - } - - private TensorType ensureValid(RankingExpression expression, String expressionDescription, TypeContext context) { - if (expression == null) return null; - return ensureValid(expression.getRoot(), expressionDescription, context); - } - - private TensorType ensureValid(ExpressionNode expression, String expressionDescription, TypeContext context) { - TensorType type; - try { - type = expression.type(context); - } - catch (IllegalArgumentException e) { - throw new IllegalArgumentException("The " + expressionDescription + " is invalid", e); - } - if (type == null) // Not expected to happen - throw new IllegalStateException("Could not determine the type produced by " + expressionDescription); - return type; - } - - private void ensureValidDouble(RankingExpression expression, String expressionDescription, TypeContext context) { - if (expression == null) return; - TensorType type = ensureValid(expression, expressionDescription, context); - if ( ! type.equals(TensorType.empty)) - throw new IllegalArgumentException("The " + expressionDescription + " must produce a double " + - "(a tensor with no dimensions), but produces " + type); - } - -} diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/RankProfileTypeSettingsProcessor.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/RankProfileTypeSettingsProcessor.java index ec4cbdfe58b..3bde76c1c79 100644 --- a/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/RankProfileTypeSettingsProcessor.java +++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/RankProfileTypeSettingsProcessor.java @@ -44,6 +44,7 @@ public class RankProfileTypeSettingsProcessor extends Processor { } private void processAttributeFields() { + if (search == null) return; // we're processing global profiles for (SDField field : search.allConcreteFields()) { Attribute attribute = field.getAttributes().get(field.getName()); if (attribute != null && attribute.tensorType().isPresent()) { @@ -53,6 +54,7 @@ public class RankProfileTypeSettingsProcessor extends Processor { } private void processImportedFields() { + if (search == null) return; // we're processing global profiles Optional importedFields = search.importedFields(); if (importedFields.isPresent()) { importedFields.get().fields().forEach((fieldName, field) -> processImportedField(field)); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/Service.java b/config-model/src/main/java/com/yahoo/vespa/model/Service.java index 620e44bc11a..29ec26b06d2 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/Service.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/Service.java @@ -7,7 +7,7 @@ import java.util.HashMap; import java.util.Optional; /** - * Representation of a process which runs a service + * Representation of a markProcessed which runs a service * * @author gjoranv */ diff --git a/config-model/src/main/java/com/yahoo/vespa/model/VespaModel.java b/config-model/src/main/java/com/yahoo/vespa/model/VespaModel.java index 4b70b1b5ae2..f48fcb999ed 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/VespaModel.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/VespaModel.java @@ -32,7 +32,9 @@ import com.yahoo.searchdefinition.RankProfileRegistry; import com.yahoo.searchdefinition.RankingConstants; import com.yahoo.searchdefinition.derived.AttributeFields; import com.yahoo.searchdefinition.derived.RankProfileList; +import com.yahoo.searchdefinition.processing.Processing; import com.yahoo.searchlib.rankingexpression.ExpressionFunction; +import com.yahoo.vespa.model.container.search.QueryProfiles; import com.yahoo.vespa.model.ml.ConvertedModel; import com.yahoo.searchlib.rankingexpression.RankingExpression; import com.yahoo.searchlib.rankingexpression.integration.ml.ImportedModel; @@ -168,7 +170,7 @@ public final class VespaModel extends AbstractConfigProducerRoot implements Seri createGlobalRankProfiles(deployState.getImportedModels(), deployState.rankProfileRegistry(), - deployState.getQueryProfiles().getRegistry()); + deployState.getQueryProfiles()); this.rankProfileList = new RankProfileList(null, // null search -> global rankingConstants, AttributeFields.empty, @@ -219,23 +221,22 @@ public final class VespaModel extends AbstractConfigProducerRoot implements Seri /** Adds generic application specific clusters of services */ private void addServiceClusters(ApplicationPackage app, VespaModelBuilder builder) { - for (ServiceCluster sc : builder.getClusters(app, this)) - serviceClusters.add(sc); + serviceClusters.addAll(builder.getClusters(app, this)); } /** - * Creates a rank profile not attached to any search definition, for each imported model in the application package + * Creates a rank profile not attached to any search definition, for each imported model in the application package, + * and adds it to the given rank profile registry. */ - private ImmutableList createGlobalRankProfiles(ImportedModels importedModels, - RankProfileRegistry rankProfileRegistry, - QueryProfileRegistry queryProfiles) { - List profiles = new ArrayList<>(); + private void createGlobalRankProfiles(ImportedModels importedModels, + RankProfileRegistry rankProfileRegistry, + QueryProfiles queryProfiles) { if ( ! importedModels.all().isEmpty()) { // models/ directory is available for (ImportedModel model : importedModels.all()) { RankProfile profile = new RankProfile(model.name(), this, rankProfileRegistry); rankProfileRegistry.add(profile); ConvertedModel convertedModel = ConvertedModel.fromSource(new ModelName(model.name()), - model.name(), profile, queryProfiles, model); + model.name(), profile, queryProfiles.getRegistry(), model); for (Map.Entry entry : convertedModel.expressions().entrySet()) { profile.addFunction(new ExpressionFunction(entry.getKey(), entry.getValue()), false); } @@ -253,7 +254,9 @@ public final class VespaModel extends AbstractConfigProducerRoot implements Seri } } } - return ImmutableList.copyOf(profiles); + new Processing().processRankProfiles(deployState.getDeployLogger(), + rankProfileRegistry, + queryProfiles, true, false); } /** Returns the global rank profiles as a rank profile list */ diff --git a/config-model/src/test/derived/gemini2/gemini.sd b/config-model/src/test/derived/gemini2/gemini.sd index 01e20c1b30a..8a570e58fa8 100644 --- a/config-model/src/test/derived/gemini2/gemini.sd +++ b/config-model/src/test/derived/gemini2/gemini.sd @@ -2,6 +2,12 @@ search gemini { document gemini { + field right type string { + indexing: attribute + } + field wrong type string { + indexing: attribute + } } rank-profile test { diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/RankingExpressionLoopDetectionTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/RankingExpressionLoopDetectionTestCase.java index 17bebcba70e..0ff8a5cc7ca 100644 --- a/config-model/src/test/java/com/yahoo/searchdefinition/RankingExpressionLoopDetectionTestCase.java +++ b/config-model/src/test/java/com/yahoo/searchdefinition/RankingExpressionLoopDetectionTestCase.java @@ -40,7 +40,7 @@ public class RankingExpressionLoopDetectionTestCase { fail("Excepted exception"); } catch (IllegalArgumentException e) { - assertEquals("In search definition 'test', rank profile 'test': The first-phase expression is invalid: Invocation loop: foo -> foo", + assertEquals("In search definition 'test', rank profile 'test': The function 'foo' is invalid: Invocation loop: foo -> foo", Exceptions.toMessageString(e)); } } @@ -75,7 +75,7 @@ public class RankingExpressionLoopDetectionTestCase { fail("Excepted exception"); } catch (IllegalArgumentException e) { - assertEquals("In search definition 'test', rank profile 'test': The first-phase expression is invalid: Invocation loop: foo -> arg(5) -> foo", + assertEquals("In search definition 'test', rank profile 'test': The function 'foo' is invalid: Invocation loop: arg(5) -> foo -> arg(5)", Exceptions.toMessageString(e)); } } @@ -110,7 +110,7 @@ public class RankingExpressionLoopDetectionTestCase { fail("Excepted exception"); } catch (IllegalArgumentException e) { - assertEquals("In search definition 'test', rank profile 'test': The first-phase expression is invalid: Invocation loop: foo -> arg(foo) -> foo", + assertEquals("In search definition 'test', rank profile 'test': The function 'foo' is invalid: Invocation loop: arg(foo) -> foo -> arg(foo)", Exceptions.toMessageString(e)); } } diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/IntegerIndex2AttributeTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/IntegerIndex2AttributeTestCase.java index 29bba224f46..31631b0dc74 100644 --- a/config-model/src/test/java/com/yahoo/searchdefinition/processing/IntegerIndex2AttributeTestCase.java +++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/IntegerIndex2AttributeTestCase.java @@ -23,7 +23,6 @@ public class IntegerIndex2AttributeTestCase extends SearchDefinitionTestCase { @Test public void testIntegerIndex2Attribute() throws IOException, ParseException { Search search = UnprocessingSearchBuilder.buildUnprocessedFromFile("src/test/examples/integerindex2attribute.sd"); - search.process(); new IntegerIndex2Attribute(search, new BaseDeployLogger(), new RankProfileRegistry(), new QueryProfiles()).process(true, false); SDField f; diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/RankingExpressionTypeResolverTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/RankingExpressionTypeResolverTestCase.java new file mode 100644 index 00000000000..1b917b6f3a3 --- /dev/null +++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/RankingExpressionTypeResolverTestCase.java @@ -0,0 +1,238 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.searchdefinition.processing; + +import com.yahoo.searchdefinition.RankProfile; +import com.yahoo.searchdefinition.RankProfileRegistry; +import com.yahoo.searchdefinition.SearchBuilder; +import com.yahoo.searchlib.rankingexpression.rule.ReferenceNode; +import com.yahoo.tensor.TensorType; +import com.yahoo.yolean.Exceptions; +import org.junit.Test; + +import java.util.Map; +import java.util.stream.Collectors; + +import static com.yahoo.config.model.test.TestUtil.joinLines; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author bratseth + */ +public class RankingExpressionTypeResolverTestCase { + + @Test + public void tensorFirstPhaseMustProduceDouble() throws Exception { + try { + SearchBuilder builder = new SearchBuilder(); + builder.importString(joinLines( + "search test {", + " document test { ", + " field a type tensor(x[],y[]) {", + " indexing: attribute", + " }", + " }", + " rank-profile my_rank_profile {", + " first-phase {", + " expression: attribute(a)", + " }", + " }", + "}" + )); + builder.build(); + fail("Expected exception"); + } + catch (IllegalArgumentException expected) { + assertEquals("In search definition 'test', rank profile 'my_rank_profile': The first-phase expression must produce a double (a tensor with no dimensions), but produces tensor(x[],y[])", + Exceptions.toMessageString(expected)); + } + } + + @Test + public void tensorSecondPhaseMustProduceDouble() throws Exception { + try { + SearchBuilder builder = new SearchBuilder(); + builder.importString(joinLines( + "search test {", + " document test { ", + " field a type tensor(x[],y[]) {", + " indexing: attribute", + " }", + " }", + " rank-profile my_rank_profile {", + " first-phase {", + " expression: sum(attribute(a))", + " }", + " second-phase {", + " expression: attribute(a)", + " }", + " }", + "}" + )); + builder.build(); + fail("Expected exception"); + } + catch (IllegalArgumentException expected) { + assertEquals("In search definition 'test', rank profile 'my_rank_profile': The second-phase expression must produce a double (a tensor with no dimensions), but produces tensor(x[],y[])", + Exceptions.toMessageString(expected)); + } + } + + @Test + public void tensorConditionsMustHaveTypeCompatibleBranches() throws Exception { + try { + SearchBuilder searchBuilder = new SearchBuilder(); + searchBuilder.importString(joinLines( + "search test {", + " document test { ", + " field a type tensor(x[],y[]) {", + " indexing: attribute", + " }", + " field b type tensor(z[10]) {", + " indexing: attribute", + " }", + " }", + " rank-profile my_rank_profile {", + " first-phase {", + " expression: sum(if(1>0, attribute(a), attribute(b)))", + " }", + " }", + "}" + )); + searchBuilder.build(); + fail("Expected exception"); + } + catch (IllegalArgumentException expected) { + assertEquals("In search definition 'test', rank profile 'my_rank_profile': The first-phase expression is invalid: An if expression must produce compatible types in both alternatives, but the 'true' type is tensor(x[],y[]) while the 'false' type is tensor(z[10])", + Exceptions.toMessageString(expected)); + } + } + + @Test + public void testFunctionInvocationTypes() throws Exception { + RankProfileRegistry rankProfileRegistry = new RankProfileRegistry(); + SearchBuilder builder = new SearchBuilder(rankProfileRegistry); + builder.importString(joinLines( + "search test {", + " document test { ", + " field a type tensor(x[],y[]) {", + " indexing: attribute", + " }", + " field b type tensor(z[10]) {", + " indexing: attribute", + " }", + " }", + " rank-profile my_rank_profile {", + " function macro1(attribute_to_use) {", + " expression: attribute(attribute_to_use)", + " }", + " summary-features {", + " macro1(a)", + " macro1(b)", + " }", + " }", + "}" + )); + builder.build(); + RankProfile profile = + builder.getRankProfileRegistry().get(builder.getSearch(), "my_rank_profile"); + assertEquals(TensorType.fromSpec("tensor(x[],y[])"), + summaryFeatures(profile).get("macro1(a)").type(profile.typeContext(builder.getQueryProfileRegistry()))); + assertEquals(TensorType.fromSpec("tensor(z[10])"), + summaryFeatures(profile).get("macro1(b)").type(profile.typeContext(builder.getQueryProfileRegistry()))); + } + + @Test + public void testTensorFunctionInvocationTypes_Nested() throws Exception { + SearchBuilder builder = new SearchBuilder(); + builder.importString(joinLines( + "search test {", + " document test { ", + " field a type tensor(x[],y[]) {", + " indexing: attribute", + " }", + " field b type tensor(z[10]) {", + " indexing: attribute", + " }", + " }", + " rank-profile my_rank_profile {", + " function return_a() {", + " expression: return_first(attribute(a), attribute(b))", + " }", + " function return_b() {", + " expression: return_second(attribute(a), attribute(b))", + " }", + " function return_first(e1, e2) {", + " expression: e1", + " }", + " function return_second(e1, e2) {", + " expression: return_first(e2, e1)", + " }", + " summary-features {", + " return_a", + " return_b", + " }", + " }", + "}" + )); + builder.build(); + RankProfile profile = + builder.getRankProfileRegistry().get(builder.getSearch(), "my_rank_profile"); + assertEquals(TensorType.fromSpec("tensor(x[],y[])"), + summaryFeatures(profile).get("return_a").type(profile.typeContext(builder.getQueryProfileRegistry()))); + assertEquals(TensorType.fromSpec("tensor(z[10])"), + summaryFeatures(profile).get("return_b").type(profile.typeContext(builder.getQueryProfileRegistry()))); + } + + @Test + public void importedFieldsAreAvailable() throws Exception { + SearchBuilder builder = new SearchBuilder(); + builder.importString(joinLines( + "search parent {", + " document parent {", + " field a type tensor(x[],y[]) {", + " indexing: attribute", + " }", + " }", + "}" + )); + builder.importString(joinLines( + "search child {", + " document child { ", + " field ref type reference {", + "indexing: attribute | summary", + " }", + " }", + " import field ref.a as imported_a {}", + " rank-profile my_rank_profile {", + " first-phase {", + " expression: sum(attribute(imported_a))", + " }", + " }", + "}" + )); + builder.build(); + } + + @Test + public void undeclaredQueryFeaturesAreAccepted() throws Exception { + SearchBuilder builder = new SearchBuilder(); + builder.importString(joinLines( + "search test {", + " document test { ", + " }", + " rank-profile my_rank_profile {", + " first-phase {", + " expression: query(foo)", + " }", + " }", + "}" + )); + builder.build(); + } + + private Map summaryFeatures(RankProfile profile) { + return profile.getSummaryFeatures().stream().collect(Collectors.toMap(f -> f.toString(), f -> f)); + } + +} diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/RankingExpressionTypeValidatorTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/RankingExpressionTypeValidatorTestCase.java deleted file mode 100644 index 0d8cbbf2e6a..00000000000 --- a/config-model/src/test/java/com/yahoo/searchdefinition/processing/RankingExpressionTypeValidatorTestCase.java +++ /dev/null @@ -1,238 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.searchdefinition.processing; - -import com.yahoo.searchdefinition.RankProfile; -import com.yahoo.searchdefinition.RankProfileRegistry; -import com.yahoo.searchdefinition.SearchBuilder; -import com.yahoo.searchlib.rankingexpression.rule.ReferenceNode; -import com.yahoo.tensor.TensorType; -import com.yahoo.yolean.Exceptions; -import org.junit.Test; - -import java.util.Map; -import java.util.stream.Collectors; - -import static com.yahoo.config.model.test.TestUtil.joinLines; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -/** - * @author bratseth - */ -public class RankingExpressionTypeValidatorTestCase { - - @Test - public void tensorFirstPhaseMustProduceDouble() throws Exception { - try { - SearchBuilder builder = new SearchBuilder(); - builder.importString(joinLines( - "search test {", - " document test { ", - " field a type tensor(x[],y[]) {", - " indexing: attribute", - " }", - " }", - " rank-profile my_rank_profile {", - " first-phase {", - " expression: attribute(a)", - " }", - " }", - "}" - )); - builder.build(); - fail("Expected exception"); - } - catch (IllegalArgumentException expected) { - assertEquals("In search definition 'test', rank profile 'my_rank_profile': The first-phase expression must produce a double (a tensor with no dimensions), but produces tensor(x[],y[])", - Exceptions.toMessageString(expected)); - } - } - - @Test - public void tensorSecondPhaseMustProduceDouble() throws Exception { - try { - SearchBuilder builder = new SearchBuilder(); - builder.importString(joinLines( - "search test {", - " document test { ", - " field a type tensor(x[],y[]) {", - " indexing: attribute", - " }", - " }", - " rank-profile my_rank_profile {", - " first-phase {", - " expression: sum(attribute(a))", - " }", - " second-phase {", - " expression: attribute(a)", - " }", - " }", - "}" - )); - builder.build(); - fail("Expected exception"); - } - catch (IllegalArgumentException expected) { - assertEquals("In search definition 'test', rank profile 'my_rank_profile': The second-phase expression must produce a double (a tensor with no dimensions), but produces tensor(x[],y[])", - Exceptions.toMessageString(expected)); - } - } - - @Test - public void tensorConditionsMustHaveTypeCompatibleBranches() throws Exception { - try { - SearchBuilder searchBuilder = new SearchBuilder(); - searchBuilder.importString(joinLines( - "search test {", - " document test { ", - " field a type tensor(x[],y[]) {", - " indexing: attribute", - " }", - " field b type tensor(z[10]) {", - " indexing: attribute", - " }", - " }", - " rank-profile my_rank_profile {", - " first-phase {", - " expression: sum(if(1>0, attribute(a), attribute(b)))", - " }", - " }", - "}" - )); - searchBuilder.build(); - fail("Expected exception"); - } - catch (IllegalArgumentException expected) { - assertEquals("In search definition 'test', rank profile 'my_rank_profile': The first-phase expression is invalid: An if expression must produce compatible types in both alternatives, but the 'true' type is tensor(x[],y[]) while the 'false' type is tensor(z[10])", - Exceptions.toMessageString(expected)); - } - } - - @Test - public void testFunctionInvocationTypes() throws Exception { - RankProfileRegistry rankProfileRegistry = new RankProfileRegistry(); - SearchBuilder builder = new SearchBuilder(rankProfileRegistry); - builder.importString(joinLines( - "search test {", - " document test { ", - " field a type tensor(x[],y[]) {", - " indexing: attribute", - " }", - " field b type tensor(z[10]) {", - " indexing: attribute", - " }", - " }", - " rank-profile my_rank_profile {", - " function macro1(attribute_to_use) {", - " expression: attribute(attribute_to_use)", - " }", - " summary-features {", - " macro1(a)", - " macro1(b)", - " }", - " }", - "}" - )); - builder.build(); - RankProfile profile = - builder.getRankProfileRegistry().get(builder.getSearch(), "my_rank_profile"); - assertEquals(TensorType.fromSpec("tensor(x[],y[])"), - summaryFeatures(profile).get("macro1(a)").type(profile.typeContext(builder.getQueryProfileRegistry()))); - assertEquals(TensorType.fromSpec("tensor(z[10])"), - summaryFeatures(profile).get("macro1(b)").type(profile.typeContext(builder.getQueryProfileRegistry()))); - } - - @Test - public void testTensorFunctionInvocationTypes_Nested() throws Exception { - SearchBuilder builder = new SearchBuilder(); - builder.importString(joinLines( - "search test {", - " document test { ", - " field a type tensor(x[],y[]) {", - " indexing: attribute", - " }", - " field b type tensor(z[10]) {", - " indexing: attribute", - " }", - " }", - " rank-profile my_rank_profile {", - " function return_a() {", - " expression: return_first(attribute(a), attribute(b))", - " }", - " function return_b() {", - " expression: return_second(attribute(a), attribute(b))", - " }", - " function return_first(e1, e2) {", - " expression: e1", - " }", - " function return_second(e1, e2) {", - " expression: return_first(e2, e1)", - " }", - " summary-features {", - " return_a", - " return_b", - " }", - " }", - "}" - )); - builder.build(); - RankProfile profile = - builder.getRankProfileRegistry().get(builder.getSearch(), "my_rank_profile"); - assertEquals(TensorType.fromSpec("tensor(x[],y[])"), - summaryFeatures(profile).get("return_a").type(profile.typeContext(builder.getQueryProfileRegistry()))); - assertEquals(TensorType.fromSpec("tensor(z[10])"), - summaryFeatures(profile).get("return_b").type(profile.typeContext(builder.getQueryProfileRegistry()))); - } - - @Test - public void importedFieldsAreAvailable() throws Exception { - SearchBuilder builder = new SearchBuilder(); - builder.importString(joinLines( - "search parent {", - " document parent {", - " field a type tensor(x[],y[]) {", - " indexing: attribute", - " }", - " }", - "}" - )); - builder.importString(joinLines( - "search child {", - " document child { ", - " field ref type reference {", - "indexing: attribute | summary", - " }", - " }", - " import field ref.a as imported_a {}", - " rank-profile my_rank_profile {", - " first-phase {", - " expression: sum(attribute(imported_a))", - " }", - " }", - "}" - )); - builder.build(); - } - - @Test - public void undeclaredQueryFeaturesAreAccepted() throws Exception { - SearchBuilder builder = new SearchBuilder(); - builder.importString(joinLines( - "search test {", - " document test { ", - " }", - " rank-profile my_rank_profile {", - " first-phase {", - " expression: query(foo)", - " }", - " }", - "}" - )); - builder.build(); - } - - private Map summaryFeatures(RankProfile profile) { - return profile.getSummaryFeatures().stream().collect(Collectors.toMap(f -> f.toString(), f -> f)); - } - -} diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/SummaryFieldsMustHaveValidSourceTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/SummaryFieldsMustHaveValidSourceTestCase.java index d0c1bf8b0ca..39ab7195892 100644 --- a/config-model/src/test/java/com/yahoo/searchdefinition/processing/SummaryFieldsMustHaveValidSourceTestCase.java +++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/SummaryFieldsMustHaveValidSourceTestCase.java @@ -11,7 +11,6 @@ import org.junit.Test; import java.io.IOException; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class SummaryFieldsMustHaveValidSourceTestCase extends SearchDefinitionTestCase { @@ -19,7 +18,6 @@ public class SummaryFieldsMustHaveValidSourceTestCase extends SearchDefinitionTe @Test public void requireThatInvalidSourceIsCaught() throws IOException, ParseException { Search search = UnprocessingSearchBuilder.buildUnprocessedFromFile("src/test/examples/invalidsummarysource.sd"); - search.process(); try { new SummaryFieldsMustHaveValidSource(search, new BaseDeployLogger(), new RankProfileRegistry(), new QueryProfiles()).process(true, false); fail("This should throw and never get here"); @@ -31,7 +29,6 @@ public class SummaryFieldsMustHaveValidSourceTestCase extends SearchDefinitionTe @Test public void requireThatInvalidImplicitSourceIsCaught() throws IOException, ParseException { Search search = UnprocessingSearchBuilder.buildUnprocessedFromFile("src/test/examples/invalidimplicitsummarysource.sd"); - search.process(); try { new SummaryFieldsMustHaveValidSource(search, new BaseDeployLogger(), new RankProfileRegistry(), new QueryProfiles()).process(true, false); fail("This should throw and never get here"); @@ -43,7 +40,6 @@ public class SummaryFieldsMustHaveValidSourceTestCase extends SearchDefinitionTe @Test public void requireThatInvalidSelfReferingSingleSource() throws IOException, ParseException { Search search = UnprocessingSearchBuilder.buildUnprocessedFromFile("src/test/examples/invalidselfreferringsummary.sd"); - search.process(); try { new SummaryFieldsMustHaveValidSource(search, new BaseDeployLogger(), new RankProfileRegistry(), new QueryProfiles()).process(true, false); fail("This should throw and never get here"); @@ -55,7 +51,6 @@ public class SummaryFieldsMustHaveValidSourceTestCase extends SearchDefinitionTe @Test public void requireThatDocumentIdIsAllowedToPass() throws IOException, ParseException { Search search = UnprocessingSearchBuilder.buildUnprocessedFromFile("src/test/examples/documentidinsummary.sd"); - search.process(); BaseDeployLogger deployLogger = new BaseDeployLogger(); RankProfileRegistry rankProfileRegistry = new RankProfileRegistry(); new SummaryFieldsMustHaveValidSource(search, deployLogger, rankProfileRegistry, new QueryProfiles()).process(true, false); diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/TensorTransformTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/TensorTransformTestCase.java index 8e721dbe503..6e3a227e2a9 100644 --- a/config-model/src/test/java/com/yahoo/searchdefinition/processing/TensorTransformTestCase.java +++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/TensorTransformTestCase.java @@ -58,8 +58,8 @@ public class TensorTransformTestCase extends SearchDefinitionTestCase { "max(attribute(tensor_field_1),x)"); assertTransformedExpression("1+reduce(attribute(tensor_field_1),max,x)", "1 + max(attribute(tensor_field_1),x)"); - assertTransformedExpression("if(attribute(double_field),1+reduce(attribute(tensor_field_1),max,x),0)", - "if(attribute(double_field),1 + max(attribute(tensor_field_1),x),0)"); + assertTransformedExpression("if(attribute(double_field),1+reduce(attribute(tensor_field_1),max,x),attribute(tensor_field_1))", + "if(attribute(double_field),1 + max(attribute(tensor_field_1),x),attribute(tensor_field_1))"); assertTransformedExpression("reduce(max(attribute(tensor_field_1),attribute(tensor_field_2)),max,x)", "max(max(attribute(tensor_field_1),attribute(tensor_field_2)),x)"); assertTransformedExpression("reduce(if(attribute(double_field),attribute(tensor_field_2),attribute(tensor_field_2)),max,x)", -- cgit v1.2.3