diff --git a/fdb-java-annotations/src/main/java/com/apple/foundationdb/annotation/GenerateVisitorAnnotationHelper.java b/fdb-java-annotations/src/main/java/com/apple/foundationdb/annotation/GenerateVisitorAnnotationHelper.java index 956fbfce87..93171a7776 100644 --- a/fdb-java-annotations/src/main/java/com/apple/foundationdb/annotation/GenerateVisitorAnnotationHelper.java +++ b/fdb-java-annotations/src/main/java/com/apple/foundationdb/annotation/GenerateVisitorAnnotationHelper.java @@ -20,6 +20,7 @@ package com.apple.foundationdb.annotation; +import com.google.common.annotations.VisibleForTesting; import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; @@ -43,11 +44,13 @@ import javax.lang.model.element.Modifier; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; import javax.tools.Diagnostic; import java.io.IOException; +import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Map; @@ -55,6 +58,7 @@ import java.util.Set; import java.util.function.BiFunction; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * A separate class to support (@link GenerateVisitorAnnotationProcessor) so that dependency on javapoet does not leak to anyone @@ -102,8 +106,8 @@ static boolean process(final ProcessingEnvironment processingEnv, Set packageElement.getEnclosedElements().stream()) - .filter(element -> element.getKind() == ElementKind.CLASS && - !element.getModifiers().contains(Modifier.ABSTRACT)) + .flatMap(element -> element.getKind() == ElementKind.CLASS && element.getModifiers().contains(Modifier.ABSTRACT) ? element.getEnclosedElements().stream() : Stream.of(element) ) + .filter(element -> element.getKind() == ElementKind.CLASS && !element.getModifiers().contains(Modifier.ABSTRACT)) .map(Element::asType) .filter(mirror -> mirror.getKind() == TypeKind.DECLARED) .filter(mirror -> typeUtils.isSubtype(mirror, rootTypeMirror)) @@ -152,10 +156,11 @@ private static void generateInterface(@Nonnull final Types typeUtils, .addModifiers(Modifier.PUBLIC) .addTypeVariable(typeVariableName); + final var packageName = packageElement.getQualifiedName().toString(); final var jumpMapBuilder = FieldSpec.builder(ParameterizedTypeName.get(ClassName.get(Map.class), ParameterizedTypeName.get(ClassName.get(Class.class), WildcardTypeName.subtypeOf(Object.class)), ParameterizedTypeName.get(ClassName.get(BiFunction.class), - ParameterizedTypeName.get(ClassName.get(packageElement.getQualifiedName().toString(), interfaceName), WildcardTypeName.subtypeOf(Object.class)), + ParameterizedTypeName.get(ClassName.get(packageName, interfaceName), WildcardTypeName.subtypeOf(Object.class)), TypeName.get(rootTypeMirror), WildcardTypeName.subtypeOf(Object.class))), "jumpMap", Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL); @@ -163,7 +168,7 @@ private static void generateInterface(@Nonnull final Types typeUtils, final var initializerStrings = subClassTypeMirrors.stream() .map(typeMirror -> { final var typeElement = (TypeElement)typeUtils.asElement(typeMirror); - return "Map.entry(" + typeElement.getSimpleName() + ".class, (visitor, element) -> visitor." + methodNameOfVisitMethod(generateVisitor, typeElement) + "((" + typeElement.getSimpleName() + ")element))"; + return "Map.entry(" + getRawTypeName(typeMirror, packageName) + ".class, (visitor, element) -> visitor." + methodNameOfVisitMethod(generateVisitor, typeElement) + "((" + getWildcardTypeName(typeMirror, packageName) + ")element))"; }) .collect(Collectors.joining(", \n")); @@ -172,6 +177,7 @@ private static void generateInterface(@Nonnull final Types typeUtils, .build(); typeBuilder.addField(jumpMapBuilder + .addAnnotation(AnnotationSpec.builder(SuppressWarnings.class).addMember("value", "$S", "unchecked").build()) .initializer(initializerBlock) .build()); @@ -183,7 +189,7 @@ private static void generateInterface(@Nonnull final Types typeUtils, .methodBuilder(methodName) .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) .addAnnotation(Nonnull.class) - .addParameter(ParameterSpec.builder(TypeName.get(typeMirror), parameterName).addAnnotation(Nonnull.class).build()) + .addParameter(ParameterSpec.builder(getWildcardTypeName(typeMirror, packageName), parameterName).addAnnotation(Nonnull.class).build()) .returns(typeVariableName); typeBuilder.addMethod(specificVisitMethodBuilder.build()); } @@ -193,7 +199,7 @@ private static void generateInterface(@Nonnull final Types typeUtils, .methodBuilder(defaultMethodName) .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) .addAnnotation(Nonnull.class) - .addParameter(ParameterSpec.builder(TypeName.get(rootTypeMirror), parameterName).addAnnotation(Nonnull.class).build()) + .addParameter(ParameterSpec.builder(getWildcardTypeName(rootTypeMirror, packageName), parameterName).addAnnotation(Nonnull.class).build()) .returns(typeVariableName); typeBuilder.addMethod(visitDefaultMethodBuilder.build()); @@ -216,6 +222,105 @@ private static void generateInterface(@Nonnull final Types typeUtils, .writeTo(Objects.requireNonNull(filer)); } + /** + * Converts a type mirror to a raw TypeName without type parameters. + *

+ * For generic types, this method returns the raw type without any type arguments + * (e.g., {@code List} becomes {@code List}). For non-generic types, the type + * is returned as-is. If the type belongs to the same package as {@code currentPackage}, + * the package prefix is omitted from the generated type name. + *

+ * This is particularly useful when generating code that needs to reference the + * {@code .class} literal of a generic type, as class literals must use raw types. + * + * @param typeMirror the type mirror to convert + * @param currentPackage the current package name, used to determine whether to omit + * package prefixes for types in the same package + * @return a TypeName representing the raw type (without type parameters) if the type + * is generic, or the original type name if not generic + */ + @Nonnull + private static TypeName getRawTypeName(@Nonnull TypeMirror typeMirror, @Nonnull String currentPackage) { + if (typeMirror.getKind() == TypeKind.DECLARED) { + final var declaredType = (DeclaredType) typeMirror; + final var typeElement = (TypeElement) declaredType.asElement(); + final boolean isGeneric = !typeElement.getTypeParameters().isEmpty(); + + if (isGeneric) { + final ClassName className = ClassName.get(typeElement); + return removePackagePrefix(className, currentPackage); + } + } + + // return as-is, remove the package if it is the same as the currentPackage. + final TypeName typeName = TypeName.get(typeMirror); + if (typeName instanceof ClassName) { + return removePackagePrefix((ClassName) typeName, currentPackage); + } + + return typeName; + } + + /** + * Converts a type mirror to a TypeName with wildcard type arguments for generic types. + *

+ * For generic types, this method creates a parameterized type with wildcard bounds + * (e.g., {@code List} becomes {@code List}). For non-generic types, the type + * is returned as-is. If the type belongs to the same package as {@code currentPackage}, + * the package prefix is omitted from the generated type name. + * + * @param typeMirror the type mirror to convert + * @param currentPackage the current package name, used to determine whether to omit + * package prefixes for types in the same package + * @return a TypeName representing the type with wildcard type arguments if the type + * is generic, or the original type name if not generic + */ + @Nonnull + private static TypeName getWildcardTypeName(@Nonnull final TypeMirror typeMirror, @Nonnull final String currentPackage) { + if (typeMirror.getKind() == TypeKind.DECLARED) { + final var declaredType = (DeclaredType) typeMirror; + final var typeElement = (TypeElement) declaredType.asElement(); + final boolean isGeneric = !typeElement.getTypeParameters().isEmpty(); + + if (isGeneric) { + ClassName rawType = ClassName.get(typeElement); + rawType = removePackagePrefix(rawType, currentPackage); + + final WildcardTypeName[] wildcards = new WildcardTypeName[typeElement.getTypeParameters().size()]; + Arrays.fill(wildcards, WildcardTypeName.subtypeOf(Object.class)); + return ParameterizedTypeName.get(rawType, wildcards); + } + } + + // return as-is, remove the package if it is the same as the currentPackage. + final TypeName typeName = TypeName.get(typeMirror); + if (typeName instanceof ClassName) { + return removePackagePrefix((ClassName) typeName, currentPackage); + } + + return typeName; + } + + /** + * Removes the package prefix from a ClassName if it belongs to the same package as currentPackage. + *

+ * This is useful when generating code references to types that are in the same package, + * as the package prefix can be omitted for brevity. + * + * @param className the ClassName to potentially strip the package prefix from + * @param currentPackage the current package name to compare against + * @return a ClassName without the package prefix if it's in the same package, + * otherwise returns the original ClassName unchanged + */ + @Nonnull + private static ClassName removePackagePrefix(@Nonnull final ClassName className, @Nonnull final String currentPackage) { + if (className.packageName().equals(currentPackage)) { + return ClassName.get("", className.topLevelClassName().simpleName(), + className.simpleNames().subList(1, className.simpleNames().size()).toArray(new String[0])); + } + return className; + } + private static void generateImplementationWithDefaults(@Nonnull final Types typeUtils, @Nonnull final Filer filer, @Nonnull final GenerateVisitor generateVisitor, @@ -240,7 +345,7 @@ private static void generateImplementationWithDefaults(@Nonnull final Types type .addModifiers(Modifier.PUBLIC, Modifier.DEFAULT) .addAnnotation(Nonnull.class) .addAnnotation(Override.class) - .addParameter(ParameterSpec.builder(TypeName.get(typeMirror), parameterName).addAnnotation(Nonnull.class).build()) + .addParameter(ParameterSpec.builder(getWildcardTypeName(typeMirror, packageElement.getQualifiedName().toString()), parameterName).addAnnotation(Nonnull.class).build()) .returns(typeVariableName) .addCode(CodeBlock.builder() .addStatement("return " + defaultMethodName + "(" + parameterName + ")") diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/metadata/IndexTypes.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/metadata/IndexTypes.java index 1d19171093..8d10f26d9e 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/metadata/IndexTypes.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/metadata/IndexTypes.java @@ -164,6 +164,11 @@ public class IndexTypes { */ public static final String MULTIDIMENSIONAL = "multidimensional"; + /** + * An index using an HNSW structure. + */ + public static final String VECTOR = "vector"; + private IndexTypes() { } } diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/IndexKeyValueToPartialRecord.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/IndexKeyValueToPartialRecord.java index c5a8ddee35..edc50f8830 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/IndexKeyValueToPartialRecord.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/IndexKeyValueToPartialRecord.java @@ -463,6 +463,9 @@ public boolean copy(@Nonnull Descriptors.Descriptor recordDescriptor, @Nonnull M return !fieldDescriptor.isRequired(); } switch (fieldDescriptor.getType()) { + case INT32: + value = ((Number)value).intValue(); + break; case MESSAGE: value = TupleFieldsHelper.toProto(value, fieldDescriptor.getMessageType()); break; diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/Value.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/Value.java index 0b5da02fef..14126baf64 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/Value.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/Value.java @@ -21,6 +21,7 @@ package com.apple.foundationdb.record.query.plan.cascades.values; import com.apple.foundationdb.annotation.API; +import com.apple.foundationdb.annotation.GenerateVisitor; import com.apple.foundationdb.record.EvaluationContext; import com.apple.foundationdb.record.PlanHashable; import com.apple.foundationdb.record.PlanSerializable; @@ -91,6 +92,7 @@ * A scalar value type. */ @API(API.Status.EXPERIMENTAL) +@GenerateVisitor public interface Value extends Correlated, TreeLike, UsesValueEquivalence, PlanHashable, Typed, Narrowable, PlanSerializable { @Nonnull diff --git a/fdb-relational-core/src/jmh/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalBenchmark.java b/fdb-relational-core/src/jmh/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalBenchmark.java index 706f0ccc1b..367205cbdf 100644 --- a/fdb-relational-core/src/jmh/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalBenchmark.java +++ b/fdb-relational-core/src/jmh/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalBenchmark.java @@ -63,8 +63,8 @@ public abstract class EmbeddedRelationalBenchmark { "CREATE TABLE \"RestaurantRecord\" (\"rest_no\" bigint, \"name\" string, \"location\" \"Location\", \"reviews\" \"RestaurantReview\" ARRAY, \"tags\" \"RestaurantTag\" ARRAY, \"customer\" string ARRAY, PRIMARY KEY(\"rest_no\")) " + "CREATE TABLE \"RestaurantReviewer\" (\"id\" bigint, \"name\" string, \"email\" string, \"stats\" \"ReviewerStats\", PRIMARY KEY(\"id\")) " + - "CREATE INDEX \"record_name_idx\" as select \"name\" from \"RestaurantRecord\" " + - "CREATE INDEX \"reviewer_name_idx\" as select \"name\" from \"RestaurantReviewer\" "; + "CREATE INDEX \"record_name_idx\" ON \"RestaurantRecord\"(\"name\") " + + "CREATE INDEX \"reviewer_name_idx\" ON \"RestaurantReviewer\"(\"name\") "; static final String restaurantRecordTable = "RestaurantRecord"; diff --git a/fdb-relational-core/src/main/antlr/RelationalLexer.g4 b/fdb-relational-core/src/main/antlr/RelationalLexer.g4 index 77a34e26f3..bcbbc340c7 100644 --- a/fdb-relational-core/src/main/antlr/RelationalLexer.g4 +++ b/fdb-relational-core/src/main/antlr/RelationalLexer.g4 @@ -999,6 +999,15 @@ GREATEST: 'GREATEST'; GTID_SUBSET: 'GTID_SUBSET'; GTID_SUBTRACT: 'GTID_SUBTRACT'; HEX: 'HEX'; +HNSW_EF_CONSTRUCTION: 'HNSW_EF_CONSTRUCTION'; +HNSW_M_MAX: 'HNSW_M_MAX'; +HNSW_M: 'HNSW_M'; +HNSW_MAINTAIN_STATS_PROBABILITY: 'HNSW_MAINTAIN_STATS_PROBABILITY'; +HNSW_METRIC: 'HNSW_METRIC'; +HNSW_RABITQ_NUM_EX_BITS: 'HNSW_RABITQ_NUM_EX_BITS'; +HNSW_SAMPLE_VECTOR_STATS_PROBABILITY:'HNSW_SAMPLE_VECTOR_STATS_PROBABILITY'; +HNSW_STATS_THRESHOLD: 'HNSW_STATS_THRESHOLD'; +HNSW_USE_RABITQ: 'HNSW_USE_RABITQ'; IFNULL: 'IFNULL'; INET6_ATON: 'INET6_ATON'; INET6_NTOA: 'INET6_NTOA'; diff --git a/fdb-relational-core/src/main/antlr/RelationalParser.g4 b/fdb-relational-core/src/main/antlr/RelationalParser.g4 index 3bbbd614b5..2b84c79e4a 100644 --- a/fdb-relational-core/src/main/antlr/RelationalParser.g4 +++ b/fdb-relational-core/src/main/antlr/RelationalParser.g4 @@ -168,7 +168,49 @@ enumDefinition ; indexDefinition - : (UNIQUE)? INDEX indexName=uid AS queryTerm indexAttributes? + : (UNIQUE)? INDEX indexName=uid AS queryTerm indexAttributes? #indexAsSelectDefinition + | (UNIQUE)? INDEX indexName=uid ON source=fullId indexColumnList includeClause? indexOptions? #indexOnSourceDefinition + | VECTOR INDEX indexName=uid ON source=fullId indexColumnList partitionClause? vectorIndexOptions? #vectorIndexDefinition + ; + +indexColumnList + : '(' indexColumnSpec (',' indexColumnSpec)* ')' + ; + +indexColumnSpec + : columnName=uid orderClause? + ; + +includeClause + : INCLUDE '(' uidList ')' + ; + +indexType + : UNIQUE | VECTOR + ; + +indexOptions + : OPTIONS '(' indexOption (COMMA indexOption)* ')' + ; + +indexOption + : LEGACY_EXTREMUM_EVER + ; + +vectorIndexOptions + : OPTIONS '(' vectorIndexOption (COMMA vectorIndexOptions)* ')' + ; + +vectorIndexOption + : HNSW_EF_CONSTRUCTION '=' mValue=DECIMAL_LITERAL + | HNSW_M '=' mValue=DECIMAL_LITERAL + | HNSW_M_MAX '=' mValue=DECIMAL_LITERAL + | HNSW_MAINTAIN_STATS_PROBABILITY '=' mValue=DECIMAL_LITERAL + | HNSW_METRIC '=' mValue=DECIMAL_LITERAL // change + | HNSW_RABITQ_NUM_EX_BITS '=' mValue=DECIMAL_LITERAL + | HNSW_SAMPLE_VECTOR_STATS_PROBABILITY '=' mValue=DECIMAL_LITERAL + | HNSW_STATS_THRESHOLD '=' mValue=DECIMAL_LITERAL + | HNSW_USE_RABITQ '=' mValue=DECIMAL_LITERAL // change ; indexAttributes @@ -413,7 +455,12 @@ orderByClause ; orderByExpression - : expression order=(ASC | DESC)? (NULLS nulls=(FIRST | LAST))? + : expression orderClause? + ; + +orderClause + : order=(ASC | DESC) (NULLS nulls=(FIRST | LAST))? + | NULLS nulls=(FIRST | LAST) ; tableSources // done @@ -1104,10 +1151,11 @@ frameRange | expression (PRECEDING | FOLLOWING) ; +*/ + partitionClause - : PARTITION BY expression (',' expression)* + : PARTITION BY uid (',' uid)* ; -*/ scalarFunctionName : functionNameBase diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/AbstractEmbeddedStatement.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/AbstractEmbeddedStatement.java index 140a86496f..bb203cbcb2 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/AbstractEmbeddedStatement.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/AbstractEmbeddedStatement.java @@ -215,10 +215,15 @@ private int countUpdates(@Nonnull ResultSet resultSet) throws SQLException { } return count; } catch (SQLException | RuntimeException ex) { - if (conn.canCommit()) { - conn.rollbackInternal(); + SQLException finalException = ExceptionUtil.toRelationalException(ex).toSqlException(); + try { + if (conn.canCommit()) { + conn.rollbackInternal(); + } + } catch (SQLException | RuntimeException rollbackError) { + finalException.addSuppressed(rollbackError); } - throw ExceptionUtil.toRelationalException(ex).toSqlException(); + throw finalException; } } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerIndex.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerIndex.java index 00095bc203..cfa48a62ae 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerIndex.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerIndex.java @@ -200,6 +200,15 @@ public Builder setOptions(@Nonnull final Map options) { return this; } + @Nonnull + public Builder addAllOptions(@Nonnull final Map options) { + if (optionsBuilder == null) { + optionsBuilder = ImmutableMap.builder(); + } + optionsBuilder.putAll(options); + return this; + } + @Nonnull public Builder setOption(@Nonnull final String optionKey, @Nonnull final String optionValue) { if (optionsBuilder == null) { diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ParseHelpers.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ParseHelpers.java index 53b8454405..88fa8466a4 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ParseHelpers.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ParseHelpers.java @@ -40,6 +40,7 @@ import org.antlr.v4.runtime.tree.ParseTree; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Base64; import java.util.Locale; import java.util.function.Supplier; @@ -163,13 +164,18 @@ public static byte[] parseBytes(String text) { } } - public static boolean isDescending(@Nonnull RelationalParser.OrderByExpressionContext orderByExpressionContext) { - return (orderByExpressionContext.ASC() == null) && (orderByExpressionContext.DESC() != null); + public static boolean isNullsLast(@Nullable RelationalParser.OrderClauseContext orderClause, boolean isDescending) { + if (orderClause == null || orderClause.nulls == null) { + return isDescending; // Default behavior: ASC NULLS FIRST, DESC NULLS LAST + } + return orderClause.LAST() != null; } - public static boolean isNullsLast(@Nonnull RelationalParser.OrderByExpressionContext orderByExpressionContext, boolean isDescending) { - return orderByExpressionContext.nulls == null ? isDescending : - (orderByExpressionContext.FIRST() == null) && (orderByExpressionContext.LAST() != null); + public static boolean isDescending(@Nullable RelationalParser.OrderClauseContext orderClause) { + if (orderClause == null) { + return false; // Default is ASC + } + return orderClause.DESC() != null; } public static class ParseTreeLikeAdapter implements TreeLike { diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/ColumnSort.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/ColumnSort.java new file mode 100644 index 0000000000..0614faa94a --- /dev/null +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/ColumnSort.java @@ -0,0 +1,60 @@ +/* + * ColumnSort.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors + * + * Licensed 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 com.apple.foundationdb.relational.recordlayer.query.ddl; + +import javax.annotation.Nonnull; + +public enum ColumnSort { + + Undefined("undefined"), + AscendingNullsFirst("order_asc_nulls_first"), + AscendingNullsLast("order_asc_nulls_last"), + DescendingNullsFirst("order_desc_nulls_first"), + DescendingNullsLast("order_desc_nulls_last"); + + @Nonnull + private final String keyExpressionFunctionName; + + ColumnSort(@Nonnull final String keyExpressionFunctionName) { + this.keyExpressionFunctionName = keyExpressionFunctionName; + } + + @Nonnull + public String getKeyExpressionFunctionName() { + return keyExpressionFunctionName; + } + + @Nonnull + public static ColumnSort of(boolean isDescending, boolean isNullsLast) { + if (isDescending) { + if (isNullsLast) { + return DescendingNullsLast; + } else { + return DescendingNullsFirst; + } + } else + if (isNullsLast) { + return AscendingNullsLast; + } else { + return AscendingNullsFirst; + } + } +} diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/IndexGenerator.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/IndexGenerator.java new file mode 100644 index 0000000000..33b2a2bf71 --- /dev/null +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/IndexGenerator.java @@ -0,0 +1,294 @@ +/* + * IndexBuilder.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors + * + * Licensed 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 com.apple.foundationdb.relational.recordlayer.query.ddl; + +import com.apple.foundationdb.record.EvaluationContext; +import com.apple.foundationdb.record.RecordCoreException; +import com.apple.foundationdb.record.RecordMetaDataProto; +import com.apple.foundationdb.record.metadata.IndexPredicate; +import com.apple.foundationdb.record.metadata.IndexTypes; +import com.apple.foundationdb.record.query.combinatorics.TopologicalSort; +import com.apple.foundationdb.record.query.plan.cascades.AliasMap; +import com.apple.foundationdb.record.query.plan.cascades.Reference; +import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression; +import com.apple.foundationdb.record.query.plan.cascades.values.RecordConstructorValue; +import com.apple.foundationdb.relational.api.exceptions.ErrorCode; +import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerIndex; +import com.apple.foundationdb.relational.recordlayer.query.Expression; +import com.apple.foundationdb.relational.recordlayer.query.Expressions; +import com.apple.foundationdb.relational.recordlayer.query.Identifier; +import com.apple.foundationdb.relational.recordlayer.query.SemanticAnalyzer; +import com.apple.foundationdb.relational.util.Assert; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Streams; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.apple.foundationdb.record.query.plan.cascades.properties.ReferencesAndDependenciesProperty.referencesAndDependencies; +import static java.util.stream.Collectors.toList; + +public final class IndexGenerator { + + @Nonnull + private final Identifier indexName; + + @Nonnull + private final List keyColumns; + + @Nonnull + private final List valueColumns; + + @Nonnull + private final String indexType; + + @Nonnull + private final Map indexOptions; + + @Nonnull + private final RelationalExpression source; + + @Nonnull + private final SemanticAnalyzer semanticAnalyzer; + + private final boolean isUnique; + + private final boolean useLegacyExtremum; + + public IndexGenerator(@Nonnull final Identifier indexName, @Nonnull final RelationalExpression source, @Nonnull final SemanticAnalyzer semanticAnalyzer, + @Nonnull final List keyColumns, @Nonnull final List valueColumns, + @Nonnull final String indexType, @Nonnull final Map indexOptions, final boolean isUnique, + final boolean useLegacyExtremum) { + this.indexName = indexName; + this.source = source; + this.semanticAnalyzer = semanticAnalyzer; + this.keyColumns = ImmutableList.copyOf(keyColumns); + this.valueColumns = ImmutableList.copyOf(valueColumns); + this.indexType = indexType; + this.indexOptions = ImmutableMap.copyOf(indexOptions); + this.isUnique = isUnique; + this.useLegacyExtremum = useLegacyExtremum; + } + + @Nonnull + public RecordLayerIndex generate() { + final var recordLayerIndexBuilder = buildKeyExpression(); + return recordLayerIndexBuilder.setName(indexName.getName()) + .setUnique(isUnique) + .setIndexType(indexType) + .addAllOptions(indexOptions) + .build(); + } + + @Nonnull + private RecordLayerIndex.Builder buildKeyExpression() { + final var derivedSourceValue = new ValueLineageVisitor().visit(source).simplify(EvaluationContext.empty(), AliasMap.emptyMap(), Set.of()); + final var derivedSourceExpression = Expressions.of(Assert.castUnchecked(derivedSourceValue, RecordConstructorValue.class) + .getColumns().stream().map(Expression::fromColumn).collect(ImmutableList.toImmutableList())); + + final var keyExpressionDecoratorBuilder = KeyExpressionBuilder.KeyExpressionDecorator.newBuilder() + .setUseLegacyExtremum(useLegacyExtremum); + final var indexedExpressions = Streams.concat( + keyColumns.stream().map(keyColumn -> { + final var value = Assert.optionalUnchecked(semanticAnalyzer.lookupAlias(keyColumn.identifier, derivedSourceExpression), + ErrorCode.UNDEFINED_COLUMN, () -> "Attempting to index non existing column '" + keyColumn.identifier + "'"); + if (keyColumn.columnSort != ColumnSort.Undefined) { + keyExpressionDecoratorBuilder.addOrderKeyExpression(value.getUnderlying(), keyColumn.columnSort); + } + return value; + }), + valueColumns.stream().map(includeUid -> + Assert.optionalUnchecked(semanticAnalyzer.lookupAlias(includeUid.identifier, derivedSourceExpression), + ErrorCode.UNDEFINED_COLUMN, () -> "Attempting to index non existing column '" + includeUid.identifier + "'"))) + .collect(ImmutableList.toImmutableList()); + final var indexExpr = Expressions.of(indexedExpressions); + final var rcv = RecordConstructorValue.ofColumns(indexExpr.underlyingAsColumns()); + if (!valueColumns.isEmpty()) { + keyExpressionDecoratorBuilder.addKeyValueExpression(rcv, keyColumns.size()); + } + + final var keyExpressionBuilder = KeyExpressionBuilder.buildKeyExpression(rcv, keyExpressionDecoratorBuilder.build()); + final var indexBuilder = RecordLayerIndex.newBuilder(); + + indexBuilder.setKeyExpression(keyExpressionBuilder.getKeyExpression()) + .setIndexType(keyExpressionBuilder.getIndexType()) + .setTableName(keyExpressionBuilder.getBaseTypeName()); + + @Nullable var indexPredicate = getIndexPredicate(); + if (indexPredicate != null) { + indexBuilder.setPredicate(indexPredicate); + } + + return indexBuilder; + } + + @Nullable + private RecordMetaDataProto.Predicate getIndexPredicate() { + final var partialOrder = referencesAndDependencies().evaluate(Reference.initialOf(source)); + final var expressionRefs = + TopologicalSort.anyTopologicalOrderPermutation(partialOrder) + .orElseThrow(() -> new RecordCoreException("graph has cycles")).stream().map(Reference::get).collect(toList()); + final var predicate = LegacyIndexGenerator.getTopLevelPredicate(Lists.reverse(expressionRefs)); + if (predicate == null) { + return null; + } + return IndexPredicate.fromQueryPredicate(predicate).toProto(); + } + + public static final class IndexedColumn { + + @Nonnull + private final Identifier identifier; + + @Nonnull + private final ColumnSort columnSort; + + private IndexedColumn(@Nonnull final Identifier identifier, + @Nonnull final ColumnSort sortCriteria) { + this.identifier = identifier; + this.columnSort = sortCriteria; + } + + @Nonnull + public static IndexedColumn of(@Nonnull final Identifier identifier) { + return new IndexedColumn(identifier, ColumnSort.Undefined); + } + + @Nonnull + public static IndexedColumn of(@Nonnull final Identifier identifier, ColumnSort sort) { + return new IndexedColumn(identifier, sort); + } + + @Nonnull + public static IndexedColumn of(@Nonnull final Identifier identifier, boolean isDescending, boolean isNullsLast) { + return new IndexedColumn(identifier, ColumnSort.of(isDescending, isNullsLast)); + } + } + + @Nonnull + public static Builder newBuilder() { + return new Builder(); + } + + public static final class Builder { + + private Identifier indexName; + + private RelationalExpression indexSource; + + private SemanticAnalyzer semanticAnalyzer; + + @Nonnull + private final List keyColumns; + + @Nonnull + private final List valueColumns; + + @Nonnull + private final Map indexOptions; + + private String indexType; + + private boolean isUnique; + + private boolean useLegacyExtremum; + + private Builder() { + this.keyColumns = new ArrayList<>(); + this.valueColumns = new ArrayList<>(); + this.indexOptions = new LinkedHashMap<>(); + } + + @Nonnull + public Builder setIndexName(@Nonnull final Identifier indexName) { + this.indexName = indexName; + return this; + } + + @Nonnull + public Builder setIndexSource(@Nonnull final RelationalExpression relationalExpression) { + indexSource = relationalExpression; + return this; + } + + @Nonnull + public Builder setSemanticAnalyzer(@Nonnull final SemanticAnalyzer semanticAnalyzer) { + this.semanticAnalyzer = semanticAnalyzer; + return this; + } + + @Nonnull + public Builder addKeyColumn(@Nonnull final IndexedColumn keyColumn) { + keyColumns.add(keyColumn); + return this; + } + + @Nonnull + public Builder addValueColumn(@Nonnull final IndexedColumn keyColumn) { + valueColumns.add(keyColumn); + return this; + } + + @Nonnull + public Builder addIndexOption(@Nonnull final String key, @Nonnull final String value) { + indexOptions.put(key, value); + return this; + } + + @Nonnull + public Builder setIndexType(@Nonnull final String indexType) { + this.indexType = indexType; + return this; + } + + @Nonnull + public Builder setUnique(boolean isUnique) { + this.isUnique = isUnique; + return this; + } + + @Nonnull + public Builder setUseLegacyExtremum(boolean useLegacyExtremum) { + this.useLegacyExtremum = useLegacyExtremum; + return this; + } + + @Nonnull + public IndexGenerator build() { + Assert.notNullUnchecked(indexName); + Assert.notNullUnchecked(indexSource); + Assert.notNullUnchecked(semanticAnalyzer); + Assert.thatUnchecked(!keyColumns.isEmpty()); + if (indexType == null) { + indexType = IndexTypes.VALUE; + } + return new IndexGenerator(indexName, indexSource, semanticAnalyzer, keyColumns, valueColumns, indexType, + indexOptions, isUnique, useLegacyExtremum); + } + } +} diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/KeyExpressionBuilder.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/KeyExpressionBuilder.java new file mode 100644 index 0000000000..8bb95cc935 --- /dev/null +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/KeyExpressionBuilder.java @@ -0,0 +1,446 @@ +/* + * KeyExpressionBuilder.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors + * + * Licensed 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 com.apple.foundationdb.relational.recordlayer.query.ddl; + +import com.apple.foundationdb.record.RecordCoreException; +import com.apple.foundationdb.record.metadata.IndexTypes; +import com.apple.foundationdb.record.metadata.Key; +import com.apple.foundationdb.record.metadata.expressions.EmptyKeyExpression; +import com.apple.foundationdb.record.metadata.expressions.FieldKeyExpression; +import com.apple.foundationdb.record.metadata.expressions.GroupingKeyExpression; +import com.apple.foundationdb.record.metadata.expressions.KeyExpression; +import com.apple.foundationdb.record.metadata.expressions.ThenKeyExpression; +import com.apple.foundationdb.record.query.plan.cascades.LinkedIdentityMap; +import com.apple.foundationdb.record.query.plan.cascades.LinkedIdentitySet; +import com.apple.foundationdb.record.query.plan.cascades.typing.Type; +import com.apple.foundationdb.record.query.plan.cascades.values.CountValue; +import com.apple.foundationdb.record.query.plan.cascades.values.FieldValue; +import com.apple.foundationdb.record.query.plan.cascades.values.IndexOnlyAggregateValue; +import com.apple.foundationdb.record.query.plan.cascades.values.NumericAggregationValue; +import com.apple.foundationdb.record.query.plan.cascades.values.QuantifiedObjectValue; +import com.apple.foundationdb.record.query.plan.cascades.values.QueriedValue; +import com.apple.foundationdb.record.query.plan.cascades.values.RecordConstructorValue; +import com.apple.foundationdb.record.query.plan.cascades.values.Value; +import com.apple.foundationdb.record.query.plan.cascades.values.ValueVisitorWithDefaults; +import com.apple.foundationdb.relational.util.Assert; +import com.google.common.base.Verify; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Multimaps; +import com.google.common.collect.SetMultimap; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Stream; + +import static com.apple.foundationdb.record.metadata.Key.Expressions.field; + +public final class KeyExpressionBuilder { + + @Nonnull + private final ValueToKeyExpressionVisitor.KeyExpressionConstruction keyExpressionConstruction; + + private KeyExpressionBuilder(@Nonnull final Value value, @Nonnull final KeyExpressionDecorator keyExpressionDecorator) { + final var visitor = new ValueToKeyExpressionVisitor(keyExpressionDecorator); + keyExpressionConstruction = visitor.visit(value); + } + + @Nonnull + public KeyExpression getKeyExpression() { + return Assert.notNullUnchecked(keyExpressionConstruction.getKeyExpression()); + } + + @Nonnull + public String getIndexType() { + @Nullable final var indexType = keyExpressionConstruction.getIndexType(); + return indexType == null ? IndexTypes.VALUE : indexType; + } + + @Nonnull + public Type getBaseType() { + @Nullable final var baseType = keyExpressionConstruction.getBaseType(); + return baseType == null ? Type.any() : baseType; + } + + @Nonnull + public String getBaseTypeName() { + final var type = getBaseType(); + Assert.thatUnchecked(type.isRecord()); + final var relationType = Assert.castUnchecked(type, Type.Record.class); + return Assert.notNullUnchecked(relationType.getName()); + } + + private static final class ValueToKeyExpressionVisitor implements ValueVisitorWithDefaults { + + @Nonnull + private final KeyExpressionDecorator keyExpressionDecorator; + + ValueToKeyExpressionVisitor(@Nonnull final KeyExpressionDecorator keyExpressionDecorator) { + this.keyExpressionDecorator = keyExpressionDecorator; + } + + @Nonnull + private KeyExpressionConstruction decorate(@Nonnull final Value value, @Nonnull final KeyExpressionConstruction keyExpressionConstruction) { + final var keyExpression = keyExpressionConstruction.keyExpression; + if (keyExpression == null) { + return keyExpressionConstruction; + } + return keyExpressionConstruction.replaceKeyExpression(Objects.requireNonNull(keyExpressionDecorator.decorate(value, keyExpression))); + } + + @Nonnull + @Override + public KeyExpressionConstruction visitRecordConstructorValue(@Nonnull final RecordConstructorValue recordConstructorValue) { + final var fieldExpressionsBuilder = ImmutableList.builder(); + for (final var fieldValue : recordConstructorValue.getChildren()) { + fieldExpressionsBuilder.add(visit(fieldValue)); + } + final var fieldExpressions = fieldExpressionsBuilder.build(); + return decorate(recordConstructorValue, KeyExpressionConstruction.combine(fieldExpressions)); + } + + @Nonnull + @Override + public KeyExpressionConstruction visitDefault(@Nonnull final Value element) { + throw new RecordCoreException("generating key expression from " + element + " is not supported"); + } + + @Nonnull + @Override + public KeyExpressionConstruction visitSum(@Nonnull final NumericAggregationValue.Sum element) { + final var child = visit(element.getChild()); + return decorate(element, child.withIndexType(IndexTypes.SUM)); + } + + @Nonnull + @Override + public KeyExpressionConstruction visitCountValue(@Nonnull final CountValue element) { + final var child = visit(Iterables.getOnlyElement(element.getChildren())); + return decorate(element, child.withIndexType(IndexTypes.COUNT)); + } + + @Nonnull + @Override + public KeyExpressionConstruction visitMaxEverValue(@Nonnull final IndexOnlyAggregateValue.MaxEverValue element) { + final var child = visit(element.getChild()); + if (keyExpressionDecorator.useLegacyExtremum) { + Verify.verify(element.getChild().getResultType().isNumeric(), "only numeric types allowed in " + + IndexTypes.MAX_EVER_LONG + " aggregation operation"); + return decorate(element, child.withIndexType(IndexTypes.MAX_EVER_LONG)); + } + return decorate(element, child.withIndexType(IndexTypes.MAX_EVER_TUPLE)); + } + + @Nonnull + @Override + public KeyExpressionConstruction visitMinEverValue(@Nonnull final IndexOnlyAggregateValue.MinEverValue element) { + final var child = visit(element.getChild()); + if (keyExpressionDecorator.useLegacyExtremum) { + Verify.verify(element.getChild().getResultType().isNumeric(), "only numeric types allowed in " + + IndexTypes.MIN_EVER_LONG + " aggregation operation"); + return decorate(element, child.withIndexType(IndexTypes.MIN_EVER_LONG)); + } + return decorate(element, child.withIndexType(IndexTypes.MIN_EVER_TUPLE)); + } + + @Nonnull + @Override + public KeyExpressionConstruction visitFieldValue(@Nonnull final FieldValue fieldValue) { + final var keyExpression = toKeyExpression(fieldValue); + final var keyExpressionConstruction = visit(fieldValue.getChild()); + return decorate(fieldValue, keyExpressionConstruction.withKeyExpression(keyExpression)); + } + + @Nonnull + @Override + public KeyExpressionConstruction visitQuantifiedObjectValue(@Nonnull final QuantifiedObjectValue quantifiedObjectValue) { + return decorate(quantifiedObjectValue, KeyExpressionConstruction.ofBaseType(quantifiedObjectValue.getResultType())); + } + + @Nonnull + @Override + public KeyExpressionConstruction visitQueriedValue(@Nonnull final QueriedValue queriedValue) { + return decorate(queriedValue, KeyExpressionConstruction.ofBaseType(queriedValue.getResultType())); + } + + @Nonnull + private static KeyExpression toKeyExpression(@Nonnull final FieldValue fieldValue) { + return toKeyExpression(fieldValue, 0); + } + + @Nonnull + private static KeyExpression toKeyExpression(@Nonnull final FieldValue fieldValue, int index) { + final var field = fieldValue.getFieldPath().getFieldAccessors().get(index); + final var keyExpression = toKeyExpression(Assert.notNullUnchecked(field.getName()), field.getType()); + if (index + 1 < fieldValue.getFieldPath().getFieldAccessors().size()) { + return keyExpression.nest(toKeyExpression(fieldValue, index + 1)); + } + return keyExpression; + } + + @Nonnull + public static FieldKeyExpression toKeyExpression(@Nonnull final String name, @Nonnull final Type type) { + Assert.notNullUnchecked(name); + final var fanType = type.getTypeCode() == Type.TypeCode.ARRAY ? + KeyExpression.FanType.FanOut : + KeyExpression.FanType.None; + return field(name, fanType); + } + + public static final class KeyExpressionConstruction { + @Nullable + private final KeyExpression keyExpression; + + @Nullable + private final Type baseType; + + @Nullable + private final String indexType; + + private KeyExpressionConstruction(@Nullable final KeyExpression keyExpression, @Nullable final Type baseType, @Nullable String indexType) { + this.keyExpression = keyExpression; + this.baseType = baseType; + this.indexType = indexType; + } + + @Nonnull + public KeyExpressionConstruction withKeyExpression(@Nonnull final KeyExpression keyExpression) { + Assert.isNullUnchecked(this.keyExpression); + return new KeyExpressionConstruction(keyExpression, baseType, indexType); + } + + @Nonnull + KeyExpressionConstruction replaceKeyExpression(@Nonnull final KeyExpression keyExpression) { + return new KeyExpressionConstruction(keyExpression, baseType, indexType); + } + + @Nonnull + public KeyExpressionConstruction withBaseType(@Nonnull final Type type) { + if (baseType == null) { + return new KeyExpressionConstruction(keyExpression, type, indexType); + } + Assert.thatUnchecked(baseType.equals(type), "defining key expression on multiple types is not supported"); + return this; + } + + @Nonnull + public KeyExpressionConstruction withIndexType(@Nonnull final String indexType) { + if (this.indexType == null) { + return new KeyExpressionConstruction(keyExpression, baseType, indexType); + } + Assert.thatUnchecked(this.indexType.equals(indexType), "defining a key expression with multiple index types is not supported"); + return this; + } + + @Nullable + public Type getBaseType() { + return baseType; + } + + @Nullable + public String getIndexType() { + return indexType; + } + + @Nullable + public KeyExpression getKeyExpression() { + return keyExpression; + } + + private boolean isAggregate() { + return IndexTypes.COUNT.equals(indexType) || + IndexTypes.COUNT_NOT_NULL.equals(indexType) || + IndexTypes.SUM.equals(indexType) || + IndexTypes.MAX_EVER_LONG.equals(indexType) || + IndexTypes.PERMUTED_MAX.equals(indexType) || + IndexTypes.MIN_EVER_LONG.equals(indexType) || + IndexTypes.MIN_EVER_TUPLE.equals(indexType) || + IndexTypes.BITMAP_VALUE.equals(indexType); + } + + @Nonnull + static KeyExpressionConstruction ofBaseType(@Nonnull final Type baseType) { + return new KeyExpressionConstruction(null, baseType, null); + } + + @Nonnull + static KeyExpressionConstruction ofExpression(@Nonnull final KeyExpression expression) { + return new KeyExpressionConstruction(expression, null, null); + } + + @Nonnull + static KeyExpressionConstruction ofIndexType(@Nonnull final String indexType) { + return new KeyExpressionConstruction(null, null, indexType); + } + + @Nonnull + static KeyExpressionConstruction combine(@Nonnull final List fields) { + Assert.thatUnchecked(!fields.isEmpty()); + if (fields.size() == 1) { + return fields.get(0); + } + final var fieldTypes = fields.stream().map(KeyExpressionConstruction::getBaseType).flatMap(Stream::ofNullable).collect(ImmutableSet.toImmutableSet()); + Assert.thatUnchecked(fieldTypes.size() == 1, "defining key expression on multiple base types is not supported"); + final var indexTypes = fields.stream().map(KeyExpressionConstruction::getIndexType).flatMap(Stream::ofNullable).collect(ImmutableSet.toImmutableSet()); + Assert.thatUnchecked(indexTypes.size() <= 1, "defining key expression on multiple index types is not supported"); + final var fieldKeyExpressions = fields.stream().map(KeyExpressionConstruction::getKeyExpression).flatMap(Stream::ofNullable).collect(ImmutableList.toImmutableList()); + final var containsAggregates = fields.stream().anyMatch(KeyExpressionConstruction::isAggregate); + if (!containsAggregates) { + return new KeyExpressionConstruction(Key.Expressions.concat(fieldKeyExpressions), Assert.optionalUnchecked(fieldTypes.stream().findFirst()), null); + } + final var aggregateKeyExpression = generateAggregateIndexKeyExpression(fields); + return aggregateKeyExpression.withBaseType(Assert.optionalUnchecked(fieldTypes.stream().findFirst())); + } + + @Nonnull + private static KeyExpressionConstruction generateAggregateIndexKeyExpression(@Nonnull List fields) { + final var remainingFieldsBuilder = ImmutableList.builder(); + KeyExpressionConstruction aggregateField = null; + for (final var field : fields) { + if (field.isAggregate()) { + Assert.isNullUnchecked(aggregateField, "defining key expression with multiple aggregations is not supported"); + aggregateField = field; + } else { + remainingFieldsBuilder.add(field); + } + } + Assert.notNullUnchecked(aggregateField); + final var remainingFields = remainingFieldsBuilder.build(); + var indexTypeName = Assert.notNullUnchecked(aggregateField.indexType); + final KeyExpression groupedValue; + final GroupingKeyExpression keyExpression; + // COUNT(*) is a special case. + if (IndexTypes.COUNT.equals(indexTypeName)) { + if (!remainingFields.isEmpty()) { + keyExpression = new GroupingKeyExpression(Assert.notNullUnchecked(combine(remainingFields).keyExpression), 0); + } else { + keyExpression = new GroupingKeyExpression(EmptyKeyExpression.EMPTY, 0); + } +// } else if (aggregateValue instanceof NumericAggregationValue.BitmapConstructAgg && IndexTypes.BITMAP_VALUE.equals(indexTypeName)) { +// Assert.thatUnchecked(child instanceof FieldValue || child instanceof ArithmeticValue, "Unsupported index definition, expecting a column argument in aggregation function"); +// groupedValue = generate(List.of(child), Collections.emptyMap()); +// // only support bitmap_construct_agg(bitmap_bit_position(column)) +// // doesn't support bitmap_construct_agg(column) +// Assert.thatUnchecked(groupedValue instanceof FunctionKeyExpression, "Unsupported index definition, expecting a bitmap_bit_position function in bitmap_construct_agg function"); +// final FunctionKeyExpression functionGroupedValue = (FunctionKeyExpression) groupedValue; +// Assert.thatUnchecked(BITMAP_BIT_POSITION.equals(functionGroupedValue.getName()), "Unsupported index definition, expecting a bitmap_bit_position function in bitmap_construct_agg function"); +// final var groupedColumnValue = ((ThenKeyExpression) ((FunctionKeyExpression) groupedValue).getArguments()).getChildren().get(0); +// +// if (maybeGroupingExpression.isPresent()) { +// final var afterRemove = removeBitmapBucketOffset(maybeGroupingExpression.get()); +// if (afterRemove == null) { +// keyExpression = ((FieldKeyExpression) groupedColumnValue).ungrouped(); +// } else { +// keyExpression = ((FieldKeyExpression) groupedColumnValue).groupBy(afterRemove); +// } +// } else { +// throw Assert.failUnchecked("Unsupported index definition, unexpected grouping expression " + groupedValue); +// } + } else { + groupedValue = aggregateField.keyExpression; + Assert.thatUnchecked(groupedValue instanceof FieldKeyExpression || groupedValue instanceof ThenKeyExpression); + if (!remainingFields.isEmpty()) { + keyExpression = (groupedValue instanceof FieldKeyExpression) ? + ((FieldKeyExpression)groupedValue).groupBy(Assert.notNullUnchecked(combine(remainingFields).keyExpression)) : + ((ThenKeyExpression)groupedValue).groupBy(Assert.notNullUnchecked(combine(remainingFields).keyExpression)); + } else { + keyExpression = (groupedValue instanceof FieldKeyExpression) ? + ((FieldKeyExpression)groupedValue).ungrouped() : + ((ThenKeyExpression)groupedValue).ungrouped(); + } + } + return KeyExpressionConstruction.ofExpression(keyExpression).withIndexType(aggregateField.getIndexType()); + } + } + } + + public static final class KeyExpressionDecorator { + @Nonnull + private final SetMultimap> expressionDecorationMap; + + private final boolean useLegacyExtremum; + + private KeyExpressionDecorator(@Nonnull final SetMultimap> expressionDecorationMap, + final boolean useLegacyExtremum) { + this.expressionDecorationMap = expressionDecorationMap; + this.useLegacyExtremum = useLegacyExtremum; + } + + @Nullable + KeyExpression decorate(@Nonnull final Value value, @Nullable final KeyExpression keyExpression) { + if (keyExpression == null) { + return null; + } + return expressionDecorationMap.get(value).stream() + .reduce(keyExpression, (acc, function) -> Objects.requireNonNull(function).apply(acc), (a, b) -> b); + } + + @Nonnull + public static Builder newBuilder() { + return new Builder(); + } + + public static final class Builder { + + @Nonnull + private final SetMultimap> expressionDecorationMap; + + private boolean useLegacyExtremum; + + Builder() { + expressionDecorationMap = Multimaps.newSetMultimap(new LinkedIdentityMap<>(), LinkedIdentitySet::new); + } + + @Nonnull + public Builder addOrderKeyExpression(@Nonnull final Value value, @Nonnull final ColumnSort columnSort) { + expressionDecorationMap.put(value, keyExpression -> Key.Expressions.function(columnSort.getKeyExpressionFunctionName(), keyExpression)); + return this; + } + + @Nonnull + public Builder addKeyValueExpression(@Nonnull final Value value, int keySize) { + expressionDecorationMap.put(value, keyExpression -> Key.Expressions.keyWithValue(keyExpression, keySize)); + return this; + } + + @Nonnull + public Builder setUseLegacyExtremum(boolean useLegacyExtremum) { + this.useLegacyExtremum = useLegacyExtremum; + return this; + } + + @Nonnull + public KeyExpressionDecorator build() { + return new KeyExpressionDecorator(expressionDecorationMap, useLegacyExtremum); + } + } + } + + @Nonnull + public static KeyExpressionBuilder buildKeyExpression(@Nonnull final Value value, @Nonnull final KeyExpressionDecorator keyExpressionDecorator) { + return new KeyExpressionBuilder(value, keyExpressionDecorator); + } +} diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/IndexGenerator.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/LegacyIndexGenerator.java similarity index 97% rename from fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/IndexGenerator.java rename to fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/LegacyIndexGenerator.java index 14c10f53c5..66b798a6b1 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/IndexGenerator.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/LegacyIndexGenerator.java @@ -3,7 +3,7 @@ * * This source file is part of the FoundationDB open source project * - * Copyright 2021-2025 Apple Inc. and the FoundationDB project authors + * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ * limitations under the License. */ -package com.apple.foundationdb.relational.recordlayer.query; +package com.apple.foundationdb.relational.recordlayer.query.ddl; import com.apple.foundationdb.annotation.API; import com.apple.foundationdb.record.EvaluationContext; @@ -71,6 +71,7 @@ import com.apple.foundationdb.relational.api.exceptions.ErrorCode; import com.apple.foundationdb.relational.api.exceptions.RelationalException; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerIndex; +import com.apple.foundationdb.relational.recordlayer.query.FieldValueTrieNode; import com.apple.foundationdb.relational.util.Assert; import com.apple.foundationdb.relational.util.NullableArrayUtils; import com.google.common.base.Verify; @@ -110,7 +111,7 @@ */ @SuppressWarnings({"PMD.TooManyStaticImports", "OptionalUsedAsFieldOrParameterType"}) @API(API.Status.EXPERIMENTAL) -public final class IndexGenerator { +public final class LegacyIndexGenerator { private static final String BITMAP_BIT_POSITION = "bitmap_bit_position"; private static final String BITMAP_BUCKET_OFFSET = "bitmap_bucket_offset"; @@ -129,7 +130,7 @@ public final class IndexGenerator { private final boolean useLegacyBasedExtremumEver; - private IndexGenerator(@Nonnull RelationalExpression relationalExpression, boolean useLegacyBasedExtremumEver) { + private LegacyIndexGenerator(@Nonnull RelationalExpression relationalExpression, boolean useLegacyBasedExtremumEver) { collectQuantifiers(relationalExpression); final var partialOrder = referencesAndDependencies().evaluate(Reference.initialOf(relationalExpression)); relationalExpressions = @@ -604,7 +605,7 @@ private void checkValidity(@Nonnull List express } @Nullable - private static QueryPredicate getTopLevelPredicate(@Nonnull List expressions) { + public static QueryPredicate getTopLevelPredicate(@Nonnull List expressions) { if (expressions.isEmpty()) { return null; } @@ -629,7 +630,11 @@ private static QueryPredicate getTopLevelPredicate(@Nonnull List { + + @Nonnull + @Override + public Value visitDefault(@Nonnull final RelationalExpression element) { + var translationMapBuilder = TranslationMap.regularBuilder(); + final var quantifiers = element.getQuantifiers(); + for (final var quantifier : quantifiers) { + translationMapBuilder + .when(quantifier.getAlias()) + .then(((sourceAlias, leafValue) -> visit(quantifier.getRangesOver().get()))); + } + return element.getResultValue().translateCorrelations(translationMapBuilder.build()); + } + + + @Nonnull + @Override + public Value visitLogicalTypeFilterExpression(@Nonnull final LogicalTypeFilterExpression element) { + return projectFields(Assert.castUnchecked(element.getResultValue(), QuantifiedValue.class)); + } + + @Nonnull + @Override + public Value visitExplodeExpression(@Nonnull final ExplodeExpression element) { + return element.getCollectionValue(); + } + + @Nonnull + private static Value projectFields(@Nonnull final QuantifiedValue value) { + final var type = value.getResultType(); + if (!type.isRecord()) { + return value; + } + final var recordType = Assert.castUnchecked(type, Type.Record.class); + final var columns = ImmutableList.>builder(); + for (final var field : recordType.getFields()) { + final var fieldName = field.getFieldName(); + columns.add(Column.of(field, FieldValue.ofFieldName(value, fieldName))); + } + return RecordConstructorValue.ofColumns(columns.build()); + } +} diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/package-info.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/package-info.java new file mode 100644 index 0000000000..4542e54c51 --- /dev/null +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/package-info.java @@ -0,0 +1,25 @@ +/* + * package-info.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2021-2025 Apple Inc. and the FoundationDB project authors + * + * Licensed 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. + */ + +/** + * This package contains code responsible for creating index definitions from SQL. + */ + +package com.apple.foundationdb.relational.recordlayer.query.ddl; diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/BaseVisitor.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/BaseVisitor.java index c318a443b3..9a1a306b20 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/BaseVisitor.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/BaseVisitor.java @@ -398,8 +398,32 @@ public DataType.Named visitEnumDefinition(@Nonnull RelationalParser.EnumDefiniti @Nonnull @Override - public RecordLayerIndex visitIndexDefinition(@Nonnull RelationalParser.IndexDefinitionContext ctx) { - return ddlVisitor.visitIndexDefinition(ctx); + public RecordLayerIndex visitIndexAsSelectDefinition(@Nonnull RelationalParser.IndexAsSelectDefinitionContext ctx) { + return ddlVisitor.visitIndexAsSelectDefinition(ctx); + } + + @Nonnull + @Override + public RecordLayerIndex visitIndexOnSourceDefinition(@Nonnull RelationalParser.IndexOnSourceDefinitionContext ctx) { + return ddlVisitor.visitIndexOnSourceDefinition(ctx); + } + + @Nonnull + @Override + public Object visitIndexColumnList(@Nonnull RelationalParser.IndexColumnListContext ctx) { + return ddlVisitor.visitIndexColumnList(ctx); + } + + @Nonnull + @Override + public Object visitIndexColumnSpec(@Nonnull RelationalParser.IndexColumnSpecContext ctx) { + return ddlVisitor.visitIndexColumnSpec(ctx); + } + + @Nonnull + @Override + public Object visitIncludeClause(@Nonnull RelationalParser.IncludeClauseContext ctx) { + return ddlVisitor.visitIncludeClause(ctx); } @Override @@ -1692,4 +1716,9 @@ public DdlQueryFactory getDdlQueryFactory() { public URI getDbUri() { return dbUri; } + + @Override + public Object visitOrderClause(@Nonnull RelationalParser.OrderClauseContext ctx) { + return visitChildren(ctx); + } } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/DdlVisitor.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/DdlVisitor.java index 48ebf89a8c..56d27e38ec 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/DdlVisitor.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/DdlVisitor.java @@ -21,6 +21,7 @@ package com.apple.foundationdb.relational.recordlayer.query.visitors; import com.apple.foundationdb.annotation.API; +import com.apple.foundationdb.record.metadata.IndexTypes; import com.apple.foundationdb.record.query.plan.cascades.CorrelationIdentifier; import com.apple.foundationdb.record.query.plan.cascades.RawSqlFunction; import com.apple.foundationdb.record.query.plan.cascades.UserDefinedFunction; @@ -45,7 +46,8 @@ import com.apple.foundationdb.relational.recordlayer.query.Expression; import com.apple.foundationdb.relational.recordlayer.query.Expressions; import com.apple.foundationdb.relational.recordlayer.query.Identifier; -import com.apple.foundationdb.relational.recordlayer.query.IndexGenerator; +import com.apple.foundationdb.relational.recordlayer.query.ddl.IndexGenerator; +import com.apple.foundationdb.relational.recordlayer.query.ddl.LegacyIndexGenerator; import com.apple.foundationdb.relational.recordlayer.query.LogicalOperator; import com.apple.foundationdb.relational.recordlayer.query.PreparedParams; import com.apple.foundationdb.relational.recordlayer.query.ProceduralPlan; @@ -58,11 +60,13 @@ import org.antlr.v4.runtime.ParserRuleContext; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -193,23 +197,78 @@ public RecordLayerTable visitStructDefinition(@Nonnull RelationalParser.StructDe @Nonnull @Override - public RecordLayerIndex visitIndexDefinition(@Nonnull RelationalParser.IndexDefinitionContext ctx) { - final var indexId = visitUid(ctx.indexName); + public RecordLayerIndex visitIndexAsSelectDefinition(@Nonnull RelationalParser.IndexAsSelectDefinitionContext indexDefinitionContext) { + final var indexId = visitUid(indexDefinitionContext.indexName); final var ddlCatalog = metadataBuilder.build(); // parse the index SQL query using the newly constructed metadata. getDelegate().replaceSchemaTemplate(ddlCatalog); final var viewPlan = getDelegate().getPlanGenerationContext().withDisabledLiteralProcessing(() -> - Assert.castUnchecked(ctx.queryTerm().accept(this), LogicalOperator.class).getQuantifier().getRangesOver().get()); + Assert.castUnchecked(indexDefinitionContext.queryTerm().accept(this), LogicalOperator.class).getQuantifier().getRangesOver().get()); - final var useLegacyBasedExtremumEver = ctx.indexAttributes() != null && ctx.indexAttributes().indexAttribute().stream().anyMatch(attribute -> attribute.LEGACY_EXTREMUM_EVER() != null); - final var isUnique = ctx.UNIQUE() != null; - final var generator = IndexGenerator.from(viewPlan, useLegacyBasedExtremumEver); + final var useLegacyBasedExtremumEver = indexDefinitionContext.indexAttributes() != null && indexDefinitionContext.indexAttributes().indexAttribute().stream().anyMatch(attribute -> attribute.LEGACY_EXTREMUM_EVER() != null); + final var isUnique = indexDefinitionContext.UNIQUE() != null; + final var generator = LegacyIndexGenerator.from(viewPlan, useLegacyBasedExtremumEver); final var table = metadataBuilder.findTable(generator.getRecordTypeName()); Assert.thatUnchecked(viewPlan instanceof LogicalSortExpression, ErrorCode.INVALID_COLUMN_REFERENCE, "Cannot create index and order by an expression that is not present in the projection list"); return generator.generate(indexId.getName(), isUnique, table.getType(), containsNullableArray); } + @Nonnull + @Override + public RecordLayerIndex visitIndexOnSourceDefinition(@Nonnull RelationalParser.IndexOnSourceDefinitionContext indexDefinitionContext) { + final var ddlCatalog = metadataBuilder.build(); + // parse the index SQL query using the newly constructed metadata. + getDelegate().replaceSchemaTemplate(ddlCatalog); + getDelegate().pushPlanFragment(); + final var tableIdentifier = visitFullId(indexDefinitionContext.source); + final var logicalOperator = getDelegate().getPlanGenerationContext().withDisabledLiteralProcessing(() -> + LogicalOperator.generateAccess(tableIdentifier, Optional.empty(), Set.of(), + getDelegate().getSemanticAnalyzer(), getDelegate().getCurrentPlanFragment(), + getDelegate().getLogicalOperatorCatalog())); + getDelegate().getCurrentPlanFragment().setOperator(logicalOperator); + + final Identifier indexId = visitUid(indexDefinitionContext.indexName); + final var isUnique = indexDefinitionContext.UNIQUE() != null; + + @Nullable final var indexOptions = indexDefinitionContext.indexOptions(); + final var useLegacyExtremum = indexOptions != null && indexOptions.indexOption().stream().anyMatch(option -> option.LEGACY_EXTREMUM_EVER() != null); + + final var indexGeneratorBuilder = IndexGenerator.newBuilder() + .setIndexName(indexId) + .setIndexSource(getDelegate().getCurrentPlanFragment().getLogicalOperators().first().getQuantifier().getRangesOver().get()) + .setSemanticAnalyzer(getDelegate().getSemanticAnalyzer()) + .setIndexType(IndexTypes.VALUE) + .setUnique(isUnique); + + indexDefinitionContext.indexColumnList().indexColumnSpec().forEach(columnSpec -> { + final var columnId = visitUid(columnSpec.columnName); + final var orderContext = columnSpec.orderClause(); + if (orderContext != null) { + final boolean isDesc = orderContext.DESC() != null; + final boolean nullsLast; + if (orderContext.nulls == null) { + nullsLast = isDesc; + } else { + nullsLast = orderContext.LAST() != null; + } + indexGeneratorBuilder.addKeyColumn(IndexGenerator.IndexedColumn.of(columnId, isDesc, nullsLast)); + } else { + indexGeneratorBuilder.addKeyColumn(IndexGenerator.IndexedColumn.of(columnId)); + } + }); + + if (indexDefinitionContext.includeClause() != null) { + indexDefinitionContext.includeClause().uidList().uid().forEach(uid -> { + final var columnId = visitUid(uid); + indexGeneratorBuilder.addValueColumn(IndexGenerator.IndexedColumn.of(columnId)); + }); + } + + getDelegate().popPlanFragment(); + return indexGeneratorBuilder.build().generate(); + } + @Nonnull @Override public DataType.Named visitEnumDefinition(@Nonnull RelationalParser.EnumDefinitionContext ctx) { @@ -266,7 +325,6 @@ public ProceduralPlan visitCreateSchemaTemplateStatement(@Nonnull RelationalPars } structClauses.build().stream().map(this::visitStructDefinition).map(RecordLayerTable::getDatatype).forEach(metadataBuilder::addAuxiliaryType); tableClauses.build().stream().map(this::visitTableDefinition).forEach(metadataBuilder::addTable); - final var indexes = indexClauses.build().stream().map(this::visitIndexDefinition).collect(ImmutableList.toImmutableList()); // TODO: this is currently relying on the lexical order of the function to resolve function dependencies which // is limited. sqlInvokedFunctionClauses.build().forEach(functionClause -> { @@ -277,6 +335,7 @@ public ProceduralPlan visitCreateSchemaTemplateStatement(@Nonnull RelationalPars final var view = getViewMetadata(viewClause, metadataBuilder.build()); metadataBuilder.addView(view); }); + final var indexes = indexClauses.build().stream().map(clause -> Assert.castUnchecked(visit(clause), RecordLayerIndex.class)).collect(ImmutableList.toImmutableList()); for (final var index : indexes) { final var table = metadataBuilder.extractTable(index.getTableName()); final var tableWithIndex = RecordLayerTable.Builder.from(table).addIndex(index).build(); diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/DelegatingVisitor.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/DelegatingVisitor.java index 4273b248ac..74d751aef1 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/DelegatingVisitor.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/DelegatingVisitor.java @@ -242,10 +242,29 @@ public DataType.Named visitEnumDefinition(@Nonnull RelationalParser.EnumDefiniti return getDelegate().visitEnumDefinition(ctx); } - @Nonnull @Override - public RecordLayerIndex visitIndexDefinition(@Nonnull RelationalParser.IndexDefinitionContext ctx) { - return getDelegate().visitIndexDefinition(ctx); + public Object visitIndexType(final RelationalParser.IndexTypeContext ctx) { + return getDelegate().visitIndexType(ctx); + } + + @Override + public Object visitIndexOptions(final RelationalParser.IndexOptionsContext ctx) { + return getDelegate().visitIndexOptions(ctx); + } + + @Override + public Object visitIndexOption(final RelationalParser.IndexOptionContext ctx) { + return getDelegate().visitIndexOption(ctx); + } + + @Override + public Object visitVectorIndexOptions(final RelationalParser.VectorIndexOptionsContext ctx) { + return getDelegate().visitVectorIndexOptions(ctx); + } + + @Override + public Object visitVectorIndexOption(final RelationalParser.VectorIndexOptionContext ctx) { + return getDelegate().visitVectorIndexOption(ctx); } @Nonnull @@ -542,6 +561,41 @@ public OrderByExpression visitOrderByExpression(@Nonnull RelationalParser.OrderB return getDelegate().visitOrderByExpression(ctx); } + @Nonnull + @Override + public RecordLayerIndex visitIndexAsSelectDefinition(@Nonnull RelationalParser.IndexAsSelectDefinitionContext ctx) { + return getDelegate().visitIndexAsSelectDefinition(ctx); + } + + @Nonnull + @Override + public RecordLayerIndex visitIndexOnSourceDefinition(@Nonnull RelationalParser.IndexOnSourceDefinitionContext ctx) { + return getDelegate().visitIndexOnSourceDefinition(ctx); + } + + @Override + public Object visitVectorIndexDefinition(final RelationalParser.VectorIndexDefinitionContext ctx) { + return getDelegate().visitVectorIndexDefinition(ctx); + } + + @Nonnull + @Override + public Object visitIndexColumnList(@Nonnull RelationalParser.IndexColumnListContext ctx) { + return getDelegate().visitIndexColumnList(ctx); + } + + @Nonnull + @Override + public Object visitIndexColumnSpec(@Nonnull RelationalParser.IndexColumnSpecContext ctx) { + return getDelegate().visitIndexColumnSpec(ctx); + } + + @Nonnull + @Override + public Object visitIncludeClause(@Nonnull RelationalParser.IncludeClauseContext ctx) { + return getDelegate().visitIncludeClause(ctx); + } + @Override @Nullable public Void visitTableSources(@Nonnull RelationalParser.TableSourcesContext ctx) { @@ -1370,6 +1424,11 @@ public Object visitWindowName(@Nonnull RelationalParser.WindowNameContext ctx) { return getDelegate().visitWindowName(ctx); } + @Override + public Object visitPartitionClause(final RelationalParser.PartitionClauseContext ctx) { + return null; + } + @Nonnull @Override public Object visitScalarFunctionName(@Nonnull RelationalParser.ScalarFunctionNameContext ctx) { @@ -1578,6 +1637,11 @@ public Object visitChildren(RuleNode node) { return getDelegate().visitChildren(node); } + @Override + public Object visitOrderClause(@Nonnull RelationalParser.OrderClauseContext ctx) { + return getDelegate().visitOrderClause(ctx); + } + @Override public Object visitTerminal(TerminalNode node) { return getDelegate().visitTerminal(node); diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/ExpressionVisitor.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/ExpressionVisitor.java index 32bc2e7ff7..0be9baadec 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/ExpressionVisitor.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/ExpressionVisitor.java @@ -199,8 +199,8 @@ public List visitOrderByClause(@Nonnull RelationalParser.Orde @Override public OrderByExpression visitOrderByExpression(@Nonnull RelationalParser.OrderByExpressionContext orderByExpressionContext) { final var expression = Assert.castUnchecked(orderByExpressionContext.expression().accept(this), Expression.class); - final var descending = ParseHelpers.isDescending(orderByExpressionContext); - final var nullsLast = ParseHelpers.isNullsLast(orderByExpressionContext, descending); + final var descending = ParseHelpers.isDescending(orderByExpressionContext.orderClause()); + final var nullsLast = ParseHelpers.isNullsLast(orderByExpressionContext.orderClause(), descending); return OrderByExpression.of(expression, descending, nullsLast); } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java index 0ff0587b33..dd5b75c288 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java @@ -595,8 +595,8 @@ public List visitOrderByClauseForSelect(@Nonnull RelationalPa final var matchingExpressionMaybe = isAliasMaybe.flatMap(alias -> semanticAnalyzer.lookupAlias(Identifier.toProtobufCompliant(visitFullId(alias)), validSelectAliases)); matchingExpressionMaybe.ifPresentOrElse( matchingExpression -> { - final var descending = ParseHelpers.isDescending(orderByExpression); - final var nullsLast = ParseHelpers.isNullsLast(orderByExpression, descending); + final var descending = ParseHelpers.isDescending(orderByExpression.orderClause()); + final var nullsLast = ParseHelpers.isNullsLast(orderByExpression.orderClause(), descending); orderBysBuilder.add(OrderByExpression.of(matchingExpression, descending, nullsLast)); }, () -> orderBysBuilder.add(visitOrderByExpression(orderByExpression)) diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/TypedVisitor.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/TypedVisitor.java index 92a0ca8db9..b31f15c611 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/TypedVisitor.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/TypedVisitor.java @@ -165,7 +165,23 @@ public interface TypedVisitor extends RelationalParserVisitor { @Nonnull @Override - RecordLayerIndex visitIndexDefinition(@Nonnull RelationalParser.IndexDefinitionContext ctx); + RecordLayerIndex visitIndexAsSelectDefinition(@Nonnull RelationalParser.IndexAsSelectDefinitionContext ctx); + + @Nonnull + @Override + RecordLayerIndex visitIndexOnSourceDefinition(@Nonnull RelationalParser.IndexOnSourceDefinitionContext ctx); + + @Nonnull + @Override + Object visitIndexColumnList(@Nonnull RelationalParser.IndexColumnListContext ctx); + + @Nonnull + @Override + Object visitIndexColumnSpec(@Nonnull RelationalParser.IndexColumnSpecContext ctx); + + @Nonnull + @Override + Object visitIncludeClause(@Nonnull RelationalParser.IncludeClauseContext ctx); @Override Object visitIndexAttributes(RelationalParser.IndexAttributesContext ctx); diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/util/ExceptionUtil.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/util/ExceptionUtil.java index 65035feaf2..fe41d069bc 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/util/ExceptionUtil.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/util/ExceptionUtil.java @@ -22,6 +22,7 @@ import com.apple.foundationdb.annotation.API; import com.apple.foundationdb.record.RecordCoreException; +import com.apple.foundationdb.record.RecordIndexUniquenessViolation; import com.apple.foundationdb.record.metadata.MetaDataException; import com.apple.foundationdb.record.provider.foundationdb.FDBExceptions; import com.apple.foundationdb.record.provider.foundationdb.RecordAlreadyExistsException; @@ -66,7 +67,7 @@ private static RelationalException recordCoreToRelationalException(RecordCoreExc code = ErrorCode.TRANSACTION_INACTIVE; } else if (re instanceof RecordDeserializationException || re.getCause() instanceof RecordDeserializationException) { code = ErrorCode.DESERIALIZATION_FAILURE; - } else if (re instanceof RecordAlreadyExistsException || re.getCause() instanceof RecordAlreadyExistsException) { + } else if (re instanceof RecordAlreadyExistsException || re.getCause() instanceof RecordAlreadyExistsException || re instanceof RecordIndexUniquenessViolation) { code = ErrorCode.UNIQUE_CONSTRAINT_VIOLATION; } else if (re instanceof MetaDataException) { //TODO(bfines) map this to specific error codes based on the violation diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlStatementParsingTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlStatementParsingTest.java index a6d3871aa0..fd3ad74959 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlStatementParsingTest.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlStatementParsingTest.java @@ -20,10 +20,14 @@ package com.apple.foundationdb.relational.api.ddl; +import com.apple.foundationdb.record.RecordMetaDataProto; import com.apple.foundationdb.record.expressions.RecordKeyExpressionProto; +import com.apple.foundationdb.record.metadata.IndexTypes; +import com.apple.foundationdb.record.metadata.Key; import com.apple.foundationdb.record.metadata.expressions.KeyExpression; import com.apple.foundationdb.record.metadata.expressions.ThenKeyExpression; import com.apple.foundationdb.relational.api.Options; +import com.apple.foundationdb.relational.api.ddl.DdlTestUtil.IndexedColumn; import com.apple.foundationdb.relational.api.exceptions.ErrorCode; import com.apple.foundationdb.relational.api.exceptions.RelationalException; import com.apple.foundationdb.relational.api.metadata.Index; @@ -55,6 +59,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; @@ -65,6 +70,7 @@ import java.sql.SQLException; import java.sql.Types; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.function.IntPredicate; import java.util.stream.Collectors; @@ -78,6 +84,7 @@ * that the underlying execution is correct, only that the language is parsed as expected. */ public class DdlStatementParsingTest { + @RegisterExtension @Order(0) public final EmbeddedRelationalExtension relationalExtension = new EmbeddedRelationalExtension(); @@ -111,7 +118,9 @@ public static Stream columnTypePermutations() { final List items = List.of(validPrimitiveDataTypes); final PermutationIterator permutations = PermutationIterator.generatePermutations(items, numColumns); - return permutations.stream().map(Arguments::of); + return permutations.stream() + .flatMap(permutation -> Arrays.stream(DdlTestUtil.IndexSyntax.values()) + .map(syntax -> Arguments.of(syntax, permutation))); } void shouldFailWith(@Nonnull final String query, @Nullable final ErrorCode errorCode) throws Exception { @@ -184,25 +193,28 @@ private static DescriptorProtos.FileDescriptorProto getProtoDescriptor(@Nonnull return asRecordLayerSchemaTemplate.toRecordMetadata().toProto().getRecords(); } - @Test - void indexFailsWithNonExistingTable() throws Exception { + @EnumSource(DdlTestUtil.IndexSyntax.class) + @ParameterizedTest + void indexFailsWithNonExistingTable(DdlTestUtil.IndexSyntax indexSyntax) throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + - "CREATE INDEX t_idx as select a from foo"; + DdlTestUtil.generateIndexDdlStatement(indexSyntax, "t_idx", List.of(new IndexedColumn("a")), List.of(), "foo"); shouldFailWith(stmt, ErrorCode.INVALID_SCHEMA_TEMPLATE); } - @Test - void indexFailsWithNonExistingIndexColumn() throws Exception { + @EnumSource(DdlTestUtil.IndexSyntax.class) + @ParameterizedTest + void indexFailsWithNonExistingIndexColumn(DdlTestUtil.IndexSyntax indexSyntax) throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + - "CREATE TABLE foo(a bigint, PRIMARY KEY(a))" + - " CREATE INDEX t_idx as select non_existing from foo"; + "CREATE TABLE foo(a bigint, PRIMARY KEY(a)) " + + DdlTestUtil.generateIndexDdlStatement(indexSyntax, "t_idx", List.of(new IndexedColumn("non_existing")), List.of(), "foo"); shouldFailWith(stmt, ErrorCode.UNDEFINED_COLUMN); } - @Test - void indexFailsWithReservedKeywordAsName() throws Exception { + @EnumSource(DdlTestUtil.IndexSyntax.class) + @ParameterizedTest + void indexFailsWithReservedKeywordAsName(DdlTestUtil.IndexSyntax indexSyntax) throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + - "CREATE INDEX table as select a from foo"; + DdlTestUtil.generateIndexDdlStatement(indexSyntax, "table", List.of(new IndexedColumn("a")), List.of(), "foo"); shouldFailWith(stmt, ErrorCode.SYNTAX_ERROR); } @@ -385,7 +397,8 @@ void createInvalidVectorType(String vectorType) throws Exception { @ParameterizedTest @MethodSource("columnTypePermutations") - void createSchemaTemplateWithOutOfOrderDefinitionsWork(List columns) throws Exception { + void createSchemaTemplateWithOutOfOrderDefinitionsWork(DdlTestUtil.IndexSyntax indexSyntax, List columns) throws Exception { + Assumptions.assumeTrue(indexSyntax == DdlTestUtil.IndexSyntax.INDEX_AS_SYNTAX); final String templateStatement = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TABLE TBL " + makeColumnDefinition(columns, true) + "CREATE TYPE AS STRUCT FOO " + makeColumnDefinition(columns, false); @@ -406,7 +419,8 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat /*Schema Template tests*/ @ParameterizedTest @MethodSource("columnTypePermutations") - void createSchemaTemplates(List columns) throws Exception { + void createSchemaTemplates(DdlTestUtil.IndexSyntax indexSyntax, List columns) throws Exception { + Assumptions.assumeTrue(indexSyntax == DdlTestUtil.IndexSyntax.INDEX_AS_SYNTAX); final String columnStatement = "CREATE SCHEMA TEMPLATE test_template " + " CREATE TYPE AS STRUCT foo " + makeColumnDefinition(columns, false) + " CREATE TABLE bar (col0 bigint, col1 foo, PRIMARY KEY(col0))"; @@ -432,7 +446,8 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @ParameterizedTest @MethodSource("columnTypePermutations") - void createSchemaTemplateTableWithOnlyRecordType(List columns) throws Exception { + void createSchemaTemplateTableWithOnlyRecordType(DdlTestUtil.IndexSyntax indexSyntax, List columns) throws Exception { + Assumptions.assumeTrue(indexSyntax == DdlTestUtil.IndexSyntax.INDEX_AS_SYNTAX); final String baseTableDef = replaceLast(makeColumnDefinition(columns, false), ')', ", SINGLE ROW ONLY)"); final String columnStatement = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TABLE foo " + baseTableDef; @@ -459,12 +474,12 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @ParameterizedTest @MethodSource("columnTypePermutations") - void createSchemaTemplateWithDuplicateIndexesFails(List columns) throws Exception { + void createSchemaTemplateWithDuplicateIndexesFails(DdlTestUtil.IndexSyntax indexSyntax, List columns) throws Exception { final String baseTableDef = makeColumnDefinition(columns, true); final String columnStatement = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TABLE FOO " + baseTableDef + - " CREATE INDEX foo_idx as select col0 from foo order by col0" + - " CREATE INDEX foo_idx as select col1 from foo order by col1"; //duplicate with the same name on same table should fail + DdlTestUtil.generateIndexDdlStatement(indexSyntax, "foo_idx", List.of(new IndexedColumn("col0")), List.of(), "foo") + + DdlTestUtil.generateIndexDdlStatement(indexSyntax, "foo_idx", List.of(new IndexedColumn("col1")), List.of(), "foo"); //duplicate with the same name on same table should fail shouldFailWithInjectedFactory(columnStatement, ErrorCode.INDEX_ALREADY_EXISTS, new AbstractMetadataOperationsFactory() { @Nonnull @@ -480,12 +495,12 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @ParameterizedTest @MethodSource("columnTypePermutations") - void createSchemaTemplateWithIndex(List columns) throws Exception { - final String indexColumns = String.join(",", chooseIndexColumns(columns, n -> n % 2 == 0)); + void createSchemaTemplateWithIndex(DdlTestUtil.IndexSyntax indexSyntax, List columns) throws Exception { + final List indexColumns = chooseIndexColumns(columns, n -> n % 2 == 0); final String templateStatement = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TYPE AS STRUCT foo " + makeColumnDefinition(columns, false) + "CREATE TABLE tbl " + makeColumnDefinition(columns, true) + - "CREATE INDEX v_idx as select " + indexColumns + " from tbl order by " + indexColumns; + DdlTestUtil.generateIndexDdlStatement(indexSyntax, "v_idx", indexColumns.stream().map(IndexedColumn::new).collect(Collectors.toList()), List.of(), "tbl"); shouldWorkWithInjectedFactory(templateStatement, new AbstractMetadataOperationsFactory() { @Nonnull @@ -526,14 +541,14 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @ParameterizedTest @MethodSource("columnTypePermutations") - void createSchemaTemplateWithIndexAndInclude(List columns) throws Exception { + void createSchemaTemplateWithIndexAndInclude(DdlTestUtil.IndexSyntax indexSyntax, List columns) throws Exception { Assumptions.assumeTrue(columns.size() > 1); //the test only works with multiple columns final List indexedColumns = chooseIndexColumns(columns, n -> n % 2 == 0); //choose every other column final List unindexedColumns = chooseIndexColumns(columns, n -> n % 2 != 0); final String templateStatement = "CREATE SCHEMA TEMPLATE test_template " + " CREATE TYPE AS STRUCT foo " + makeColumnDefinition(columns, false) + " CREATE TABLE tbl " + makeColumnDefinition(columns, true) + - " CREATE INDEX v_idx as select " + Stream.concat(indexedColumns.stream(), unindexedColumns.stream()).collect(Collectors.joining(",")) + " from tbl order by " + String.join(",", indexedColumns); + DdlTestUtil.generateIndexDdlStatement(indexSyntax, "v_idx", indexedColumns.stream().map(IndexedColumn::new).collect(Collectors.toList()), unindexedColumns, "tbl"); shouldWorkWithInjectedFactory(templateStatement, new AbstractMetadataOperationsFactory() { @Nonnull @Override @@ -634,7 +649,8 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @ParameterizedTest @MethodSource("columnTypePermutations") - void createTable(List columns) throws Exception { + void createTable(DdlTestUtil.IndexSyntax indexSyntax, List columns) throws Exception { + Assumptions.assumeTrue(indexSyntax == DdlTestUtil.IndexSyntax.INDEX_AS_SYNTAX); final String columnStatement = "CREATE SCHEMA TEMPLATE test_template CREATE TABLE foo " + makeColumnDefinition(columns, true); shouldWorkWithInjectedFactory(columnStatement, new AbstractMetadataOperationsFactory() { @@ -659,7 +675,8 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @ParameterizedTest @MethodSource("columnTypePermutations") - void createTableAndType(List columns) throws Exception { + void createTableAndType(DdlTestUtil.IndexSyntax indexSyntax, List columns) throws Exception { + Assumptions.assumeTrue(indexSyntax == DdlTestUtil.IndexSyntax.INDEX_AS_SYNTAX); final String typeDef = "CREATE TYPE AS STRUCT typ " + makeColumnDefinition(columns, false); // current implementation of metadata prunes unused types in the serialization, this may or may not // be something we want to commit to long term. @@ -1126,9 +1143,9 @@ void createViewWithFunctionAndCteComplexNestingWorks() throws Exception { final String schemaStatement = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TYPE AS STRUCT baz (a bigint, b bigint) " + "CREATE TYPE AS ENUM foo ('OPTION_1', 'OPTION_2') " + + "CREATE TABLE bar (id bigint, baz_field baz, foo_field foo, PRIMARY KEY(id)) " + "CREATE FUNCTION F1 (IN A BIGINT) AS SELECT id, baz_field, foo_field FROM bar WHERE id > A " + - "CREATE VIEW v AS WITH C1 AS (WITH C2 AS (SELECT foo_field, id, baz_field FROM F1(20)) SELECT * FROM C2) SELECT * FROM C1 " + - "CREATE TABLE bar (id bigint, baz_field baz, foo_field foo, PRIMARY KEY(id)) "; + "CREATE VIEW v AS WITH C1 AS (WITH C2 AS (SELECT foo_field, id, baz_field FROM F1(20)) SELECT * FROM C2) SELECT * FROM C1 "; shouldWorkWithInjectedFactory(schemaStatement, new AbstractMetadataOperationsFactory() { @Nonnull @@ -1186,6 +1203,215 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat }); } + @Test + void createIndexOnBasicSyntax() throws Exception { + final String schemaStatement = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TYPE AS STRUCT baz (a bigint, b bigint) " + + "CREATE TYPE AS ENUM foo ('OPTION_1', 'OPTION_2') " + + "CREATE TABLE bar (id bigint, a bigint, b bigint, c bigint, PRIMARY KEY(id)) " + + "CREATE INDEX i1 on bar(a, b) include (c)"; + + shouldWorkWithInjectedFactory(schemaStatement, new AbstractMetadataOperationsFactory() { + @Nonnull + @Override + public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, + @Nonnull Options templateProperties) { + final var tableMaybe = Assertions.assertDoesNotThrow(() -> template.findTableByName("bar")); + assertThat(tableMaybe).isPresent(); + final var table = Assert.optionalUnchecked(tableMaybe); + assertThat(table.getIndexes().size()).isEqualTo(1); + final var index = Assert.optionalUnchecked(table.getIndexes().stream().findFirst()); + assertThat(index.getName()).isEqualTo("i1"); + assertThat(index.getIndexType()).isEqualTo(IndexTypes.VALUE); + assertThat(index).isInstanceOf(RecordLayerIndex.class); + final var recordLayerIndex = Assert.castUnchecked(index, RecordLayerIndex.class); + assertThat(recordLayerIndex.getKeyExpression()).isEqualTo(Key.Expressions.keyWithValue( + Key.Expressions.concat(Key.Expressions.field("a"), Key.Expressions.field("b"), Key.Expressions.field("c")), 2)); + return txn -> { + }; + } + }); + } + + @Test + void createIndexOnPredicatedView() throws Exception { + final String schemaStatement = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TYPE AS STRUCT baz (a bigint, b bigint) " + + "CREATE TYPE AS ENUM foo ('OPTION_1', 'OPTION_2') " + + "CREATE TABLE bar (id bigint, a bigint, b bigint, c bigint, PRIMARY KEY(id)) " + + "CREATE VIEW v1 AS SELECT b, c FROM bar WHERE a < 100 " + + "CREATE INDEX i1 on v1(b, c)"; + + shouldWorkWithInjectedFactory(schemaStatement, new AbstractMetadataOperationsFactory() { + @Nonnull + @Override + public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, + @Nonnull Options templateProperties) { + final var tableMaybe = Assertions.assertDoesNotThrow(() -> template.findTableByName("bar")); + assertThat(tableMaybe).isPresent(); + final var table = Assert.optionalUnchecked(tableMaybe); + assertThat(table.getIndexes().size()).isEqualTo(1); + final var index = Assert.optionalUnchecked(table.getIndexes().stream().findFirst()); + assertThat(index.getName()).isEqualTo("i1"); + assertThat(index.getIndexType()).isEqualTo(IndexTypes.VALUE); + assertThat(index).isInstanceOf(RecordLayerIndex.class); + final var recordLayerIndex = Assert.castUnchecked(index, RecordLayerIndex.class); + assertThat(recordLayerIndex.getKeyExpression()).isEqualTo( + Key.Expressions.concat(Key.Expressions.field("b"), Key.Expressions.field("c"))); + assertThat(recordLayerIndex.getPredicate()).isNotNull(); + final var predicate = Assert.notNullUnchecked(recordLayerIndex.getPredicate()); + final var expectedPredicateProto = RecordMetaDataProto.Predicate.newBuilder() + .setValuePredicate(RecordMetaDataProto.ValuePredicate.newBuilder().addValue("a") + .setComparison(RecordMetaDataProto.Comparison.newBuilder() + .setSimpleComparison(RecordMetaDataProto.SimpleComparison.newBuilder() + .setType(RecordMetaDataProto.ComparisonType.LESS_THAN) + .setOperand(RecordKeyExpressionProto.Value.newBuilder().setLongValue(100L).build()) + .build()) + .build()) + .build()) + .build(); + assertThat(predicate).isEqualTo(expectedPredicateProto); + return txn -> { + }; + } + }); + } + + @Test + void createVectorIndex() throws Exception { + final String schemaStatement = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TYPE AS STRUCT baz (a bigint, b bigint) " + + "CREATE TYPE AS ENUM foo ('OPTION_1', 'OPTION_2') " + + "CREATE TABLE bar (id bigint, a bigint, b bigint, c bigint, PRIMARY KEY(id)) " + + "CREATE VIEW v1 AS SELECT b, c FROM bar WHERE a < 100 " + + "CREATE INDEX i1 on v1(b, c)"; + + shouldWorkWithInjectedFactory(schemaStatement, new AbstractMetadataOperationsFactory() { + @Nonnull + @Override + public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, + @Nonnull Options templateProperties) { + final var tableMaybe = Assertions.assertDoesNotThrow(() -> template.findTableByName("bar")); + assertThat(tableMaybe).isPresent(); + final var table = Assert.optionalUnchecked(tableMaybe); + assertThat(table.getIndexes().size()).isEqualTo(1); + final var index = Assert.optionalUnchecked(table.getIndexes().stream().findFirst()); + assertThat(index.getName()).isEqualTo("i1"); + assertThat(index.getIndexType()).isEqualTo(IndexTypes.VALUE); + assertThat(index).isInstanceOf(RecordLayerIndex.class); + final var recordLayerIndex = Assert.castUnchecked(index, RecordLayerIndex.class); + assertThat(recordLayerIndex.getKeyExpression()).isEqualTo( + Key.Expressions.concat(Key.Expressions.field("b"), Key.Expressions.field("c"))); + assertThat(recordLayerIndex.getPredicate()).isNotNull(); + final var predicate = Assert.notNullUnchecked(recordLayerIndex.getPredicate()); + final var expectedPredicateProto = RecordMetaDataProto.Predicate.newBuilder() + .setValuePredicate(RecordMetaDataProto.ValuePredicate.newBuilder().addValue("a") + .setComparison(RecordMetaDataProto.Comparison.newBuilder() + .setSimpleComparison(RecordMetaDataProto.SimpleComparison.newBuilder() + .setType(RecordMetaDataProto.ComparisonType.LESS_THAN) + .setOperand(RecordKeyExpressionProto.Value.newBuilder().setLongValue(100L).build()) + .build()) + .build()) + .build()) + .build(); + assertThat(predicate).isEqualTo(expectedPredicateProto); + return txn -> { + }; + } + }); + } + + @Test + void createIndexOnBasicSyntaxComplex() throws Exception { + final String schemaStatement = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TYPE AS STRUCT baz (a bigint, b bigint) " + + "CREATE TYPE AS ENUM foo ('OPTION_1', 'OPTION_2') " + + "CREATE TABLE bar (id bigint, a bigint, b bigint, c bigint, PRIMARY KEY(id)) " + + "CREATE VIEW v1 AS SELECT * FROM (SELECT b as x, c as y FROM bar) as d " + + "CREATE INDEX i1 on v1(x desc nulls first, y asc nulls last)"; + + shouldWorkWithInjectedFactory(schemaStatement, new AbstractMetadataOperationsFactory() { + @Nonnull + @Override + public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, + @Nonnull Options templateProperties) { + final var tableMaybe = Assertions.assertDoesNotThrow(() -> template.findTableByName("bar")); + assertThat(tableMaybe).isPresent(); + final var table = Assert.optionalUnchecked(tableMaybe); + assertThat(table.getIndexes().size()).isEqualTo(1); + final var index = Assert.optionalUnchecked(table.getIndexes().stream().findFirst()); + assertThat(index.getIndexType()).isEqualTo(IndexTypes.VALUE); + assertThat(index.getName()).isEqualTo("i1"); + assertThat(index).isInstanceOf(RecordLayerIndex.class); + final var recordLayerIndex = Assert.castUnchecked(index, RecordLayerIndex.class); + assertThat(recordLayerIndex.getKeyExpression()).isEqualTo( + Key.Expressions.concat(Key.Expressions.function("order_desc_nulls_first", Key.Expressions.field("b")), + Key.Expressions.function("order_asc_nulls_last", Key.Expressions.field("c")))); + return txn -> { + }; + } + }); + } + + @Test + void createIndexOnRepeated() throws Exception { + final String schemaStatement = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TYPE AS STRUCT A(x bigint) " + + "CREATE TABLE T(p bigint, a A array, primary key(p)) " + + "CREATE VIEW mv1 AS SELECT SQ.x, t.p from T AS t, (select M.x from t.a AS M) SQ order by SQ.x, t.p " + + "CREATE INDEX i1 on mv1(x, p)"; + + shouldWorkWithInjectedFactory(schemaStatement, new AbstractMetadataOperationsFactory() { + @Nonnull + @Override + public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, + @Nonnull Options templateProperties) { + final var tableMaybe = Assertions.assertDoesNotThrow(() -> template.findTableByName("T")); + assertThat(tableMaybe).isPresent(); + final var table = Assert.optionalUnchecked(tableMaybe); + assertThat(table.getIndexes().size()).isEqualTo(1); + final var index = Assert.optionalUnchecked(table.getIndexes().stream().findFirst()); + assertThat(index.getIndexType()).isEqualTo(IndexTypes.VALUE); + assertThat(index.getName()).isEqualTo("i1"); + assertThat(index).isInstanceOf(RecordLayerIndex.class); + final var recordLayerIndex = Assert.castUnchecked(index, RecordLayerIndex.class); + assertThat(recordLayerIndex.getKeyExpression()).isEqualTo( + Key.Expressions.concat(Key.Expressions.field("a", KeyExpression.FanType.FanOut).nest("x"), Key.Expressions.field("p"))); + return txn -> { + }; + } + }); + } + + @Test + void createIndexOnAggregate() throws Exception { + final String schemaStatement = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, a bigint, b bigint, c bigint, primary key(p)) " + + "CREATE VIEW mv1 AS SELECT sum(c) as S, a, b from T group by a, b " + + "CREATE INDEX i1 on mv1(S, a, b)"; + + shouldWorkWithInjectedFactory(schemaStatement, new AbstractMetadataOperationsFactory() { + @Nonnull + @Override + public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, + @Nonnull Options templateProperties) { + final var tableMaybe = Assertions.assertDoesNotThrow(() -> template.findTableByName("T")); + assertThat(tableMaybe).isPresent(); + final var table = Assert.optionalUnchecked(tableMaybe); + assertThat(table.getIndexes().size()).isEqualTo(1); + final var index = Assert.optionalUnchecked(table.getIndexes().stream().findFirst()); + assertThat(index.getIndexType()).isEqualTo(IndexTypes.SUM); + assertThat(index.getName()).isEqualTo("i1"); + assertThat(index).isInstanceOf(RecordLayerIndex.class); + final var recordLayerIndex = Assert.castUnchecked(index, RecordLayerIndex.class); + assertThat(recordLayerIndex.getKeyExpression()).isEqualTo( + Key.Expressions.field("c").groupBy(Key.Expressions.concat(Key.Expressions.field("a"), Key.Expressions.field("b")))); + return txn -> { + }; + } + }); + } + @Nonnull private static String makeColumnDefinition(@Nonnull final List columns, boolean isTable) { StringBuilder columnStatement = new StringBuilder("("); diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlTestUtil.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlTestUtil.java index 5697cbe746..0e450d511a 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlTestUtil.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlTestUtil.java @@ -39,6 +39,7 @@ import org.assertj.core.api.Assertions; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.net.URI; import java.sql.SQLException; import java.util.ArrayList; @@ -453,4 +454,51 @@ ParsedType getTable(@Nonnull final String tableName) { return null; // not reachable. } } + + /** + * Enum to represent the different index creation syntaxes. + */ + enum IndexSyntax { + INDEX_AS_SYNTAX, // CREATE INDEX AS SELECT ... FROM ... ORDER BY ... + INDEX_ON_SYNTAX // CREATE INDEX ON table(columns) ... + } + + static class IndexedColumn { + String column; + String order = ""; + String nullsOrder = ""; + + IndexedColumn(String column, @Nullable String order, @Nullable String nullsOrder) { + this.column = column; + this.order = order == null ? "" : " " + order; + this.nullsOrder = nullsOrder == null ? "" : " " + nullsOrder; + } + + IndexedColumn(String column) { + this(column, null, null); + } + + @Override + public String toString() { + return column + order + nullsOrder; + } + } + + @Nonnull + static String generateIndexDdlStatement(@Nonnull final IndexSyntax indexSyntax, @Nonnull final String indexName, + @Nonnull final List indexedColumns, @Nonnull final List includedColumns, + @Nonnull final String tableName) { + final var indexedColumnsString = indexedColumns.stream().map(IndexedColumn::toString).collect(Collectors.joining(",")); + final var includedColumnsString = String.join(",", includedColumns); + if (indexSyntax == IndexSyntax.INDEX_AS_SYNTAX) { + return " CREATE INDEX " + indexName + + " AS SELECT " + indexedColumnsString + (includedColumns.isEmpty() ? "" : ", " + includedColumnsString) + + " FROM " + tableName + + (indexedColumns.size() > 1 || !includedColumns.isEmpty() ? " ORDER BY " + indexedColumnsString : "") + " "; + } else { + return " CREATE INDEX " + indexName + + " ON " + tableName + "(" + indexedColumnsString + ") " + + (includedColumns.isEmpty() ? "" : "INCLUDE (" + includedColumnsString + ")") + " "; + } + } } diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/DeleteRangeNoMetadataKeyTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/DeleteRangeNoMetadataKeyTest.java index d8c45e6515..cd30e2e11a 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/DeleteRangeNoMetadataKeyTest.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/DeleteRangeNoMetadataKeyTest.java @@ -295,8 +295,8 @@ void deleteUsingNonKeyColumns() throws Exception { @Test void testDeleteWithIndexWithSamePrefix() throws Exception { - final String schemaTemplateSuffix = " CREATE INDEX idx1 as select id, a from t1 order by id, a " + - "CREATE INDEX idx2 AS SELECT id, a, e, f FROM t2 ORDER BY id, a, e"; + final String schemaTemplateSuffix = " CREATE INDEX idx1 ON t1(id, a) " + + "CREATE INDEX idx2 ON t2(id, a, e) INCLUDE(f)"; try (var ddl = getDdl(schemaTemplateSuffix)) { try (var stmt = ddl.setSchemaAndGetConnection().createStatement()) { insertData(stmt); diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/DeleteRangeTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/DeleteRangeTest.java index 63654fda98..29f8933c3d 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/DeleteRangeTest.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/DeleteRangeTest.java @@ -220,7 +220,7 @@ void deleteUsingNonKeyColumns() throws Exception { @Test void testDeleteWithIndexWithSamePrefix() throws Exception { - final String schemaTemplate = SCHEMA_TEMPLATE + " CREATE INDEX idx1 as select id, a from t1 order by id, a"; + final String schemaTemplate = SCHEMA_TEMPLATE + " CREATE INDEX idx1 ON t1(id, a)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var stmt = ddl.setSchemaAndGetConnection().createStatement()) { insertData(stmt); @@ -250,7 +250,7 @@ void testDeleteWithIndexWithSamePrefix() throws Exception { @Test void testDeleteWithIndexSamePrefixButDeleteGoesBeyondIndex() throws Exception { - final String schemaTemplate = SCHEMA_TEMPLATE + " CREATE INDEX idx1 as select id from t1"; + final String schemaTemplate = SCHEMA_TEMPLATE + " CREATE INDEX idx1 ON t1(id)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var stmt = ddl.setSchemaAndGetConnection().createStatement()) { insertData(stmt); diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/UniqueIndexTests.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/UniqueIndexTests.java index ad0003cab6..c3474e944c 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/UniqueIndexTests.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/UniqueIndexTests.java @@ -24,6 +24,7 @@ import com.apple.foundationdb.relational.api.EmbeddedRelationalStruct; import com.apple.foundationdb.relational.api.Options; import com.apple.foundationdb.relational.api.RelationalStruct; +import com.apple.foundationdb.relational.api.exceptions.ErrorCode; import com.apple.foundationdb.relational.utils.SimpleDatabaseRule; import org.junit.jupiter.api.Assertions; @@ -86,6 +87,7 @@ private void checkErrorOnNonUniqueInsertionsToTable(@Nonnull List [COUNTRY: VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q347.NAME AS NAME)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c8 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_INCLUDE
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q347> label="q347" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +U + basic_indexFEXPLAIN select name from customers_index_on_table where name = 'Alice' +ה N(0 8@COVERING(IDX_IOT_INCLUDE [EQUALS promote(@c8 AS STRING)] -> [COUNTRY: VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q403.NAME AS NAME)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c8 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_INCLUDE
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q403> label="q403" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +X + basic_indexIEXPLAIN select email from customers_materialized_view order by email desc +Ui D(\08@xCOVERING(IDX_MV_EMAIL <,> -> [EMAIL: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[2]]) | MAP (_.EMAIL AS EMAIL) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q126.EMAIL AS EMAIL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS EMAIL)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_EMAIL
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q126> label="q126" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +U + basic_indexFEXPLAIN select email from customers_index_on_table order by email desc +рWi E(d08@yCOVERING(IDX_IOT_EMAIL <,> -> [EMAIL: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[2]]) | MAP (_.EMAIL AS EMAIL) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.EMAIL AS EMAIL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS EMAIL)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_EMAIL
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q110> label="q110" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +j + basic_index[EXPLAIN select age, city from customers_materialized_view where age > 25 order by age, city +䝟X ƼB(e08&@COVERING(IDX_MV_MULTI [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q141.AGE AS AGE, q141.CITY AS CITY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE, STRING AS CITY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c10 AS INT)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_MULTI
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q141> label="q141" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +g + basic_indexXEXPLAIN select age, city from customers_index_on_table where age > 25 order by age, city +̏Y D(m08&@COVERING(IDX_IOT_MULTI [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q125.AGE AS AGE, q125.CITY AS CITY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE, STRING AS CITY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c10 AS INT)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_MULTI
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q125> label="q125" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +k +include_clauseYEXPLAIN select name, email, country from customers_materialized_view where name = 'Alice' +Ƭg ,(08@COVERING(IDX_MV_INCLUDE [EQUALS promote(@c12 AS STRING)] -> [COUNTRY: VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME, _.EMAIL AS EMAIL, _.COUNTRY AS COUNTRY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q346.NAME AS NAME, q346.EMAIL AS EMAIL, q346.COUNTRY AS COUNTRY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, STRING AS EMAIL, STRING AS COUNTRY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c12 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_INCLUDE
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q346> label="q346" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +h +include_clauseVEXPLAIN select name, email, country from customers_index_on_table where name = 'Alice' + @(08@COVERING(IDX_IOT_INCLUDE [EQUALS promote(@c12 AS STRING)] -> [COUNTRY: VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME, _.EMAIL AS EMAIL, _.COUNTRY AS COUNTRY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q402.NAME AS NAME, q402.EMAIL AS EMAIL, q402.COUNTRY AS COUNTRY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, STRING AS EMAIL, STRING AS COUNTRY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c12 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_INCLUDE
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q402> label="q402" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +v +mixed_asc_descdEXPLAIN select age, city from customers_materialized_view where age > 30 order by age asc, city desc + (e058&@COVERING(IDX_MV_ASC_DESC [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q141.AGE AS AGE, q141.CITY AS CITY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE, STRING AS CITY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c10 AS INT)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_ASC_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q141> label="q141" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +s +mixed_asc_descaEXPLAIN select age, city from customers_index_on_table where age > 30 order by age asc, city desc + (m0/8&@COVERING(IDX_IOT_ASC_DESC [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q125.AGE AS AGE, q125.CITY AS CITY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE, STRING AS CITY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c10 AS INT)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_ASC_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q125> label="q125" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +w +mixed_asc_desceEXPLAIN select age, city from customers_materialized_view where age < 40 order by age desc, city desc + (e068&@COVERING(IDX_MV_MULTI [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q141.AGE AS AGE, q141.CITY AS CITY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE, STRING AS CITY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[LESS_THAN promote(@c10 AS INT)]]
direction: reversed
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_MULTI
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q141> label="q141" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +t +mixed_asc_descbEXPLAIN select age, city from customers_index_on_table where age < 40 order by age desc, city desc + (m028&@COVERING(IDX_IOT_MULTI [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q125.AGE AS AGE, q125.CITY AS CITY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE, STRING AS CITY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[LESS_THAN promote(@c10 AS INT)]]
direction: reversed
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_MULTI
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q125> label="q125" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +v +mixed_asc_descdEXPLAIN select age, city from customers_materialized_view where age < 50 order by age desc, city asc + (e018&@COVERING(IDX_MV_ASC_DESC [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q141.AGE AS AGE, q141.CITY AS CITY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE, STRING AS CITY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[LESS_THAN promote(@c10 AS INT)]]
direction: reversed
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_ASC_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q141> label="q141" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +s +mixed_asc_descaEXPLAIN select age, city from customers_index_on_table where age < 50 order by age desc, city asc + (m018&@COVERING(IDX_IOT_ASC_DESC [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q125.AGE AS AGE, q125.CITY AS CITY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE, STRING AS CITY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[LESS_THAN promote(@c10 AS INT)]]
direction: reversed
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_ASC_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q125> label="q125" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +l +nulls_first_lastXEXPLAIN SELECT country FROM customers_materialized_view ORDER BY country ASC NULLS FIRST +i +(\08@^COVERING(IDX_MV_NULLS_FIRST <,> -> [COUNTRY: KEY[0], ID: KEY[2]]) | MAP (_.COUNTRY AS COUNTRY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q126.COUNTRY AS COUNTRY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_NULLS_FIRST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q126> label="q126" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +i +nulls_first_lastUEXPLAIN SELECT country FROM customers_index_on_table ORDER BY country ASC NULLS FIRST + (t0g8@@ +dCOVERING(IDX_IOT_AGE_MAX_EXTREMUM <,> -> [COUNTRY: KEY[0], ID: KEY[2]]) | MAP (_.COUNTRY AS COUNTRY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q150.COUNTRY AS COUNTRY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_AGE_MAX_EXTREMUM
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q150> label="q150" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +k +nulls_first_lastWEXPLAIN SELECT country FROM customers_materialized_view ORDER BY country ASC NULLS LAST +i (\08@COVERING(IDX_MV_NULLS_LAST <,> -> [COUNTRY: from_ordered_bytes(KEY:[0], ASC_NULLS_LAST), ID: KEY[2]]) | MAP (_.COUNTRY AS COUNTRY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q126.COUNTRY AS COUNTRY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_NULLS_LAST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q126> label="q126" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +h +nulls_first_lastTEXPLAIN SELECT country FROM customers_index_on_table ORDER BY country ASC NULLS LAST +i (d08@COVERING(IDX_IOT_NULLS_LAST <,> -> [COUNTRY: from_ordered_bytes(KEY:[0], ASC_NULLS_LAST), ID: KEY[2]]) | MAP (_.COUNTRY AS COUNTRY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.COUNTRY AS COUNTRY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_NULLS_LAST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q110> label="q110" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +e +nulls_first_lastQEXPLAIN SELECT age FROM customers_materialized_view ORDER BY age DESC NULLS FIRST +i Յ +(\0ˬ8@~COVERING(IDX_MV_DESC_NULLS_FIRST <,> -> [AGE: from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST), ID: KEY[2]]) | MAP (_.AGE AS AGE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q126.AGE AS AGE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_DESC_NULLS_FIRST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q126> label="q126" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +b +nulls_first_lastNEXPLAIN SELECT age FROM customers_index_on_table ORDER BY age DESC NULLS FIRST +i (d08@COVERING(IDX_IOT_DESC_NULLS_FIRST <,> -> [AGE: from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST), ID: KEY[2]]) | MAP (_.AGE AS AGE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.AGE AS AGE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_DESC_NULLS_FIRST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q110> label="q110" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +d +nulls_first_lastPEXPLAIN SELECT age FROM customers_materialized_view ORDER BY age DESC NULLS LAST +ņ (h008.@|COVERING(IDX_MV_DESC_NULLS_LAST <,> -> [AGE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[2]]) | MAP (_.AGE AS AGE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q146.AGE AS AGE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_DESC_NULLS_LAST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q146> label="q146" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +a +nulls_first_lastMEXPLAIN SELECT age FROM customers_index_on_table ORDER BY age DESC NULLS LAST + (p0>8.@}COVERING(IDX_IOT_DESC_NULLS_LAST <,> -> [AGE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[2]]) | MAP (_.AGE AS AGE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q130.AGE AS AGE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_DESC_NULLS_LAST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q130> label="q130" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +#extremum_ever_without_legacy_optionmEXPLAIN SELECT country, min_ever(age) FROM customers_materialized_view WHERE country = 'USA' GROUP BY country +6 Ղ(~0x8K@AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_MV_AGE_MIN_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +#extremum_ever_without_legacy_optionpEXPLAIN SELECT country, max_ever(age) FROM customers_materialized_view WHERE country = 'Canada' GROUP BY country +5 (~0z8K@AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_MV_AGE_MAX_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +#extremum_ever_without_legacy_option|EXPLAIN SELECT country, min_ever(age), max_ever(age) FROM customers_materialized_view WHERE country = 'USA' GROUP BY country + +ƐX <(08y@AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) COMPARE BY () WITH q0, q1 RETURN (q0._0 AS _0, q0._1 AS _1, q1._1 AS _2) | MAP (_._0 AS COUNTRY, _._1 AS _1, _._2 AS _2)digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1, q6._2 AS _2)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1, INT AS _2)" ]; + 2 [ label=<
Intersection
COMPARE BY ()
RESULT (q239._0 AS _0, q239._1 AS _1, q241._1 AS _2)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1, INT AS _2)" ]; + 3 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c18 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 4 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c18 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 5 [ label=<
Index
IDX_MV_AGE_MIN_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 6 [ label=<
Index
IDX_MV_AGE_MAX_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ label=< q239> label="q239" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 2 [ label=< q241> label="q241" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 3 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 6 -> 4 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +#extremum_ever_without_legacy_optionpEXPLAIN SELECT country, min_ever(age) FROM customers_materialized_view WHERE country = 'France' GROUP BY country +6 Ղ(~0x8K@AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_MV_AGE_MIN_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +#extremum_ever_without_legacy_optionpEXPLAIN SELECT country, max_ever(age) FROM customers_materialized_view WHERE country = 'France' GROUP BY country +5 (~0z8K@AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_MV_AGE_MAX_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + + extremum_ever_with_legacy_optionmEXPLAIN SELECT country, min_ever(age) FROM customers_materialized_view WHERE country = 'USA' GROUP BY country +6 Ղ(~0x8K@AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_MV_AGE_MIN_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + + extremum_ever_with_legacy_optionpEXPLAIN SELECT country, max_ever(age) FROM customers_materialized_view WHERE country = 'Canada' GROUP BY country +5 (~0z8K@AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_MV_AGE_MAX_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + + extremum_ever_with_legacy_option|EXPLAIN SELECT country, min_ever(age), max_ever(age) FROM customers_materialized_view WHERE country = 'USA' GROUP BY country + +ƐX <(08y@AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) COMPARE BY () WITH q0, q1 RETURN (q0._0 AS _0, q0._1 AS _1, q1._1 AS _2) | MAP (_._0 AS COUNTRY, _._1 AS _1, _._2 AS _2)digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1, q6._2 AS _2)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1, INT AS _2)" ]; + 2 [ label=<
Intersection
COMPARE BY ()
RESULT (q239._0 AS _0, q239._1 AS _1, q241._1 AS _2)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1, INT AS _2)" ]; + 3 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c18 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 4 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c18 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 5 [ label=<
Index
IDX_MV_AGE_MAX_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 6 [ label=<
Index
IDX_MV_AGE_MIN_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ label=< q241> label="q241" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 2 [ label=< q239> label="q239" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 3 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 6 -> 4 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + + extremum_ever_with_legacy_optionpEXPLAIN SELECT country, min_ever(age) FROM customers_materialized_view WHERE country = 'France' GROUP BY country +6 Ղ(~0x8K@AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_MV_AGE_MIN_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + + extremum_ever_with_legacy_optionpEXPLAIN SELECT country, max_ever(age) FROM customers_materialized_view WHERE country = 'France' GROUP BY country +5 (~0z8K@AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_MV_AGE_MAX_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} \ No newline at end of file diff --git a/yaml-tests/src/test/resources/index-ddl.metrics.yaml b/yaml-tests/src/test/resources/index-ddl.metrics.yaml new file mode 100644 index 0000000000..f0eb42eec3 --- /dev/null +++ b/yaml-tests/src/test/resources/index-ddl.metrics.yaml @@ -0,0 +1,398 @@ +basic_index: +- query: EXPLAIN select name from customers_materialized_view where name = 'Alice' + explain: 'COVERING(IDX_MV_INCLUDE [EQUALS promote(@c8 AS STRING)] -> [COUNTRY: + VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME)' + task_count: 1873 + task_total_time_ms: 351 + transform_count: 469 + transform_time_ms: 207 + transform_yield_count: 173 + insert_time_ms: 20 + insert_new_count: 219 + insert_reused_count: 21 +- query: EXPLAIN select name from customers_index_on_table where name = 'Alice' + explain: 'COVERING(IDX_IOT_INCLUDE [EQUALS promote(@c8 AS STRING)] -> [COUNTRY: + VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME)' + task_count: 2377 + task_total_time_ms: 306 + transform_count: 593 + transform_time_ms: 165 + transform_yield_count: 209 + insert_time_ms: 20 + insert_new_count: 287 + insert_reused_count: 25 +- query: EXPLAIN select email from customers_materialized_view order by email desc + explain: 'COVERING(IDX_MV_EMAIL <,> -> [EMAIL: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), + ID: KEY[2]]) | MAP (_.EMAIL AS EMAIL)' + task_count: 350 + task_total_time_ms: 180 + transform_count: 105 + transform_time_ms: 144 + transform_yield_count: 92 + insert_time_ms: 4 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN select email from customers_index_on_table order by email desc + explain: 'COVERING(IDX_IOT_EMAIL <,> -> [EMAIL: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), + ID: KEY[2]]) | MAP (_.EMAIL AS EMAIL)' + task_count: 366 + task_total_time_ms: 182 + transform_count: 105 + transform_time_ms: 146 + transform_yield_count: 100 + insert_time_ms: 2 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN select age, city from customers_materialized_view where age > 25 + order by age, city + explain: 'COVERING(IDX_MV_MULTI [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: + KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)' + task_count: 504 + task_total_time_ms: 185 + transform_count: 141 + transform_time_ms: 139 + transform_yield_count: 101 + insert_time_ms: 7 + insert_new_count: 38 + insert_reused_count: 2 +- query: EXPLAIN select age, city from customers_index_on_table where age > 25 order + by age, city + explain: 'COVERING(IDX_IOT_MULTI [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: + KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)' + task_count: 520 + task_total_time_ms: 188 + transform_count: 141 + transform_time_ms: 143 + transform_yield_count: 109 + insert_time_ms: 6 + insert_new_count: 38 + insert_reused_count: 2 +include_clause: +- query: EXPLAIN select name, email, country from customers_materialized_view where + name = 'Alice' + explain: 'COVERING(IDX_MV_INCLUDE [EQUALS promote(@c12 AS STRING)] -> [COUNTRY: + VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME, + _.EMAIL AS EMAIL, _.COUNTRY AS COUNTRY)' + task_count: 1814 + task_total_time_ms: 216 + transform_count: 465 + transform_time_ms: 93 + transform_yield_count: 169 + insert_time_ms: 11 + insert_new_count: 213 + insert_reused_count: 21 +- query: EXPLAIN select name, email, country from customers_index_on_table where + name = 'Alice' + explain: 'COVERING(IDX_IOT_INCLUDE [EQUALS promote(@c12 AS STRING)] -> [COUNTRY: + VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME, + _.EMAIL AS EMAIL, _.COUNTRY AS COUNTRY)' + task_count: 2318 + task_total_time_ms: 294 + transform_count: 589 + transform_time_ms: 136 + transform_yield_count: 205 + insert_time_ms: 15 + insert_new_count: 281 + insert_reused_count: 25 +mixed_asc_desc: +- query: EXPLAIN select age, city from customers_materialized_view where age > 30 + order by age asc, city desc + explain: 'COVERING(IDX_MV_ASC_DESC [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: + KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | + MAP (_.AGE AS AGE, _.CITY AS CITY)' + task_count: 504 + task_total_time_ms: 62 + transform_count: 141 + transform_time_ms: 36 + transform_yield_count: 101 + insert_time_ms: 0 + insert_new_count: 38 + insert_reused_count: 2 +- query: EXPLAIN select age, city from customers_index_on_table where age > 30 order + by age asc, city desc + explain: 'COVERING(IDX_IOT_ASC_DESC [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: + KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | + MAP (_.AGE AS AGE, _.CITY AS CITY)' + task_count: 520 + task_total_time_ms: 64 + transform_count: 141 + transform_time_ms: 39 + transform_yield_count: 109 + insert_time_ms: 0 + insert_new_count: 38 + insert_reused_count: 2 +- query: EXPLAIN select age, city from customers_materialized_view where age < 40 + order by age desc, city desc + explain: 'COVERING(IDX_MV_MULTI [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> + [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)' + task_count: 504 + task_total_time_ms: 59 + transform_count: 141 + transform_time_ms: 35 + transform_yield_count: 101 + insert_time_ms: 0 + insert_new_count: 38 + insert_reused_count: 2 +- query: EXPLAIN select age, city from customers_index_on_table where age < 40 order + by age desc, city desc + explain: 'COVERING(IDX_IOT_MULTI [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> + [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)' + task_count: 520 + task_total_time_ms: 64 + transform_count: 141 + transform_time_ms: 40 + transform_yield_count: 109 + insert_time_ms: 0 + insert_new_count: 38 + insert_reused_count: 2 +- query: EXPLAIN select age, city from customers_materialized_view where age < 50 + order by age desc, city asc + explain: 'COVERING(IDX_MV_ASC_DESC [[LESS_THAN promote(@c10 AS INT)]] REVERSE + -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) + | MAP (_.AGE AS AGE, _.CITY AS CITY)' + task_count: 504 + task_total_time_ms: 63 + transform_count: 141 + transform_time_ms: 36 + transform_yield_count: 101 + insert_time_ms: 0 + insert_new_count: 38 + insert_reused_count: 2 +- query: EXPLAIN select age, city from customers_index_on_table where age < 50 order + by age desc, city asc + explain: 'COVERING(IDX_IOT_ASC_DESC [[LESS_THAN promote(@c10 AS INT)]] REVERSE + -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) + | MAP (_.AGE AS AGE, _.CITY AS CITY)' + task_count: 520 + task_total_time_ms: 65 + transform_count: 141 + transform_time_ms: 39 + transform_yield_count: 109 + insert_time_ms: 0 + insert_new_count: 38 + insert_reused_count: 2 +nulls_first_last: +- query: EXPLAIN SELECT country FROM customers_materialized_view ORDER BY country + ASC NULLS FIRST + explain: 'COVERING(IDX_MV_NULLS_FIRST <,> -> [COUNTRY: KEY[0], ID: KEY[2]]) | + MAP (_.COUNTRY AS COUNTRY)' + task_count: 350 + task_total_time_ms: 33 + transform_count: 105 + transform_time_ms: 21 + transform_yield_count: 92 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN SELECT country FROM customers_index_on_table ORDER BY country ASC + NULLS FIRST + explain: 'COVERING(IDX_IOT_AGE_MAX_EXTREMUM <,> -> [COUNTRY: KEY[0], ID: KEY[2]]) + | MAP (_.COUNTRY AS COUNTRY)' + task_count: 618 + task_total_time_ms: 65 + transform_count: 149 + transform_time_ms: 37 + transform_yield_count: 116 + insert_time_ms: 1 + insert_new_count: 64 + insert_reused_count: 10 +- query: EXPLAIN SELECT country FROM customers_materialized_view ORDER BY country + ASC NULLS LAST + explain: 'COVERING(IDX_MV_NULLS_LAST <,> -> [COUNTRY: from_ordered_bytes(KEY:[0], + ASC_NULLS_LAST), ID: KEY[2]]) | MAP (_.COUNTRY AS COUNTRY)' + task_count: 350 + task_total_time_ms: 37 + transform_count: 105 + transform_time_ms: 23 + transform_yield_count: 92 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN SELECT country FROM customers_index_on_table ORDER BY country ASC + NULLS LAST + explain: 'COVERING(IDX_IOT_NULLS_LAST <,> -> [COUNTRY: from_ordered_bytes(KEY:[0], + ASC_NULLS_LAST), ID: KEY[2]]) | MAP (_.COUNTRY AS COUNTRY)' + task_count: 366 + task_total_time_ms: 39 + transform_count: 105 + transform_time_ms: 24 + transform_yield_count: 100 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN SELECT age FROM customers_materialized_view ORDER BY age DESC NULLS + FIRST + explain: 'COVERING(IDX_MV_DESC_NULLS_FIRST <,> -> [AGE: from_ordered_bytes(KEY:[0], + DESC_NULLS_FIRST), ID: KEY[2]]) | MAP (_.AGE AS AGE)' + task_count: 350 + task_total_time_ms: 33 + transform_count: 105 + transform_time_ms: 21 + transform_yield_count: 92 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN SELECT age FROM customers_index_on_table ORDER BY age DESC NULLS + FIRST + explain: 'COVERING(IDX_IOT_DESC_NULLS_FIRST <,> -> [AGE: from_ordered_bytes(KEY:[0], + DESC_NULLS_FIRST), ID: KEY[2]]) | MAP (_.AGE AS AGE)' + task_count: 366 + task_total_time_ms: 37 + transform_count: 105 + transform_time_ms: 24 + transform_yield_count: 100 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN SELECT age FROM customers_materialized_view ORDER BY age DESC NULLS + LAST + explain: 'COVERING(IDX_MV_DESC_NULLS_LAST <,> -> [AGE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST), ID: KEY[2]]) | MAP (_.AGE AS AGE)' + task_count: 516 + task_total_time_ms: 35 + transform_count: 133 + transform_time_ms: 20 + transform_yield_count: 104 + insert_time_ms: 0 + insert_new_count: 46 + insert_reused_count: 6 +- query: EXPLAIN SELECT age FROM customers_index_on_table ORDER BY age DESC NULLS + LAST + explain: 'COVERING(IDX_IOT_DESC_NULLS_LAST <,> -> [AGE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST), ID: KEY[2]]) | MAP (_.AGE AS AGE)' + task_count: 532 + task_total_time_ms: 49 + transform_count: 133 + transform_time_ms: 28 + transform_yield_count: 112 + insert_time_ms: 1 + insert_new_count: 46 + insert_reused_count: 6 +extremum_ever_without_legacy_option: +- query: EXPLAIN SELECT country, min_ever(age) FROM customers_materialized_view + WHERE country = 'USA' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 113 + transform_count: 257 + transform_time_ms: 66 + transform_yield_count: 126 + insert_time_ms: 1 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, max_ever(age) FROM customers_materialized_view + WHERE country = 'Canada' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 113 + transform_count: 257 + transform_time_ms: 66 + transform_yield_count: 126 + insert_time_ms: 1 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, min_ever(age), max_ever(age) FROM customers_materialized_view + WHERE country = 'USA' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS + promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) COMPARE + BY () WITH q0, q1 RETURN (q0._0 AS _0, q0._1 AS _1, q1._1 AS _2) | MAP (_._0 + AS COUNTRY, _._1 AS _1, _._2 AS _2)' + task_count: 1289 + task_total_time_ms: 184 + transform_count: 347 + transform_time_ms: 126 + transform_yield_count: 146 + insert_time_ms: 5 + insert_new_count: 121 + insert_reused_count: 6 +- query: EXPLAIN SELECT country, min_ever(age) FROM customers_materialized_view + WHERE country = 'France' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 113 + transform_count: 257 + transform_time_ms: 66 + transform_yield_count: 126 + insert_time_ms: 1 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, max_ever(age) FROM customers_materialized_view + WHERE country = 'France' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 113 + transform_count: 257 + transform_time_ms: 66 + transform_yield_count: 126 + insert_time_ms: 1 + insert_new_count: 75 + insert_reused_count: 4 +extremum_ever_with_legacy_option: +- query: EXPLAIN SELECT country, min_ever(age) FROM customers_materialized_view + WHERE country = 'USA' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 113 + transform_count: 257 + transform_time_ms: 66 + transform_yield_count: 126 + insert_time_ms: 1 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, max_ever(age) FROM customers_materialized_view + WHERE country = 'Canada' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 113 + transform_count: 257 + transform_time_ms: 66 + transform_yield_count: 126 + insert_time_ms: 1 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, min_ever(age), max_ever(age) FROM customers_materialized_view + WHERE country = 'USA' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS + promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) COMPARE + BY () WITH q0, q1 RETURN (q0._0 AS _0, q0._1 AS _1, q1._1 AS _2) | MAP (_._0 + AS COUNTRY, _._1 AS _1, _._2 AS _2)' + task_count: 1289 + task_total_time_ms: 184 + transform_count: 347 + transform_time_ms: 126 + transform_yield_count: 146 + insert_time_ms: 5 + insert_new_count: 121 + insert_reused_count: 6 +- query: EXPLAIN SELECT country, min_ever(age) FROM customers_materialized_view + WHERE country = 'France' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 113 + transform_count: 257 + transform_time_ms: 66 + transform_yield_count: 126 + insert_time_ms: 1 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, max_ever(age) FROM customers_materialized_view + WHERE country = 'France' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 113 + transform_count: 257 + transform_time_ms: 66 + transform_yield_count: 126 + insert_time_ms: 1 + insert_new_count: 75 + insert_reused_count: 4 diff --git a/yaml-tests/src/test/resources/index-ddl.yamsql b/yaml-tests/src/test/resources/index-ddl.yamsql new file mode 100644 index 0000000000..e778d270fb --- /dev/null +++ b/yaml-tests/src/test/resources/index-ddl.yamsql @@ -0,0 +1,348 @@ +# +# alternate-index-syntax.yamsql +# +# This source file is part of the FoundationDB open source project +# +# Copyright 2021-2025 Apple Inc. and the FoundationDB project authors +# +# Licensed 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. + +# Test alternate CREATE INDEX ON syntax vs traditional CREATE INDEX AS SELECT +# Both syntaxes should produce equivalent query plans +--- +options: + supported_version: !current_version +--- +schema_template: | + # Table with indexes defined via materialized view + create table customers_materialized_view( + id integer, + name string, + email string, + age integer, + city string, + country string, + profession string, + primary key(id) + ) + + create index idx_mv_name as select name from customers_materialized_view order by name + create index idx_mv_email as select email from customers_materialized_view order by email desc + create index idx_mv_multi as select age, city from customers_materialized_view order by age asc, city asc + create index idx_mv_asc_desc as select age, city from customers_materialized_view order by age asc, city desc + create index idx_mv_include as select name, email, country from customers_materialized_view order by name + + # NULLS FIRST/LAST ordering + create index idx_mv_nulls_first as select country from customers_materialized_view order by country asc nulls first + create index idx_mv_nulls_last as select country from customers_materialized_view order by country asc nulls last + create index idx_mv_desc_nulls_first as select age from customers_materialized_view order by age desc nulls first + create index idx_mv_desc_nulls_last as select age from customers_materialized_view order by age desc nulls last + + # Identical table with CREATE INDEX ON syntax (index on table style) + create table customers_index_on_table( + id integer, + name string, + email string, + age integer, + city string, + country string, + profession string, + primary key(id) + ) + + # CREATE INDEX ON syntax (index on table style) + create index idx_iot_name on customers_index_on_table(name) + create index idx_iot_email on customers_index_on_table(email desc) + create index idx_iot_multi on customers_index_on_table(age, city) + create index idx_iot_asc_desc on customers_index_on_table(age, city desc) + create index idx_iot_include on customers_index_on_table(name) include(email, country) + + # Test NULLS FIRST/LAST ordering + create index idx_iot_nulls_first on customers_index_on_table(country asc nulls first) + create index idx_iot_nulls_last on customers_index_on_table(country asc nulls last) + create index idx_iot_desc_nulls_first on customers_index_on_table(age desc nulls first) + create index idx_iot_desc_nulls_last on customers_index_on_table(age desc nulls last) + + # Test UNIQUE indexes on profession column + create unique index idx_mv_unique_profession as select profession from customers_materialized_view order by profession + create unique index idx_iot_unique_profession on customers_index_on_table(profession) + + # Test MIN_EVER/MAX_EVER aggregations WITHOUT legacy_extremum_ever option + # Legacy index style (AS SELECT) WITHOUT ATTRIBUTES + create index idx_mv_age_min_no_legacy as select min_ever(age) from customers_materialized_view group by country + create index idx_mv_age_max_no_legacy as select max_ever(age) from customers_materialized_view group by country + + # New index style (ON view) WITHOUT OPTIONS + # First create views that define the aggregates + create view view_iot_age_min_no_legacy as select country, min_ever(age) as min_age from customers_index_on_table group by country + create view view_iot_age_max_no_legacy as select country, max_ever(age) as max_age from customers_index_on_table group by country + + # Then create indexes on those views without any options + create index idx_iot_age_min_no_legacy on view_iot_age_min_no_legacy(country) + create index idx_iot_age_max_no_legacy on view_iot_age_max_no_legacy(country) + + # Test MIN_EVER/MAX_EVER aggregations WITH legacy_extremum_ever option + # Legacy index style (AS SELECT) with ATTRIBUTES and GROUP BY + create index idx_mv_age_min_extremum as select min_ever(age) from customers_materialized_view group by country with attributes legacy_extremum_ever + create index idx_mv_age_max_extremum as select max_ever(age) from customers_materialized_view group by country with attributes legacy_extremum_ever + + # New index style (ON view) with OPTIONS + # First create views that define the aggregates + create view view_iot_age_min as select country, min_ever(age) as min_age from customers_index_on_table group by country + create view view_iot_age_max as select country, max_ever(age) as max_age from customers_index_on_table group by country + + # Then create indexes on those views with legacy_extremum_ever option + create index idx_iot_age_min_extremum on view_iot_age_min(country) options (legacy_extremum_ever) + create index idx_iot_age_max_extremum on view_iot_age_max(country) options (legacy_extremum_ever) +--- +setup: + steps: + # Insert test data including NULL values to test NULLS ordering + # Each table gets different profession values to test UNIQUE constraints independently + - query: INSERT INTO customers_materialized_view VALUES + (1, 'Alice', 'alice@example.com', 25, 'New York', 'USA', 'Engineer'), + (2, 'Bob', 'bob@example.com', NULL, 'London', NULL, 'Designer'), + (3, 'Charlie', NULL, 35, 'Paris', 'France', 'Manager'), + (4, NULL, 'null@example.com', 30, NULL, 'Canada', 'Analyst') + + - query: INSERT INTO customers_index_on_table VALUES + (1, 'Alice', 'alice@example.com', 25, 'New York', 'USA', 'Engineer'), + (2, 'Bob', 'bob@example.com', NULL, 'London', NULL, 'Designer'), + (3, 'Charlie', NULL, 35, 'Paris', 'France', 'Manager'), + (4, NULL, 'null@example.com', 30, NULL, 'Canada', 'Analyst') + +--- +test_block: + name: basic_index + tests: + # Test basic name index - plans should use respective indexes + - + - query: select name from customers_materialized_view where name = 'Alice' + - explain: "COVERING(IDX_MV_INCLUDE [EQUALS promote(@c8 AS STRING)] -> [COUNTRY: VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME)" + - result: [{ Alice }] + - + - query: select name from customers_index_on_table where name = 'Alice' + - explain: "COVERING(IDX_IOT_INCLUDE [EQUALS promote(@c8 AS STRING)] -> [COUNTRY: VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME)" + - result: [{ Alice }] + + # Test descending email index + - + - query: select email from customers_materialized_view order by email desc + - explain: "COVERING(IDX_MV_EMAIL <,> -> [EMAIL: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[2]]) | MAP (_.EMAIL AS EMAIL)" + - result: [{ null@example.com }, { bob@example.com }, { alice@example.com }, { !null }] + - + - query: select email from customers_index_on_table order by email desc + - explain: "COVERING(IDX_IOT_EMAIL <,> -> [EMAIL: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[2]]) | MAP (_.EMAIL AS EMAIL)" + - result: [{ null@example.com }, { bob@example.com }, { alice@example.com }, { !null }] + + # Test multi-column index - should use correct indexes now + - + - query: select age, city from customers_materialized_view where age > 25 order by age, city + - explain: "COVERING(IDX_MV_MULTI [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)" + - result: [{ 30, !null }, { 35, Paris }] + - + - query: select age, city from customers_index_on_table where age > 25 order by age, city + - explain: "COVERING(IDX_IOT_MULTI [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)" + - result: [{ 30, !null }, { 35, Paris }] + +--- +test_block: + name: include_clause + tests: + - + - query: select name, email, country from customers_materialized_view where name = 'Alice' + - explain: "COVERING(IDX_MV_INCLUDE [EQUALS promote(@c12 AS STRING)] -> [COUNTRY: VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME, _.EMAIL AS EMAIL, _.COUNTRY AS COUNTRY)" + - result: [{ Alice, alice@example.com, USA }] + - + - query: select name, email, country from customers_index_on_table where name = 'Alice' + - explain: "COVERING(IDX_IOT_INCLUDE [EQUALS promote(@c12 AS STRING)] -> [COUNTRY: VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME, _.EMAIL AS EMAIL, _.COUNTRY AS COUNTRY)" + - result: [{ Alice, alice@example.com, USA }] + +--- +test_block: + name: mixed_asc_desc + tests: + # Test mixed ASC/DESC ordering (age ASC, city DESC) + - + - query: select age, city from customers_materialized_view where age > 30 order by age asc, city desc + - explain: "COVERING(IDX_MV_ASC_DESC [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)" + - result: [{ 35, Paris }] + - + - query: select age, city from customers_index_on_table where age > 30 order by age asc, city desc + - explain: "COVERING(IDX_IOT_ASC_DESC [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)" + - result: [{ 35, Paris }] + + # Test DESC DESC ordering (should use REVERSE on ASC ASC index) + - + - query: select age, city from customers_materialized_view where age < 40 order by age desc, city desc + - explain: "COVERING(IDX_MV_MULTI [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)" + - result: [{ 35, Paris }, { 30, !null }, { 25, New York }] + - + - query: select age, city from customers_index_on_table where age < 40 order by age desc, city desc + - explain: "COVERING(IDX_IOT_MULTI [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)" + - result: [{ 35, Paris }, { 30, !null }, { 25, New York }] + + # Test DESC ASC ordering (should use REVERSE on ASC DESC index) + - + - query: select age, city from customers_materialized_view where age < 50 order by age desc, city asc + - explain: "COVERING(IDX_MV_ASC_DESC [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)" + - result: [{ 35, Paris }, { 30, !null }, { 25, New York }] + - + - query: select age, city from customers_index_on_table where age < 50 order by age desc, city asc + - explain: "COVERING(IDX_IOT_ASC_DESC [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)" + - result: [{ 35, Paris }, { 30, !null }, { 25, New York }] + +--- +test_block: + name: nulls_first_last + tests: + # Test NULLS FIRST/LAST ordering with actual results and index usage + - + - query: SELECT country FROM customers_materialized_view ORDER BY country ASC NULLS FIRST + - explain: "COVERING(IDX_MV_NULLS_FIRST <,> -> [COUNTRY: KEY[0], ID: KEY[2]]) | MAP (_.COUNTRY AS COUNTRY)" + - result: [{ !null }, { Canada }, { France }, { USA }] + + - + - query: SELECT country FROM customers_index_on_table ORDER BY country ASC NULLS FIRST + - explain: "COVERING(IDX_IOT_AGE_MAX_EXTREMUM <,> -> [COUNTRY: KEY[0], ID: KEY[2]]) | MAP (_.COUNTRY AS COUNTRY)" + - result: [{ !null }, { Canada }, { France }, { USA }] + + - + - query: SELECT country FROM customers_materialized_view ORDER BY country ASC NULLS LAST + - explain: "COVERING(IDX_MV_NULLS_LAST <,> -> [COUNTRY: from_ordered_bytes(KEY:[0], ASC_NULLS_LAST), ID: KEY[2]]) | MAP (_.COUNTRY AS COUNTRY)" + - result: [{ Canada }, { France }, { USA }, { !null }] + + - + - query: SELECT country FROM customers_index_on_table ORDER BY country ASC NULLS LAST + - explain: "COVERING(IDX_IOT_NULLS_LAST <,> -> [COUNTRY: from_ordered_bytes(KEY:[0], ASC_NULLS_LAST), ID: KEY[2]]) | MAP (_.COUNTRY AS COUNTRY)" + - result: [{ Canada }, { France }, { USA }, { !null }] + + - + - query: SELECT age FROM customers_materialized_view ORDER BY age DESC NULLS FIRST + - explain: "COVERING(IDX_MV_DESC_NULLS_FIRST <,> -> [AGE: from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST), ID: KEY[2]]) | MAP (_.AGE AS AGE)" + - result: [{ !null }, { 35 }, { 30 }, { 25 }] + + - + - query: SELECT age FROM customers_index_on_table ORDER BY age DESC NULLS FIRST + - explain: "COVERING(IDX_IOT_DESC_NULLS_FIRST <,> -> [AGE: from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST), ID: KEY[2]]) | MAP (_.AGE AS AGE)" + - result: [{ !null }, { 35 }, { 30 }, { 25 }] + + - + - query: SELECT age FROM customers_materialized_view ORDER BY age DESC NULLS LAST + - explain: "COVERING(IDX_MV_DESC_NULLS_LAST <,> -> [AGE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[2]]) | MAP (_.AGE AS AGE)" + - result: [{ 35 }, { 30 }, { 25 }, { !null }] + + - + - query: SELECT age FROM customers_index_on_table ORDER BY age DESC NULLS LAST + - explain: "COVERING(IDX_IOT_DESC_NULLS_LAST <,> -> [AGE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[2]]) | MAP (_.AGE AS AGE)" + - result: [{ 35 }, { 30 }, { 25 }, { !null }] + +--- +test_block: + name: unique_constraint + preset: single_repetition_ordered + tests: + # Test UNIQUE constraint violations on profession column - should fail when attempting to insert duplicate professions + - + - query: INSERT INTO customers_materialized_view VALUES (5, 'David', 'david@example.com', 28, 'Boston', 'USA', 'Engineer') + - error: "23505" + + - + - query: INSERT INTO customers_index_on_table VALUES (5, 'Eve', 'eve@example.com', 32, 'Seattle', 'USA', 'Engineer') + - error: "23505" + + # Test that different professions still work + - + - query: INSERT INTO customers_materialized_view VALUES (5, 'David', 'david@example.com', 28, 'Boston', 'USA', 'Scientist') + - count: 1 + + - + - query: INSERT INTO customers_index_on_table VALUES (5, 'Eve', 'eve@example.com', 32, 'Seattle', 'USA', 'Architect') + - count: 1 + +--- +test_block: + name: extremum_ever_without_legacy_option + tests: + # Test MIN_EVER and MAX_EVER WITHOUT legacy_extremum_ever option + # These tests verify that aggregate indexes work correctly without the legacy option + # Using legacy index style (AS SELECT) without attributes + + # Test MIN_EVER with specific country filter + - + - query: SELECT country, min_ever(age) FROM customers_materialized_view WHERE country = 'USA' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{USA, 25}] + + # Test MAX_EVER with specific country filter + - + - query: SELECT country, max_ever(age) FROM customers_materialized_view WHERE country = 'Canada' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{Canada, 30}] + + # Test combined MIN_EVER and MAX_EVER in same query + - + - query: SELECT country, min_ever(age), max_ever(age) FROM customers_materialized_view WHERE country = 'USA' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) COMPARE BY () WITH q0, q1 RETURN (q0._0 AS _0, q0._1 AS _1, q1._1 AS _2) | MAP (_._0 AS COUNTRY, _._1 AS _1, _._2 AS _2)" + - result: [{USA, 25, 28}] + + # Test with France to verify different data point + - + - query: SELECT country, min_ever(age) FROM customers_materialized_view WHERE country = 'France' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{France, 35}] + + # Test MAX_EVER with France + - + - query: SELECT country, max_ever(age) FROM customers_materialized_view WHERE country = 'France' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{France, 35}] + +--- +test_block: + name: extremum_ever_with_legacy_option + tests: + # Test MIN_EVER and MAX_EVER WITH legacy_extremum_ever option + # These tests verify that aggregate indexes created with LEGACY_EXTREMUM_EVER work correctly + # Using legacy index style (AS SELECT with ATTRIBUTES) + + # Test MIN_EVER with specific country filter + - + - query: SELECT country, min_ever(age) FROM customers_materialized_view WHERE country = 'USA' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{USA, 25}] + + # Test MAX_EVER with specific country filter + - + - query: SELECT country, max_ever(age) FROM customers_materialized_view WHERE country = 'Canada' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{Canada, 30}] + + # Test combined MIN_EVER and MAX_EVER in same query + - + - query: SELECT country, min_ever(age), max_ever(age) FROM customers_materialized_view WHERE country = 'USA' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) COMPARE BY () WITH q0, q1 RETURN (q0._0 AS _0, q0._1 AS _1, q1._1 AS _2) | MAP (_._0 AS COUNTRY, _._1 AS _1, _._2 AS _2)" + - result: [{USA, 25, 28}] + + # Test with France to verify different data point + - + - query: SELECT country, min_ever(age) FROM customers_materialized_view WHERE country = 'France' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{France, 35}] + + # Test MAX_EVER with France + - + - query: SELECT country, max_ever(age) FROM customers_materialized_view WHERE country = 'France' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{France, 35}] +...