From ef4c0df7d63ddfdf0bdde93177211f5d4cdabf8c Mon Sep 17 00:00:00 2001 From: Youssef Hatem Date: Fri, 14 Nov 2025 09:20:52 +0000 Subject: [PATCH 1/7] Support new index definition syntax. --- .../GenerateVisitorAnnotationHelper.java | 119 +++++- .../plan/IndexKeyValueToPartialRecord.java | 3 + .../query/plan/cascades/values/Value.java | 27 ++ .../EmbeddedRelationalBenchmark.java | 4 +- .../src/main/antlr/RelationalLexer.g4 | 9 + .../src/main/antlr/RelationalParser.g4 | 56 ++- .../AbstractEmbeddedStatement.java | 11 +- .../metadata/RecordLayerIndex.java | 20 + .../recordlayer/query/ParseHelpers.java | 16 +- .../MaterializedViewIndexGenerator.java} | 21 +- .../query/ddl/OnSourceIndexGenerator.java | 270 ++++++++++++ .../recordlayer/query/ddl/package-info.java | 25 ++ .../query/visitors/BaseVisitor.java | 33 +- .../query/visitors/DdlVisitor.java | 96 ++++- .../query/visitors/DelegatingVisitor.java | 70 ++- .../query/visitors/ExpressionVisitor.java | 4 +- .../query/visitors/QueryVisitor.java | 4 +- .../query/visitors/TypedVisitor.java | 18 +- .../recordlayer/util/ExceptionUtil.java | 3 +- .../api/ddl/DdlStatementParsingTest.java | 352 +++++++++++++-- .../relational/api/ddl/DdlTestUtil.java | 48 +++ .../relational/api/ddl/IndexTest.java | 102 ++++- .../DeleteRangeNoMetadataKeyTest.java | 4 +- .../recordlayer/DeleteRangeTest.java | 4 +- .../recordlayer/UniqueIndexTests.java | 2 + .../recordlayer/query/CountQueryTest.java | 2 +- .../recordlayer/query/ExplainTests.java | 4 +- .../recordlayer/query/GroupByQueryTests.java | 44 +- .../query/PreparedStatementTests.java | 4 +- .../query/QueryWithContinuationTest.java | 4 +- .../recordlayer/query/StandardQueryTests.java | 8 +- .../relational/utils/TestSchemas.java | 2 +- .../src/test/java/YamlIntegrationTests.java | 10 + .../test/resources/index-ddl.metrics.binpb | 402 ++++++++++++++++++ .../src/test/resources/index-ddl.metrics.yaml | 398 +++++++++++++++++ .../src/test/resources/index-ddl.yamsql | 331 ++++++++++++++ 36 files changed, 2385 insertions(+), 145 deletions(-) rename fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/{IndexGenerator.java => ddl/MaterializedViewIndexGenerator.java} (97%) create mode 100644 fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/OnSourceIndexGenerator.java create mode 100644 fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/package-info.java create mode 100644 yaml-tests/src/test/resources/index-ddl.metrics.binpb create mode 100644 yaml-tests/src/test/resources/index-ddl.metrics.yaml create mode 100644 yaml-tests/src/test/resources/index-ddl.yamsql 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/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..aba20fe2c1 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 @@ -309,6 +311,31 @@ default Value translateCorrelations(@Nonnull final TranslationMap translationMap }, false).orElseThrow(() -> new RecordCoreException("unable to map tree")); } + @Nonnull + @SuppressWarnings("PMD.CompareObjectsWithEquals") + default Value translateCorrelationsRecursively(@Nonnull final TranslationMap translationMap) { + if (translationMap.definesOnlyIdentities()) { + return this; + } + return replaceLeavesMaybe(value -> { + if (value instanceof LeafValue) { + final var leafValue = (LeafValue)value; + final var correlatedTo = value.getCorrelatedTo(); + if (correlatedTo.isEmpty()) { + return leafValue; + } + + Verify.verify(correlatedTo.size() == 1); + final var sourceAlias = Iterables.getOnlyElement(correlatedTo); + return translationMap.containsSourceAlias(sourceAlias) + ? translationMap.applyTranslationFunction(sourceAlias, leafValue) + : leafValue; + } + Verify.verify(value.getCorrelatedTo().isEmpty()); + return value; + }, true).orElseThrow(() -> new RecordCoreException("unable to map tree")); + } + @Nonnull default V narrow(@Nonnull Class narrowedClass) { return narrowedClass.cast(this); 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 8460fdd8e4..6be6c43703 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 @@ -408,7 +450,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 @@ -1099,10 +1146,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..9bc4f5e0c1 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 @@ -113,6 +113,17 @@ public Map getOptions() { return options; } + @Nonnull + public Builder toBuilder() { + return newBuilder().setName(getName()) + .setIndexType(getIndexType()) + .setTableName(getTableName()) + .setUnique(isUnique()) + .setKeyExpression(getKeyExpression()) + .setPredicate(getPredicate()) + .setOptions(getOptions()); + } + @Nonnull public static RecordLayerIndex from(@Nonnull final String tableName, @Nonnull final com.apple.foundationdb.record.metadata.Index index) { final var indexProto = index.toProto(); @@ -200,6 +211,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/IndexGenerator.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/MaterializedViewIndexGenerator.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/MaterializedViewIndexGenerator.java index 14c10f53c5..17b85a1fd6 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/MaterializedViewIndexGenerator.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 MaterializedViewIndexGenerator { 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 MaterializedViewIndexGenerator(@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 keyColumns; + + @Nonnull + private final List valueColumns; + + @Nonnull + private final LogicalPlanFragment source; + + private final boolean isUnique; + + private final boolean useLegacyExtremum; + + private final boolean useNullableArrays; + + public OnSourceIndexGenerator(@Nonnull final Identifier indexName, @Nonnull final LogicalPlanFragment source, + @Nonnull final List keyColumns, @Nonnull final List valueColumns, + final boolean isUnique, + final boolean useLegacyExtremum, final boolean useNullableArrays) { + this.indexName = indexName; + this.source = source; + this.keyColumns = ImmutableList.copyOf(keyColumns); + this.valueColumns = ImmutableList.copyOf(valueColumns); + this.isUnique = isUnique; + this.useLegacyExtremum = useLegacyExtremum; + this.useNullableArrays = useNullableArrays; + } + + @Nonnull + public RecordLayerIndex generate(@Nonnull final RecordLayerSchemaTemplate catalog) { + final var keyIdentifiers = keyColumns.stream().map(IndexedColumn::getIdentifier).collect(ImmutableList.toImmutableList()); + final var keyIdentifiersAsSet = ImmutableSet.copyOf(keyIdentifiers); + final var valueIdentifiers = valueColumns.stream().map(IndexedColumn::getIdentifier) + .filter(id -> !keyIdentifiersAsSet.contains(id)).collect(ImmutableList.toImmutableList()); + + final var topLevelOperator = Iterables.getOnlyElement(source.getLogicalOperators()); + var topLevelSelect = topLevelOperator.getQuantifier().getRangesOver().get(); + Assert.thatUnchecked(topLevelSelect instanceof SelectExpression); + + final var resultValue = topLevelOperator.getQuantifier().getRangesOver().get().getResultValue(); + final Map> originalOutputMap = topLevelOperator.getOutput().stream() + .filter(e -> e.getName().isPresent()) + .collect(Collectors.toUnmodifiableMap( + expression -> expression.getName().get(), + expression -> { + final var name = expression.getName().map(Identifier::getName); + final var value = ImmutableList.of(expression.getUnderlying()); + final var pushedDownValue = resultValue.pushDown(value, DefaultValueSimplificationRuleSet.instance(), + EvaluationContext.empty(), AliasMap.emptyMap(), ImmutableSet.of(), topLevelOperator.getQuantifier().getAlias()).get(0); + return Column.of(name, pushedDownValue); + })); + + final List> projectionCols = ImmutableList.builder().addAll(keyIdentifiers).addAll(valueIdentifiers) + .build().stream().map(identifier -> { + final var column = originalOutputMap.get(identifier); + Assert.notNullUnchecked(column, ErrorCode.UNDEFINED_COLUMN, () -> "could not find " + identifier); + return column; + }).collect(ImmutableList.toImmutableList()); + + final var orderByExpressions = keyColumns.stream().map(keyColumn -> { + final var column = originalOutputMap.get(keyColumn.identifier); + Assert.notNullUnchecked(column, ErrorCode.UNDEFINED_COLUMN, () -> "could not find " + keyColumn.identifier); + return OrderByExpression.of(Expression.fromColumn(column), keyColumn.isDescending, keyColumn.isNullsLast); + }).collect(ImmutableList.toImmutableList()); + + + final var originalSelectExpression = (SelectExpression)topLevelSelect; + final var newSelectExpression = GraphExpansion.builder().addAllQuantifiers(originalSelectExpression.getQuantifiers()) + .addAllPredicates(originalSelectExpression.getPredicates()) + .addAllResultColumns(projectionCols) + .build().buildSelect(); + + final var projectionExpressions = Expressions.of(projectionCols.stream().map(Expression::fromColumn).collect(ImmutableList.toImmutableList())); + + final var resultingOperator = LogicalOperator.newUnnamedOperator(projectionExpressions, Quantifier.forEach(Reference.initialOf(newSelectExpression))); + final var indexPlan = LogicalOperator.generateSort(resultingOperator, orderByExpressions, ImmutableSet.of(), Optional.empty()); + final var indexGenerator = MaterializedViewIndexGenerator.from(indexPlan.getQuantifier().getRangesOver().get(), useLegacyExtremum); + final var tableType = Assert.castUnchecked(catalog.findTableByName(indexGenerator.getRecordTypeName()).get(), RecordLayerTable.class); + return indexGenerator.generate(indexName.toString(), isUnique, tableType.getType(), useNullableArrays); + } + + public static final class IndexedColumn { + + @Nonnull + private final Identifier identifier; + + private final boolean isDescending; + + private final boolean isNullsLast; + + private IndexedColumn(@Nonnull final Identifier identifier, boolean isDescending, boolean isNullsLast) { + this.identifier = identifier; + this.isDescending = isDescending; + this.isNullsLast = isNullsLast; + } + + @Nonnull + public Identifier getIdentifier() { + return identifier; + } + + public boolean isDescending() { + return isDescending; + } + + public boolean isNullsLast() { + return isNullsLast; + } + + @Nonnull + public static IndexedColumn of(@Nonnull final Identifier identifier, boolean isDescending, boolean isNullsLast) { + return new IndexedColumn(identifier, isDescending, isNullsLast); + } + } + + @Nonnull + public static Builder newBuilder() { + return new Builder(); + } + + public static final class Builder { + + private Identifier indexName; + + private LogicalPlanFragment indexSource; + + private SemanticAnalyzer semanticAnalyzer; + + @Nonnull + private final List keyColumns; + + @Nonnull + private final List valueColumns; + + private boolean isUnique; + + private boolean useLegacyExtremum; + + private boolean useNullableArrays; + + private Builder() { + this.keyColumns = new ArrayList<>(); + this.valueColumns = new ArrayList<>(); + } + + @Nonnull + public Builder setIndexName(@Nonnull final Identifier indexName) { + this.indexName = indexName; + return this; + } + + @Nonnull + public Builder setIndexSource(@Nonnull final LogicalPlanFragment indexSource) { + this.indexSource = indexSource; + 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 setIndexType(@Nonnull final String 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 Builder setUseNullableArrays(boolean useNullableArrays) { + this.useNullableArrays = useNullableArrays; + return this; + } + + @Nonnull + public OnSourceIndexGenerator build() { + Assert.notNullUnchecked(indexName); + Assert.notNullUnchecked(indexSource); + Assert.notNullUnchecked(semanticAnalyzer); + Assert.thatUnchecked(!keyColumns.isEmpty()); + return new OnSourceIndexGenerator(indexName, indexSource, keyColumns, valueColumns, + isUnique, useLegacyExtremum, useNullableArrays); + } + } +} 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 ecf6590442..8b25ed1ff5 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 @@ -1686,4 +1710,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 4e5145aca1..a79d319985 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 @@ -45,7 +45,9 @@ 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.LogicalOperators; +import com.apple.foundationdb.relational.recordlayer.query.ddl.OnSourceIndexGenerator; +import com.apple.foundationdb.relational.recordlayer.query.ddl.MaterializedViewIndexGenerator; 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; @@ -192,23 +196,93 @@ 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 = MaterializedViewIndexGenerator.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 final 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 sourceIdentifier = visitFullId(indexDefinitionContext.source); + var logicalOperator = generateSourceAccessForIndex(sourceIdentifier); + + 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 = OnSourceIndexGenerator.newBuilder() + .setIndexName(indexId) + .setIndexSource(getDelegate().getCurrentPlanFragment()) + .setSemanticAnalyzer(getDelegate().getSemanticAnalyzer()) + .setUseLegacyExtremum(useLegacyExtremum) + .setUseNullableArrays(containsNullableArray) + .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(OnSourceIndexGenerator.IndexedColumn.of(columnId, isDesc, nullsLast)); + } else { + indexGeneratorBuilder.addKeyColumn(OnSourceIndexGenerator.IndexedColumn.of(columnId, false, false)); + } + }); + + if (indexDefinitionContext.includeClause() != null) { + indexDefinitionContext.includeClause().uidList().uid().forEach(uid -> { + final var columnId = visitUid(uid); + indexGeneratorBuilder.addValueColumn(OnSourceIndexGenerator.IndexedColumn.of(columnId, false, false)); + }); + } + + getDelegate().popPlanFragment(); + return indexGeneratorBuilder.build().generate(ddlCatalog); + } + + @Nonnull + private LogicalOperator generateSourceAccessForIndex(@Nonnull final Identifier sourceIdentifier) { + final var semanticAnalyzer = getDelegate().getSemanticAnalyzer(); + var logicalOperator = getDelegate().getPlanGenerationContext().withDisabledLiteralProcessing(() -> + LogicalOperator.generateAccess(sourceIdentifier, Optional.empty(), Set.of(), + semanticAnalyzer, getDelegate().getCurrentPlanFragment(), + getDelegate().getLogicalOperatorCatalog())); + + if (semanticAnalyzer.tableExists(sourceIdentifier)) { + final var output = logicalOperator.getOutput().expanded().rewireQov(logicalOperator.getQuantifier().getFlowedObjectValue()); + logicalOperator = LogicalOperator.generateSimpleSelect(output, LogicalOperators.ofSingle(logicalOperator), + Optional.empty(), Optional.empty(), ImmutableSet.of(), true); + } + + return logicalOperator; + } + @Nonnull @Override public DataType.Named visitEnumDefinition(@Nonnull RelationalParser.EnumDefinitionContext ctx) { @@ -265,7 +339,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 -> { @@ -276,6 +349,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(); @@ -389,10 +463,14 @@ private RecordLayerView getViewMetadata(@Nonnull final RelationalParser.ViewDefi // prepared parameters in views are not supported. QueryParser.validateNoPreparedParams(viewCtx); + getDelegate().pushPlanFragment(); + // 3. visit the SQL string to generate (compile) the corresponding SQL plan. final var viewQuery = getDelegate().getPlanGenerationContext().withDisabledLiteralProcessing(() -> Assert.castUnchecked(viewCtx.viewQuery.accept(this), LogicalOperator.class)); + getDelegate().popPlanFragment(); + // 4. Return it. return RecordLayerView.newBuilder() .setName(viewName) @@ -454,7 +532,7 @@ private UserDefinedFunction visitSqlInvokedFunction(@Nonnull final RelationalPar List dataTypeList = parameters.stream().map(Expression::getDataType).collect(Collectors.toList()); List paramValueList = dataTypeList.stream().map(dt -> QuantifiedObjectValue.of(CorrelationIdentifier.uniqueId(), DataTypeUtils.toRecordLayerType(dt))).collect(Collectors.toList()); - Assert.thatUnchecked(parameters.asList().size() == 1, "we only support 1 input parameter for user defined scalar function now"); + Assert.thatUnchecked(parameters.asList().size() == 1, "only single input parameter for user defined scalar function is supported"); // only support fullId functionBody now final var functionBody = visitUserDefinedScalarFunctionStatementBody(Assert.castUnchecked(bodyCtx, RelationalParser.UserDefinedScalarFunctionStatementBodyContext.class)); 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 d82f3613e1..2c3678c530 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 @@ -536,6 +555,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) { @@ -1364,6 +1418,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) { @@ -1572,6 +1631,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 8178bd893a..fe1e9c59f7 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 @@ -193,8 +193,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 901d606abd..7d875cb843 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 @@ -587,8 +587,8 @@ public List visitOrderByClauseForSelect(@Nonnull RelationalPa final var matchingExpressionMaybe = isAliasMaybe.flatMap(alias -> semanticAnalyzer.lookupAlias(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 1730c86fef..6c803a93e7 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..5c68b35e45 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(); @@ -101,7 +108,7 @@ public static void setup() { Utils.enableCascadesDebugger(); } - private static final String[] validPrimitiveDataTypes = new String[]{ + private static final String[] validPrimitiveDataTypes = new String[] { "integer", "bigint", "double", "boolean", "string", "bytes", "vector(3, float)", "vector(4, double)", "vector(5, half)" }; @@ -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 { @@ -180,29 +189,32 @@ void shouldWorkWithInjectedQueryFactory(@Nonnull final String query, @Nonnull Dd @Nonnull private static DescriptorProtos.FileDescriptorProto getProtoDescriptor(@Nonnull final SchemaTemplate schemaTemplate) { Assertions.assertInstanceOf(RecordLayerSchemaTemplate.class, schemaTemplate); - final var asRecordLayerSchemaTemplate = (RecordLayerSchemaTemplate) schemaTemplate; + final var asRecordLayerSchemaTemplate = (RecordLayerSchemaTemplate)schemaTemplate; 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); } @@ -233,7 +245,7 @@ void basicEnumParsedCorrectly() throws Exception { @Override public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, @Nonnull Options templateProperties) { Assertions.assertInstanceOf(RecordLayerSchemaTemplate.class, template); - Assertions.assertEquals(1, ((RecordLayerSchemaTemplate) template).getTables().size(), "should have only 1 table"); + Assertions.assertEquals(1, ((RecordLayerSchemaTemplate)template).getTables().size(), "should have only 1 table"); DescriptorProtos.FileDescriptorProto fileDescriptorProto = getProtoDescriptor(template); Assertions.assertEquals(1, fileDescriptorProto.getEnumTypeCount(), "should have one enum defined"); fileDescriptorProto.getEnumTypeList().forEach(enumDescriptorProto -> { @@ -307,14 +319,14 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull final SchemaT void failsToParseEmptyTemplateStatements() throws Exception { //empty template statements are invalid, and can be rejected in the parser final String stmt = "CREATE SCHEMA TEMPLATE test_template "; - boolean[] visited = new boolean[]{false}; + boolean[] visited = new boolean[] {false}; shouldFailWithInjectedFactory(stmt, ErrorCode.SYNTAX_ERROR, new AbstractMetadataOperationsFactory() { @Nonnull @Override public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, @Nonnull Options templateProperties) { Assertions.assertInstanceOf(RecordLayerSchemaTemplate.class, template); - Assertions.assertEquals(0, ((RecordLayerSchemaTemplate) template).getTables().size(), "Tables defined!"); + Assertions.assertEquals(0, ((RecordLayerSchemaTemplate)template).getTables().size(), "Tables defined!"); visited[0] = true; return txn -> { }; @@ -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); @@ -396,7 +409,7 @@ void createSchemaTemplateWithOutOfOrderDefinitionsWork(List columns) thr public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, @Nonnull Options templateProperties) { Assertions.assertInstanceOf(RecordLayerSchemaTemplate.class, template); - Assertions.assertEquals(1, ((RecordLayerSchemaTemplate) template).getTables().size(), "Incorrect number of tables"); + Assertions.assertEquals(1, ((RecordLayerSchemaTemplate)template).getTables().size(), "Incorrect number of tables"); return txn -> { }; } @@ -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 @@ -493,13 +508,13 @@ void createSchemaTemplateWithIndex(List columns) throws Exception { public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, @Nonnull Options templateProperties) { Assertions.assertInstanceOf(RecordLayerSchemaTemplate.class, template); - Assertions.assertEquals(1, ((RecordLayerSchemaTemplate) template).getTables().size(), "Incorrect number of tables"); - Table info = ((RecordLayerSchemaTemplate) template).getTables().stream().findFirst().orElseThrow(); + Assertions.assertEquals(1, ((RecordLayerSchemaTemplate)template).getTables().size(), "Incorrect number of tables"); + Table info = ((RecordLayerSchemaTemplate)template).getTables().stream().findFirst().orElseThrow(); Assertions.assertEquals(1, info.getIndexes().size(), "Incorrect number of indexes!"); final Index index = Assert.optionalUnchecked(info.getIndexes().stream().findFirst()); Assertions.assertEquals("v_idx", index.getName(), "Incorrect index name!"); - final var actualKe = ((RecordLayerIndex) index).getKeyExpression().toKeyExpression(); + final var actualKe = ((RecordLayerIndex)index).getKeyExpression().toKeyExpression(); List keys = null; if (actualKe.hasThen()) { keys = new ArrayList<>(actualKe.getThen().getChildList()); @@ -526,26 +541,26 @@ 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 public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, @Nonnull Options templateProperties) { - Assertions.assertEquals(1, ((RecordLayerSchemaTemplate) template).getTables().size(), "Incorrect number of tables"); - Table info = ((RecordLayerSchemaTemplate) template).getTables().stream().findFirst().orElseThrow(); + Assertions.assertEquals(1, ((RecordLayerSchemaTemplate)template).getTables().size(), "Incorrect number of tables"); + Table info = ((RecordLayerSchemaTemplate)template).getTables().stream().findFirst().orElseThrow(); Assertions.assertEquals(1, info.getIndexes().size(), "Incorrect number of indexes!"); final Index index = Assert.optionalUnchecked(info.getIndexes().stream().findFirst()); Assertions.assertEquals("v_idx", index.getName(), "Incorrect index name!"); - RecordKeyExpressionProto.KeyExpression actualKe = ((RecordLayerIndex) index).getKeyExpression().toKeyExpression(); + RecordKeyExpressionProto.KeyExpression actualKe = ((RecordLayerIndex)index).getKeyExpression().toKeyExpression(); Assertions.assertNotNull(actualKe.getKeyWithValue(), "Null KeyExpression for included columns!"); final RecordKeyExpressionProto.KeyWithValue keyWithValue = actualKe.getKeyWithValue(); @@ -604,7 +619,7 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @Test void dropSchemaTemplates() throws Exception { final String columnStatement = "DROP SCHEMA TEMPLATE test_template"; - boolean[] called = new boolean[]{false}; + boolean[] called = new boolean[] {false}; shouldWorkWithInjectedFactory(columnStatement, 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. @@ -750,7 +767,7 @@ public ConstantAction getDropDatabaseConstantAction(@Nonnull URI dbUrl, boolean void listDatabasesWithoutPrefixParsesCorrectly() throws Exception { final String command = "SHOW DATABASES"; - boolean[] called = new boolean[]{false}; + boolean[] called = new boolean[] {false}; shouldWorkWithInjectedQueryFactory(command, new AbstractQueryFactory() { @Override public DdlQuery getListDatabasesQueryAction(@Nonnull URI prefixPath) { @@ -768,7 +785,7 @@ public DdlQuery getListDatabasesQueryAction(@Nonnull URI prefixPath) { void listDatabasesWithPrefixParsesCorrectly() throws Exception { final String command = "SHOW DATABASES WITH PREFIX /prefix"; - boolean[] called = new boolean[]{false}; + boolean[] called = new boolean[] {false}; shouldWorkWithInjectedQueryFactory(command, new AbstractQueryFactory() { @Override @@ -787,7 +804,7 @@ public DdlQuery getListDatabasesQueryAction(@Nonnull URI prefixPath) { void listSchemaTemplatesParsesProperly() throws Exception { final String command = "SHOW SCHEMA TEMPLATES"; - boolean[] called = new boolean[]{false}; + boolean[] called = new boolean[] {false}; shouldWorkWithInjectedQueryFactory(command, new AbstractQueryFactory() { @Override public DdlQuery getListDatabasesQueryAction(@Nonnull URI prefixPath) { @@ -821,7 +838,7 @@ public DdlQuery getListSchemaTemplatesQueryAction() { void describeSchemaTemplate() throws Exception { final String templateName = "TEST_TEMPLATE"; - boolean[] called = new boolean[]{false}; + boolean[] called = new boolean[] {false}; shouldWorkWithInjectedQueryFactory("DESCRIBE SCHEMA TEMPLATE " + templateName, new AbstractQueryFactory() { @Override public DdlQuery getDescribeSchemaTemplateQueryAction(@Nonnull String schemaId) { @@ -864,7 +881,7 @@ public DdlQuery getDescribeSchemaTemplateQueryAction(@Nonnull String schemaId) { void describeSchemaSucceedsWithoutDatabase() throws Exception { // because parser falls back to connection's database. final String templateName = "TEST_TEMPLATE"; - boolean[] called = new boolean[]{false}; + boolean[] called = new boolean[] {false}; shouldWorkWithInjectedQueryFactory("DESCRIBE SCHEMA " + templateName, new AbstractQueryFactory() { @Override public DdlQuery getDescribeSchemaQueryAction(@Nonnull URI dbUri, @Nonnull String schemaId) { @@ -882,7 +899,7 @@ public DdlQuery getDescribeSchemaQueryAction(@Nonnull URI dbUri, @Nonnull String void describeSchemaPathSucceeds() throws Exception { final String templateName = "TEST_TEMPLATE"; - boolean[] called = new boolean[]{false}; + boolean[] called = new boolean[] {false}; shouldWorkWithInjectedQueryFactory("DESCRIBE SCHEMA " + "/test_db/" + templateName, new AbstractQueryFactory() { @Override public DdlQuery getDescribeSchemaQueryAction(@Nonnull URI dbUri, @Nonnull String schemaId) { @@ -900,7 +917,7 @@ public DdlQuery getDescribeSchemaQueryAction(@Nonnull URI dbUri, @Nonnull String void describeSchemaWithSetDatabaseSucceeds() throws Exception { final String templateName = "TEST_TEMPLATE"; - boolean[] called = new boolean[]{false}; + boolean[] called = new boolean[] {false}; shouldWorkWithInjectedQueryFactory("DESCRIBE SCHEMA " + templateName, new AbstractQueryFactory() { @Override public DdlQuery getDescribeSchemaQueryAction(@Nonnull URI dbUri, @Nonnull String schemaId) { @@ -918,7 +935,7 @@ public DdlQuery getDescribeSchemaQueryAction(@Nonnull URI dbUri, @Nonnull String void createSchemaWithPath() throws Exception { final String templateName = "test_template"; - boolean[] called = new boolean[]{false}; + boolean[] called = new boolean[] {false}; shouldWorkWithInjectedFactory("CREATE SCHEMA /test_db/" + templateName + " WITH TEMPLATE " + templateName, new AbstractMetadataOperationsFactory() { @Nonnull @Override @@ -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,247 @@ 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 " + + "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.None) + .nest(Key.Expressions.field("values", KeyExpression.FanType.FanOut).nest("x")), Key.Expressions.field("p"))); + return txn -> { + }; + } + }); + } + + @Test + void createIndexOnRepeatedUsingMatViewSyntax() 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 INDEX 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 "; + + 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("mv1"); + 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.None) + .nest(Key.Expressions.field("values", 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(a, b) include (S)"; + + 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/api/ddl/IndexTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java index c61ada2fc4..7310cf9964 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java @@ -20,6 +20,8 @@ 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.expressions.GroupingKeyExpression; import com.apple.foundationdb.record.metadata.expressions.KeyExpression; @@ -62,6 +64,8 @@ import static com.apple.foundationdb.record.metadata.Key.Expressions.version; import static com.apple.foundationdb.relational.util.NullableArrayUtils.REPEATED_FIELD_NAME; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + public class IndexTest { @RegisterExtension @Order(0) @@ -110,7 +114,7 @@ private void indexIs(@Nonnull final String stmt, @Nonnull final KeyExpression ex } private void indexIs(@Nonnull final String stmt, @Nonnull final KeyExpression expectedKey, @Nonnull final String indexType, - @Nonnull final Consumer validator) throws Exception { + @Nonnull final Consumer validator) throws Exception { shouldWorkWithInjectedFactory(stmt, new AbstractMetadataOperationsFactory() { @Nonnull @Override @@ -123,11 +127,12 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull final SchemaT Assertions.assertEquals(1, table.getIndexes().size(), "Incorrect number of indexes!"); final Index index = Assert.optionalUnchecked(table.getIndexes().stream().findFirst()); Assertions.assertInstanceOf(RecordLayerIndex.class, index); + final var recordLayerIndex = (RecordLayerIndex)index; Assertions.assertEquals("MV1", index.getName(), "Incorrect index name!"); Assertions.assertEquals(indexType, index.getIndexType()); - final KeyExpression actualKey = KeyExpression.fromProto(((RecordLayerIndex) index).getKeyExpression().toKeyExpression()); + final KeyExpression actualKey = KeyExpression.fromProto((recordLayerIndex).getKeyExpression().toKeyExpression()); Assertions.assertEquals(expectedKey, actualKey); - validator.accept(index); + validator.accept(recordLayerIndex); return txn -> { }; } @@ -197,7 +202,7 @@ void createdIndexWorksDeepNestingAndConcat() throws Exception { } @Test - void createdIndexWorksDeepNestingAndConcatCartesian() throws Exception { + void createdLegacyIndexWorksDeepNestingAndConcatCartesian() throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TYPE AS STRUCT A(x bigint) " + "CREATE TYPE AS STRUCT C(z bigint, k bigint) " + @@ -219,7 +224,29 @@ void createdIndexWorksDeepNestingAndConcatCartesian() throws Exception { } @Test - void createdIndexWorksDeepNestingAndNestedCartesianConcat() throws Exception { + void createdIndexWorksDeepNestingAndConcatCartesian() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TYPE AS STRUCT A(x bigint) " + + "CREATE TYPE AS STRUCT C(z bigint, k bigint) " + + "CREATE TYPE AS STRUCT B(a A array, c C array) " + + "CREATE TABLE T(p bigint, b B array, primary key(p))" + + "CREATE VIEW v1 AS SELECT SQ1.x,SQ2.z, SQ2.k from " + + " T AS t," + + " (select M.x from t.b AS Y, (select x from Y.a) M) SQ1," + + " (select M.z, M.k from t.b AS Y, (select z,k from Y.c) M) SQ2 " + + "CREATE INDEX MV1 ON v1(Z, K, X)"; + indexIs(stmt, + concat(field("B").nest(field(REPEATED_FIELD_NAME, KeyExpression.FanType.FanOut) + .nest(field("C").nest(field(REPEATED_FIELD_NAME, KeyExpression.FanType.FanOut) + .nest(concat(field("Z"), field("K")))))), + field("B").nest(field(REPEATED_FIELD_NAME, KeyExpression.FanType.FanOut) + .nest(field("A").nest(field(REPEATED_FIELD_NAME, KeyExpression.FanType.FanOut) + .nest(field("X")))))), + IndexTypes.VALUE); + } + + @Test + void createdLegacyIndexWorksDeepNestingAndNestedCartesianConcat() throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TYPE AS STRUCT A(x bigint) " + "CREATE TYPE AS STRUCT C(z bigint) " + @@ -235,14 +262,67 @@ void createdIndexWorksDeepNestingAndNestedCartesianConcat() throws Exception { } @Test - void createIndexWithPredicateIsSupported() throws Exception { + void createdIndexWorksDeepNestingAndNestedCartesianConcat() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TYPE AS STRUCT A(x bigint) " + + "CREATE TYPE AS STRUCT C(z bigint) " + + "CREATE TYPE AS STRUCT B(a A array, c C array) " + + "CREATE TABLE T(p bigint, b B array, primary key(p))" + + "CREATE VIEW v1 AS SELECT SQ.x, SQ.z from T AS t, (select M.x, N.z from t.b AS Y, (select x from Y.a) M, (select z from Y.c) N) SQ " + + "CREATE INDEX mv1 on v1(x, z)"; + indexIs(stmt, + field("B", KeyExpression.FanType.None).nest(field(NullableArrayUtils.getRepeatedFieldName(), KeyExpression.FanType.FanOut).nest( + concat(field("A", KeyExpression.FanType.None).nest(field(NullableArrayUtils.getRepeatedFieldName(), KeyExpression.FanType.FanOut).nest(field("X", KeyExpression.FanType.None))), + field("C", KeyExpression.FanType.None).nest(field(NullableArrayUtils.getRepeatedFieldName(), KeyExpression.FanType.FanOut).nest(field("Z", KeyExpression.FanType.None))) + ))), + IndexTypes.VALUE); + } + + @Test + void createLegacyIndexWithPredicateIsSupported() throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TYPE AS STRUCT A(x bigint) " + "CREATE TYPE AS STRUCT B(y string) " + "CREATE TABLE T(p bigint, a A array, b B array, primary key(p))" + "CREATE INDEX mv1 AS SELECT p FROM T where p > 10 order by p"; + indexIs(stmt, field("P", KeyExpression.FanType.None), IndexTypes.VALUE, index -> { + assertThat(index.isUnique()).isFalse(); + assertThat(index.getName()).isEqualTo("MV1"); + assertThat(index.getPredicate()).isEqualTo(RecordMetaDataProto.Predicate.newBuilder() + .setValuePredicate(RecordMetaDataProto.ValuePredicate.newBuilder().addValue("P") + .setComparison(RecordMetaDataProto.Comparison.newBuilder() + .setSimpleComparison(RecordMetaDataProto.SimpleComparison.newBuilder() + .setType(RecordMetaDataProto.ComparisonType.GREATER_THAN) + .setOperand(RecordKeyExpressionProto.Value.newBuilder().setLongValue(10L).build()) + .build()) + .build()) + .build()) + .build()); + }); + } + + @Test + void createIndexWithPredicateIsSupported() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TYPE AS STRUCT A(x bigint) " + + "CREATE TYPE AS STRUCT B(y string) " + + "CREATE TABLE T(p bigint, a A array, b B array, primary key(p))" + + "CREATE VIEW v AS SELECT p FROM T where p > 10 " + + "CREATE INDEX mv1 ON v(p)"; // todo (yhatem) verify the predicate. - indexIs(stmt, field("P", KeyExpression.FanType.None), IndexTypes.VALUE); + indexIs(stmt, field("P", KeyExpression.FanType.None), IndexTypes.VALUE, index -> { + assertThat(index.isUnique()).isFalse(); + assertThat(index.getPredicate()).isEqualTo(RecordMetaDataProto.Predicate.newBuilder() + .setValuePredicate(RecordMetaDataProto.ValuePredicate.newBuilder().addValue("P") + .setComparison(RecordMetaDataProto.Comparison.newBuilder() + .setSimpleComparison(RecordMetaDataProto.SimpleComparison.newBuilder() + .setType(RecordMetaDataProto.ComparisonType.GREATER_THAN) + .setOperand(RecordKeyExpressionProto.Value.newBuilder().setLongValue(10L).build()) + .build()) + .build()) + .build()) + .build()); + }); } @Test @@ -703,7 +783,7 @@ void createAggregateIndexOnMinMax(String index) throws Exception { indexIs(stmt, field("COL2").groupBy(field("COL1")), "MIN".equals(index) ? IndexTypes.PERMUTED_MIN : IndexTypes.PERMUTED_MAX, - idx -> Assertions.assertEquals("0", ((RecordLayerIndex) idx).getOptions().get("permutedSize")) + idx -> Assertions.assertEquals("0", idx.getOptions().get("permutedSize")) ); } @@ -716,7 +796,7 @@ void createAggregateIndexOnMinMaxWithGroupingOrdering(String index) throws Excep indexIs(stmt, field("COL2").groupBy(field("COL1")), "MIN".equals(index) ? IndexTypes.PERMUTED_MIN : IndexTypes.PERMUTED_MAX, - idx -> Assertions.assertEquals("0", ((RecordLayerIndex) idx).getOptions().get("permutedSize")) + idx -> Assertions.assertEquals("0", idx.getOptions().get("permutedSize")) ); } @@ -729,7 +809,7 @@ void createAggregateIndexOnMinMaxWithGroupingOrderingIncludingMax(String index) indexIs(stmt, field("COL2").groupBy(field("COL1")), "MIN".equals(index) ? IndexTypes.PERMUTED_MIN : IndexTypes.PERMUTED_MAX, - idx -> Assertions.assertEquals("0", ((RecordLayerIndex) idx).getOptions().get("permutedSize")) + idx -> Assertions.assertEquals("0", idx.getOptions().get("permutedSize")) ); } @@ -742,7 +822,7 @@ void createAggregateIndexOnMinMaxWithPermutedOrdering(String index) throws Excep indexIs(stmt, field("COL4").groupBy(concatenateFields("COL1", "COL2", "COL3")), "MIN".equals(index) ? IndexTypes.PERMUTED_MIN : IndexTypes.PERMUTED_MAX, - idx -> Assertions.assertEquals("1", ((RecordLayerIndex) idx).getOptions().get("permutedSize")) + idx -> Assertions.assertEquals("1", idx.getOptions().get("permutedSize")) ); } 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(\0¸¢—8@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(d0½«©8@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(e0ŽþÍ8&@Œ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(m0ˆùŠ8&@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Ñ »©Å,(©0„µ®8Õ@º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'” +Žù»½ŒÍ ¯Öý@(Í0žÅÀ8™@»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Ú +øÀ…‚ ˜¥±(e0ŸÐ58&@µ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â +øö¾• ‡€í(e0®»68&@‘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ä +ˆû‹Í îò’(m0šÛ28&@’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Ž +ø‰²‡ ¾¦É(e0â18&@º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 +ˆˆª• ‹–„(m0ž—18&@»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 ¯¡“ +(\0Æü8@^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ã +êºôœ• Ìðþ(t0¢üg8@@ +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 …„ (\0ÇÐ8@‚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 ú°ñ (d0âœ8@ƒ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 ©º (d0ç8@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ê +„ņÚ… Èëà (h0×Ì08.@|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 Õ‚à(~0¨×x8K@Ž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 ÉôÜ(~0“„z8K@Ž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Û ©ž¸<(’0æÜÈ8y@Ò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 Õ‚à(~0¨×x8K@Ž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 ÉôÜ(~0“„z8K@Ž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 Õ‚à(~0¨×x8K@Ž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 ÉôÜ(~0“„z8K@Ž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Û ©ž¸<(’0æÜÈ8y@Ò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 Õ‚à(~0¨×x8K@Ž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 ÉôÜ(~0“„z8K@Ž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..d5abfb3fcf --- /dev/null +++ b/yaml-tests/src/test/resources/index-ddl.yamsql @@ -0,0 +1,331 @@ +# +# index-ddl.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 + 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 + 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 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) + + 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) + + 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) + + 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 + + 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 + + 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) + + 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 + + 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 + + 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}] +... From 94756fb033fabeec06e0078251ce05a6f187ba27 Mon Sep 17 00:00:00 2001 From: Youssef Hatem Date: Wed, 26 Nov 2025 18:44:53 +0000 Subject: [PATCH 2/7] Support vector index. --- .../src/main/antlr/RelationalParser.g4 | 20 +- .../ddl/MaterializedViewIndexGenerator.java | 4 +- .../query/ddl/OnSourceIndexGenerator.java | 181 ++++++++++++++++-- .../query/visitors/BaseVisitor.java | 11 ++ .../query/visitors/DdlVisitor.java | 103 +++++++--- .../query/visitors/DelegatingVisitor.java | 3 +- .../query/visitors/TypedVisitor.java | 4 + .../relational/api/ddl/IndexTest.java | 14 ++ 8 files changed, 282 insertions(+), 58 deletions(-) diff --git a/fdb-relational-core/src/main/antlr/RelationalParser.g4 b/fdb-relational-core/src/main/antlr/RelationalParser.g4 index 6be6c43703..84caf692ed 100644 --- a/fdb-relational-core/src/main/antlr/RelationalParser.g4 +++ b/fdb-relational-core/src/main/antlr/RelationalParser.g4 @@ -202,15 +202,15 @@ 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 + : HNSW_EF_CONSTRUCTION '=' efConstruction=decimalLiteral + | HNSW_M '=' m=decimalLiteral + | HNSW_M_MAX '=' mMax=decimalLiteral + | HNSW_MAINTAIN_STATS_PROBABILITY '=' maintainStatsProbability=decimalLiteral + | HNSW_METRIC '=' metric=stringLiteral + | HNSW_RABITQ_NUM_EX_BITS '=' rabitQNumExBits=decimalLiteral + | HNSW_SAMPLE_VECTOR_STATS_PROBABILITY '=' statsProbability=decimalLiteral + | HNSW_STATS_THRESHOLD '=' statsThreshold=decimalLiteral + | HNSW_USE_RABITQ '=' useRabitQ=booleanLiteral ; indexAttributes @@ -1149,7 +1149,7 @@ frameRange */ partitionClause - : PARTITION BY uid (',' uid)* + : PARTITION BY '(' indexColumnSpec (',' indexColumnSpec)* ')' ; scalarFunctionName diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/MaterializedViewIndexGenerator.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/MaterializedViewIndexGenerator.java index 17b85a1fd6..d0097cf0e6 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/MaterializedViewIndexGenerator.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/MaterializedViewIndexGenerator.java @@ -144,7 +144,7 @@ private MaterializedViewIndexGenerator(@Nonnull RelationalExpression relationalE } @Nonnull - public RecordLayerIndex generate(@Nonnull String indexName, boolean isUnique, @Nonnull Type.Record tableType, boolean containsNullableArray) { + public RecordLayerIndex.Builder generate(@Nonnull String indexName, boolean isUnique, @Nonnull Type.Record tableType, boolean containsNullableArray) { final var indexBuilder = RecordLayerIndex.newBuilder() .setName(indexName) .setTableName(getRecordTypeName()) @@ -233,7 +233,7 @@ public RecordLayerIndex generate(@Nonnull String indexName, boolean isUnique, @N Assert.failUnchecked(ErrorCode.UNSUPPORTED_OPERATION, "Unsupported index definition. Cannot order " + indexType + " index by aggregate value"); } } - return indexBuilder.build(); + return indexBuilder; } @Nonnull diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/OnSourceIndexGenerator.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/OnSourceIndexGenerator.java index 536cd93b0e..caee3148f1 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/OnSourceIndexGenerator.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/OnSourceIndexGenerator.java @@ -21,7 +21,6 @@ package com.apple.foundationdb.relational.recordlayer.query.ddl; import com.apple.foundationdb.record.EvaluationContext; -import com.apple.foundationdb.record.metadata.IndexTypes; import com.apple.foundationdb.record.query.plan.cascades.AliasMap; import com.apple.foundationdb.record.query.plan.cascades.Column; import com.apple.foundationdb.record.query.plan.cascades.GraphExpansion; @@ -31,6 +30,7 @@ import com.apple.foundationdb.record.query.plan.cascades.values.Value; import com.apple.foundationdb.record.query.plan.cascades.values.simplification.DefaultValueSimplificationRuleSet; import com.apple.foundationdb.relational.api.exceptions.ErrorCode; +import com.apple.foundationdb.relational.generated.RelationalParser; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerIndex; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerSchemaTemplate; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerTable; @@ -41,19 +41,71 @@ import com.apple.foundationdb.relational.recordlayer.query.LogicalPlanFragment; import com.apple.foundationdb.relational.recordlayer.query.OrderByExpression; import com.apple.foundationdb.relational.recordlayer.query.SemanticAnalyzer; +import com.apple.foundationdb.relational.recordlayer.query.visitors.IdentifierVisitor; import com.apple.foundationdb.relational.util.Assert; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import javax.annotation.Nonnull; import java.util.ArrayList; -import java.util.LinkedHashMap; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +/** + * Generator for creating indexes using the {@code INDEX ON} syntax. + *

+ * This class is responsible for processing and generating {@link RecordLayerIndex} instances from the + * {@code INDEX ON} DDL syntax, which allows creating indexes directly on table sources with explicit + * column specifications. This is in contrast to the {@code INDEX AS} syntax which defines indexes through + * arbitrary SELECT queries. + *

+ * The {@code INDEX ON} syntax follows this form: + *

+ * [UNIQUE] INDEX index_name ON table_name (
+ *     column1 [ASC|DESC] [NULLS FIRST|NULLS LAST],
+ *     column2 [ASC|DESC] [NULLS FIRST|NULLS LAST],
+ *     ...
+ * )
+ * [INCLUDE (value_column1, value_column2, ...)]
+ * [OPTIONS (option_name, ...)]
+ * 
+ *

+ * The index definition consists of: + *

    + *
  • Key columns: The ordered list of columns in the index key, specified in the main column list. + * Each column can have optional ordering modifiers (ASC/DESC, NULLS FIRST/LAST).
  • + *
  • Value columns: Additional columns to store in the index but not part of the key, specified + * in the optional INCLUDE clause. These columns can be used to satisfy queries without accessing + * the base table (covering index pattern).
  • + *
  • Uniqueness constraint: Optional UNIQUE modifier to enforce that key column combinations + * are unique across the indexed table.
  • + *
  • Index options: Optional configuration flags such as LEGACY_EXTREMUM_EVER or USE_NULLABLE_ARRAYS + * that affect index behavior and storage.
  • + *
+ *

+ * Example usage: + *

+ * CREATE UNIQUE INDEX idx_user_email ON users (email ASC)
+ *     INCLUDE (first_name, last_name)
+ *     OPTIONS (LEGACY_EXTREMUM_EVER);
+ * 
+ *

+ * The generator transforms the source table query into a properly ordered and projected logical plan, + * then delegates to {@link MaterializedViewIndexGenerator} to produce the final index structure. The + * generated index maintains the specified column ordering and can leverage the query planner for + * efficient index-based query execution. + *

+ * This class is typically constructed via its {@link Builder} pattern, which validates that all required + * components (index name, source query, and at least one key column) are provided before generation. + * + * @see MaterializedViewIndexGenerator + * @see RecordLayerIndex + */ public final class OnSourceIndexGenerator { @Nonnull @@ -74,10 +126,13 @@ public final class OnSourceIndexGenerator { private final boolean useNullableArrays; + @Nonnull + private final Map indexOptions; + public OnSourceIndexGenerator(@Nonnull final Identifier indexName, @Nonnull final LogicalPlanFragment source, @Nonnull final List keyColumns, @Nonnull final List valueColumns, - final boolean isUnique, - final boolean useLegacyExtremum, final boolean useNullableArrays) { + final boolean isUnique, final boolean useLegacyExtremum, final boolean useNullableArrays, + @Nonnull final Map indexOptions) { this.indexName = indexName; this.source = source; this.keyColumns = ImmutableList.copyOf(keyColumns); @@ -85,59 +140,88 @@ public OnSourceIndexGenerator(@Nonnull final Identifier indexName, @Nonnull fina this.isUnique = isUnique; this.useLegacyExtremum = useLegacyExtremum; this.useNullableArrays = useNullableArrays; + this.indexOptions = ImmutableMap.copyOf(indexOptions); } + /** + * Generates a {@link RecordLayerIndex} based on the configured source query and index columns. + *

+ * This method first extracts key and value column identifiers from the configured columns, ensuring value + * columns don't duplicate key columns. It then retrieves the top-level logical operator from the source query + * plan and builds a mapping of column identifiers to their underlying {@link Column} representations, pushing + * down values through the query plan. A projection is created containing all key and value columns in order, + * and ORDER BY expressions are constructed from the key columns, respecting their ordering specifications + * (ASC/DESC, NULLS FIRST/LAST). The SELECT expression is then reconstructed with the projected columns while + * preserving the original quantifiers and predicates. Finally, a sorted logical plan is generated based on + * the ORDER BY expressions, and the method delegates to {@link MaterializedViewIndexGenerator} to produce + * the final index structure. + *

+ * The generated index will be ordered according to the key columns and can optionally enforce uniqueness + * if configured via {@link Builder#setUnique(boolean)}. + * + * @param catalog the schema catalog used to resolve table types for the index + * @return a fully configured {@link RecordLayerIndex} ready to be added to the schema + */ @Nonnull - public RecordLayerIndex generate(@Nonnull final RecordLayerSchemaTemplate catalog) { + public RecordLayerIndex.Builder generate(@Nonnull final RecordLayerSchemaTemplate catalog) { final var keyIdentifiers = keyColumns.stream().map(IndexedColumn::getIdentifier).collect(ImmutableList.toImmutableList()); final var keyIdentifiersAsSet = ImmutableSet.copyOf(keyIdentifiers); final var valueIdentifiers = valueColumns.stream().map(IndexedColumn::getIdentifier) .filter(id -> !keyIdentifiersAsSet.contains(id)).collect(ImmutableList.toImmutableList()); final var topLevelOperator = Iterables.getOnlyElement(source.getLogicalOperators()); - var topLevelSelect = topLevelOperator.getQuantifier().getRangesOver().get(); + final var topLevelSelect = topLevelOperator.getQuantifier().getRangesOver().get(); Assert.thatUnchecked(topLevelSelect instanceof SelectExpression); - final var resultValue = topLevelOperator.getQuantifier().getRangesOver().get().getResultValue(); + final var selectResultValue = topLevelOperator.getQuantifier().getRangesOver().get().getResultValue(); final Map> originalOutputMap = topLevelOperator.getOutput().stream() .filter(e -> e.getName().isPresent()) .collect(Collectors.toUnmodifiableMap( expression -> expression.getName().get(), expression -> { - final var name = expression.getName().map(Identifier::getName); final var value = ImmutableList.of(expression.getUnderlying()); - final var pushedDownValue = resultValue.pushDown(value, DefaultValueSimplificationRuleSet.instance(), - EvaluationContext.empty(), AliasMap.emptyMap(), ImmutableSet.of(), topLevelOperator.getQuantifier().getAlias()).get(0); + final var pushedDownValue = selectResultValue.pushDown(value, DefaultValueSimplificationRuleSet.instance(), + EvaluationContext.empty(), AliasMap.emptyMap(), ImmutableSet.of(), + topLevelOperator.getQuantifier().getAlias()).get(0); + final var name = expression.getName().map(Identifier::getName); return Column.of(name, pushedDownValue); })); - final List> projectionCols = ImmutableList.builder().addAll(keyIdentifiers).addAll(valueIdentifiers) + final List> projectionCols = ImmutableList.builder() + .addAll(keyIdentifiers) + .addAll(valueIdentifiers) .build().stream().map(identifier -> { final var column = originalOutputMap.get(identifier); Assert.notNullUnchecked(column, ErrorCode.UNDEFINED_COLUMN, () -> "could not find " + identifier); return column; }).collect(ImmutableList.toImmutableList()); - final var orderByExpressions = keyColumns.stream().map(keyColumn -> { - final var column = originalOutputMap.get(keyColumn.identifier); - Assert.notNullUnchecked(column, ErrorCode.UNDEFINED_COLUMN, () -> "could not find " + keyColumn.identifier); - return OrderByExpression.of(Expression.fromColumn(column), keyColumn.isDescending, keyColumn.isNullsLast); + final List orderByExpressions = keyColumns.stream().map(keyColumn -> { + final var column = originalOutputMap.get(keyColumn.getIdentifier()); + Assert.notNullUnchecked(column, ErrorCode.UNDEFINED_COLUMN, () -> "could not find " + keyColumn.getIdentifier()); + return OrderByExpression.of(Expression.fromColumn(column), keyColumn.isDescending(), keyColumn.isNullsLast()); }).collect(ImmutableList.toImmutableList()); final var originalSelectExpression = (SelectExpression)topLevelSelect; - final var newSelectExpression = GraphExpansion.builder().addAllQuantifiers(originalSelectExpression.getQuantifiers()) + final var newSelectExpression = GraphExpansion.builder() + .addAllQuantifiers(originalSelectExpression.getQuantifiers()) .addAllPredicates(originalSelectExpression.getPredicates()) .addAllResultColumns(projectionCols) .build().buildSelect(); - final var projectionExpressions = Expressions.of(projectionCols.stream().map(Expression::fromColumn).collect(ImmutableList.toImmutableList())); + final var projectionExpressions = Expressions.of(projectionCols.stream() + .map(Expression::fromColumn) + .collect(ImmutableList.toImmutableList())); - final var resultingOperator = LogicalOperator.newUnnamedOperator(projectionExpressions, Quantifier.forEach(Reference.initialOf(newSelectExpression))); + final var resultingOperator = LogicalOperator.newUnnamedOperator(projectionExpressions, + Quantifier.forEach(Reference.initialOf(newSelectExpression))); final var indexPlan = LogicalOperator.generateSort(resultingOperator, orderByExpressions, ImmutableSet.of(), Optional.empty()); final var indexGenerator = MaterializedViewIndexGenerator.from(indexPlan.getQuantifier().getRangesOver().get(), useLegacyExtremum); final var tableType = Assert.castUnchecked(catalog.findTableByName(indexGenerator.getRecordTypeName()).get(), RecordLayerTable.class); - return indexGenerator.generate(indexName.toString(), isUnique, tableType.getType(), useNullableArrays); + final var indexBuilder = indexGenerator.generate(indexName.toString(), isUnique, tableType.getType(), useNullableArrays); + indexBuilder.addAllOptions(indexOptions); + return indexBuilder; } public static final class IndexedColumn { @@ -172,6 +256,50 @@ public boolean isNullsLast() { public static IndexedColumn of(@Nonnull final Identifier identifier, boolean isDescending, boolean isNullsLast) { return new IndexedColumn(identifier, isDescending, isNullsLast); } + + /** + * Parses an index column specification from a parser context into an {@link IndexedColumn}. + *

+ * This method extracts the column identifier and optional ordering information (ASC/DESC and NULLS FIRST/LAST) + * from the parser context. If no order clause is specified, defaults to ascending order with nulls first. + * When an order clause is present but nulls ordering is not explicitly specified, the default behavior is: + *

    + *
  • For DESC columns: NULLS LAST
  • + *
  • For ASC columns: NULLS FIRST
  • + *
+ * + * @param columnSpec the parser context containing the column specification + * @param identifierVisitor the visitor used to extract the column identifier + * @return an {@link IndexedColumn} representing the parsed column specification + */ + @Nonnull + public static OnSourceIndexGenerator.IndexedColumn parseColSpec(@Nonnull final RelationalParser.IndexColumnSpecContext columnSpec, + @Nonnull final IdentifierVisitor identifierVisitor) { + final var columnId = identifierVisitor.visitUid(columnSpec.columnName); + final var orderContext = columnSpec.orderClause(); + + boolean isDesc = false; + boolean nullsLast = false; + + if (orderContext == null) { + return OnSourceIndexGenerator.IndexedColumn.of(columnId, isDesc, nullsLast); + } + + isDesc = orderContext.DESC() != null; + if (orderContext.nulls == null) { + nullsLast = isDesc; + } else { + nullsLast = orderContext.LAST() != null; + } + return OnSourceIndexGenerator.IndexedColumn.of(columnId, isDesc, nullsLast); + } + + @Nonnull + public static OnSourceIndexGenerator.IndexedColumn parseUid(@Nonnull final RelationalParser.UidContext uid, + @Nonnull final IdentifierVisitor identifierVisitor) { + final var columnId = identifierVisitor.visitUid(uid); + return OnSourceIndexGenerator.IndexedColumn.of(columnId, false, false); + } } @Nonnull @@ -193,6 +321,9 @@ public static final class Builder { @Nonnull private final List valueColumns; + @Nonnull + private final Map indexOptions; + private boolean isUnique; private boolean useLegacyExtremum; @@ -202,6 +333,7 @@ public static final class Builder { private Builder() { this.keyColumns = new ArrayList<>(); this.valueColumns = new ArrayList<>(); + this.indexOptions = new HashMap<>(); } @Nonnull @@ -235,7 +367,14 @@ public Builder addValueColumn(@Nonnull final IndexedColumn keyColumn) { } @Nonnull - public Builder setIndexType(@Nonnull final String indexType) { + public Builder addIndexOption(@Nonnull final String key, @Nonnull final String value) { + indexOptions.put(key, value); + return this; + } + + @Nonnull + public Builder addAllIndexOptions(@Nonnull final Map indexOptions) { + this.indexOptions.putAll(indexOptions); return this; } @@ -264,7 +403,7 @@ public OnSourceIndexGenerator build() { Assert.notNullUnchecked(semanticAnalyzer); Assert.thatUnchecked(!keyColumns.isEmpty()); return new OnSourceIndexGenerator(indexName, indexSource, keyColumns, valueColumns, - isUnique, useLegacyExtremum, useNullableArrays); + isUnique, useLegacyExtremum, useNullableArrays, indexOptions); } } } 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 8b25ed1ff5..4e0c75f00a 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 @@ -132,6 +132,11 @@ public MutablePlanGenerationContext getPlanGenerationContext() { return mutablePlanGenerationContext; } + @Nonnull + protected IdentifierVisitor getIdentifierVisitor() { + return identifierVisitor; + } + @Nonnull public Plan generateLogicalPlan(@Nonnull ParseTree parseTree) { final var result = visit(parseTree); @@ -408,6 +413,12 @@ public RecordLayerIndex visitIndexOnSourceDefinition(@Nonnull RelationalParser.I return ddlVisitor.visitIndexOnSourceDefinition(ctx); } + @Nonnull + @Override + public RecordLayerIndex visitVectorIndexDefinition(final RelationalParser.VectorIndexDefinitionContext ctx) { + return ddlVisitor.visitVectorIndexDefinition(ctx); + } + @Nonnull @Override public Object visitIndexColumnList(@Nonnull RelationalParser.IndexColumnListContext 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 a79d319985..a069767b84 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,8 @@ package com.apple.foundationdb.relational.recordlayer.query.visitors; import com.apple.foundationdb.annotation.API; +import com.apple.foundationdb.record.metadata.IndexOptions; +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; @@ -46,6 +48,7 @@ import com.apple.foundationdb.relational.recordlayer.query.Expressions; import com.apple.foundationdb.relational.recordlayer.query.Identifier; import com.apple.foundationdb.relational.recordlayer.query.LogicalOperators; +import com.apple.foundationdb.relational.recordlayer.query.ParseHelpers; import com.apple.foundationdb.relational.recordlayer.query.ddl.OnSourceIndexGenerator; import com.apple.foundationdb.relational.recordlayer.query.ddl.MaterializedViewIndexGenerator; import com.apple.foundationdb.relational.recordlayer.query.LogicalOperator; @@ -56,6 +59,7 @@ import com.apple.foundationdb.relational.recordlayer.query.functions.CompiledSqlFunction; import com.apple.foundationdb.relational.util.Assert; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.antlr.v4.runtime.ParserRuleContext; @@ -65,6 +69,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -210,26 +215,24 @@ public RecordLayerIndex visitIndexAsSelectDefinition(@Nonnull RelationalParser.I final var generator = MaterializedViewIndexGenerator.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); + return generator.generate(indexId.getName(), isUnique, table.getType(), containsNullableArray).build(); } @Nonnull @Override public RecordLayerIndex visitIndexOnSourceDefinition(@Nonnull final 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 sourceIdentifier = visitFullId(indexDefinitionContext.source); var logicalOperator = generateSourceAccessForIndex(sourceIdentifier); - 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 useLegacyExtremum = indexOptions != null && indexOptions.indexOption().stream() + .anyMatch(option -> option.LEGACY_EXTREMUM_EVER() != null); final var indexGeneratorBuilder = OnSourceIndexGenerator.newBuilder() .setIndexName(indexId) .setIndexSource(getDelegate().getCurrentPlanFragment()) @@ -238,32 +241,52 @@ public RecordLayerIndex visitIndexOnSourceDefinition(@Nonnull final RelationalPa .setUseNullableArrays(containsNullableArray) .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(OnSourceIndexGenerator.IndexedColumn.of(columnId, isDesc, nullsLast)); - } else { - indexGeneratorBuilder.addKeyColumn(OnSourceIndexGenerator.IndexedColumn.of(columnId, false, false)); - } - }); + indexDefinitionContext.indexColumnList().indexColumnSpec().forEach(colSpec -> + indexGeneratorBuilder.addKeyColumn(OnSourceIndexGenerator.IndexedColumn + .parseColSpec(colSpec, getDelegate().getIdentifierVisitor()))); if (indexDefinitionContext.includeClause() != null) { indexDefinitionContext.includeClause().uidList().uid().forEach(uid -> { - final var columnId = visitUid(uid); - indexGeneratorBuilder.addValueColumn(OnSourceIndexGenerator.IndexedColumn.of(columnId, false, false)); + indexGeneratorBuilder.addValueColumn(OnSourceIndexGenerator.IndexedColumn + .parseUid(uid, getDelegate().getIdentifierVisitor())); }); } getDelegate().popPlanFragment(); - return indexGeneratorBuilder.build().generate(ddlCatalog); + return indexGeneratorBuilder.build().generate(ddlCatalog).build(); + } + + @Nonnull + @Override + public RecordLayerIndex visitVectorIndexDefinition(final RelationalParser.VectorIndexDefinitionContext indexDefinitionContext) { + final var ddlCatalog = metadataBuilder.build(); + getDelegate().replaceSchemaTemplate(ddlCatalog); + getDelegate().pushPlanFragment(); + final var sourceIdentifier = visitFullId(indexDefinitionContext.source); + var logicalOperator = generateSourceAccessForIndex(sourceIdentifier); + getDelegate().getCurrentPlanFragment().setOperator(logicalOperator); + + final Identifier indexId = visitUid(indexDefinitionContext.indexName); + final var indexOptions = parseVectorOptions(indexDefinitionContext.vectorIndexOptions()); + final var indexGeneratorBuilder = OnSourceIndexGenerator.newBuilder() + .setIndexName(indexId) + .setIndexSource(getDelegate().getCurrentPlanFragment()) + .setSemanticAnalyzer(getDelegate().getSemanticAnalyzer()) + .addAllIndexOptions(indexOptions) + .setUseNullableArrays(containsNullableArray); + + indexDefinitionContext.indexColumnList().indexColumnSpec().forEach(colSpec -> + indexGeneratorBuilder.addValueColumn(OnSourceIndexGenerator.IndexedColumn + .parseColSpec(colSpec, getDelegate().getIdentifierVisitor()))); + + if (indexDefinitionContext.partitionClause() != null) { + indexDefinitionContext.partitionClause().indexColumnSpec().forEach(colSpec -> + indexGeneratorBuilder.addKeyColumn(OnSourceIndexGenerator.IndexedColumn + .parseColSpec(colSpec, getDelegate().getIdentifierVisitor()))); + } + + getDelegate().popPlanFragment(); + return indexGeneratorBuilder.build().generate(ddlCatalog).setIndexType(IndexTypes.VECTOR).build(); } @Nonnull @@ -283,6 +306,38 @@ semanticAnalyzer, getDelegate().getCurrentPlanFragment(), return logicalOperator; } + @Nonnull + private Map parseVectorOptions(@Nullable final RelationalParser.VectorIndexOptionsContext indexOptionsContext) { + final var indexOptionsBuilder = ImmutableMap.builder(); + if (indexOptionsContext == null) { + return indexOptionsBuilder.build(); + } + + for (final var vectorIndexOption : indexOptionsContext.vectorIndexOptions()) { + final var option = vectorIndexOption.vectorIndexOption(); + if (option.HNSW_EF_CONSTRUCTION() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_EF_CONSTRUCTION, option.efConstruction.getText()); + } else if (option.HNSW_M() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_M, option.m.getText()); + } else if (option.HNSW_M_MAX() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_M_MAX, option.mMax.getText()); + } else if (option.HNSW_MAINTAIN_STATS_PROBABILITY() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_MAINTAIN_STATS_PROBABILITY, option.maintainStatsProbability.getText()); + } else if (option.HNSW_METRIC() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_METRIC, option.metric.getText()); + } else if (option.HNSW_RABITQ_NUM_EX_BITS() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_RABITQ_NUM_EX_BITS, option.rabitQNumExBits.getText()); + } else if (option.HNSW_SAMPLE_VECTOR_STATS_PROBABILITY() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_SAMPLE_VECTOR_STATS_PROBABILITY, option.statsProbability.getText()); + } else if (option.HNSW_STATS_THRESHOLD() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_STATS_THRESHOLD, option.statsThreshold.getText()); + } else if (option.HNSW_USE_RABITQ() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_USE_RABITQ, option.useRabitQ.getText()); + } + } + return indexOptionsBuilder.build(); + } + @Nonnull @Override public DataType.Named visitEnumDefinition(@Nonnull RelationalParser.EnumDefinitionContext ctx) { 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 2c3678c530..069f6369d7 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 @@ -567,8 +567,9 @@ public RecordLayerIndex visitIndexOnSourceDefinition(@Nonnull RelationalParser.I return getDelegate().visitIndexOnSourceDefinition(ctx); } + @Nonnull @Override - public Object visitVectorIndexDefinition(final RelationalParser.VectorIndexDefinitionContext ctx) { + public RecordLayerIndex visitVectorIndexDefinition(final RelationalParser.VectorIndexDefinitionContext ctx) { return getDelegate().visitVectorIndexDefinition(ctx); } 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 6c803a93e7..1ac98985ab 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 @@ -171,6 +171,10 @@ public interface TypedVisitor extends RelationalParserVisitor { @Override RecordLayerIndex visitIndexOnSourceDefinition(@Nonnull RelationalParser.IndexOnSourceDefinitionContext ctx); + @Nonnull + @Override + RecordLayerIndex visitVectorIndexDefinition(RelationalParser.VectorIndexDefinitionContext ctx); + @Nonnull @Override Object visitIndexColumnList(@Nonnull RelationalParser.IndexColumnListContext ctx); diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java index 7310cf9964..5588e0106d 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java @@ -1007,4 +1007,18 @@ void createIndexWithOrderByMixedDirection() throws Exception { concat(field("COL1"), function("order_desc_nulls_last", field("COL2")), function("order_asc_nulls_last", field("COL3"))), IndexTypes.VALUE); } + + @Test + void createVectorIndexWorksCorrectly() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TYPE AS STRUCT A(x bigint) " + + "CREATE TYPE AS STRUCT C(z bigint, k bigint) " + + "CREATE TYPE AS STRUCT B(a A array, c C array) " + + "CREATE TABLE T(p bigint, b vector(3, float), c bigint, z bigint, primary key(p))" + + "CREATE VIEW V1 AS SELECT p, b, c, z from T where c > 50 " + + "CREATE VECTOR INDEX MV1 ON V1(b) PARTITION BY(z)"; + indexIs(stmt, + keyWithValue(concat(field("Z"), field("B")), 1), + IndexTypes.VECTOR); + } } From 429f1acd6ff175b7b505931f424ae64ef914d204 Mon Sep 17 00:00:00 2001 From: Youssef Hatem Date: Thu, 27 Nov 2025 17:40:43 +0000 Subject: [PATCH 3/7] fixes and more tests for vector index ddl. --- .../src/main/antlr/RelationalLexer.g4 | 37 ++-- .../src/main/antlr/RelationalParser.g4 | 35 +-- .../query/ddl/OnSourceIndexGenerator.java | 5 + .../query/visitors/DdlVisitor.java | 42 ++-- .../query/visitors/DelegatingVisitor.java | 5 + .../relational/api/ddl/IndexTest.java | 204 ++++++++++++++++-- 6 files changed, 268 insertions(+), 60 deletions(-) diff --git a/fdb-relational-core/src/main/antlr/RelationalLexer.g4 b/fdb-relational-core/src/main/antlr/RelationalLexer.g4 index bcbbc340c7..5fd3081018 100644 --- a/fdb-relational-core/src/main/antlr/RelationalLexer.g4 +++ b/fdb-relational-core/src/main/antlr/RelationalLexer.g4 @@ -112,6 +112,7 @@ GET: 'GET'; GRANT: 'GRANT'; GROUP: 'GROUP'; HAVING: 'HAVING'; +HNSW: 'HNSW'; HIGH_PRIORITY: 'HIGH_PRIORITY'; HISTOGRAM: 'HISTOGRAM'; IF: 'IF'; @@ -919,8 +920,8 @@ ASYMMETRIC_DERIVE: 'ASYMMETRIC_DERIVE'; ASYMMETRIC_ENCRYPT: 'ASYMMETRIC_ENCRYPT'; ASYMMETRIC_SIGN: 'ASYMMETRIC_SIGN'; ASYMMETRIC_VERIFY: 'ASYMMETRIC_VERIFY'; -ATAN: 'ATAN'; ATAN2: 'ATAN2'; +ATAN: 'ATAN'; BENCHMARK: 'BENCHMARK'; BIN: 'BIN'; BIT_COUNT: 'BIT_COUNT'; @@ -942,6 +943,7 @@ CONNECTION_ID: 'CONNECTION_ID'; CONV: 'CONV'; CONVERT_TZ: 'CONVERT_TZ'; COS: 'COS'; +COSINE_METRIC: 'COSINE_METRIC'; COT: 'COT'; CRC32: 'CRC32'; CREATE_ASYMMETRIC_PRIV_KEY: 'CREATE_ASYMMETRIC_PRIV_KEY'; @@ -961,7 +963,9 @@ DES_DECRYPT: 'DES_DECRYPT'; DES_ENCRYPT: 'DES_ENCRYPT'; DIMENSION: 'DIMENSION'; DISJOINT: 'DISJOINT'; +DOT_PRODUCT_METRIC: 'DOT_PRODUCT_METRIC'; DRY: 'DRY'; +EF_CONSTRUCTION: 'EF_CONSTRUCTION'; ELT: 'ELT'; ENABLE_LONG_ROWS: 'ENABLE_LONG_ROWS'; ENCODE: 'ENCODE'; @@ -970,6 +974,8 @@ ENDPOINT: 'ENDPOINT'; ENGINE_ATTRIBUTE: 'ENGINE_ATTRIBUTE'; ENVELOPE: 'ENVELOPE'; EQUALS: 'EQUALS'; +EUCLIDEAN_METRIC: 'EUCLIDEAN_METRIC'; +EUCLIDEAN_SQUARE_METRIC: 'EUCLIDEAN_SQUARE_METRIC'; EXP: 'EXP'; EXPORT_SET: 'EXPORT_SET'; EXTERIORRING: 'EXTERIORRING'; @@ -999,15 +1005,6 @@ 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'; @@ -1038,15 +1035,18 @@ LINESTRINGFROMWKB: 'LINESTRINGFROMWKB'; LN: 'LN'; LOAD_FILE: 'LOAD_FILE'; LOCATE: 'LOCATE'; -LOG: 'LOG'; LOG10: 'LOG10'; LOG2: 'LOG2'; +LOG: 'LOG'; LOWER: 'LOWER'; LPAD: 'LPAD'; LTRIM: 'LTRIM'; +CONNECTIVITY: 'CONNECTIVITY'; +MAINTAIN_STATS_PROBABILITY: 'MAINTAIN_STATS_PROBABILITY'; MAKEDATE: 'MAKEDATE'; MAKETIME: 'MAKETIME'; MAKE_SET: 'MAKE_SET'; +MANHATTAN_METRIC: 'MANHATTAN_METRIC'; MASTER_POS_WAIT: 'MASTER_POS_WAIT'; MBRCONTAINS: 'MBRCONTAINS'; MBRDISJOINT: 'MBRDISJOINT'; @@ -1056,6 +1056,7 @@ MBROVERLAPS: 'MBROVERLAPS'; MBRTOUCHES: 'MBRTOUCHES'; MBRWITHIN: 'MBRWITHIN'; MD5: 'MD5'; +METRIC: 'METRIC'; MLINEFROMTEXT: 'MLINEFROMTEXT'; MLINEFROMWKB: 'MLINEFROMWKB'; MONTHNAME: 'MONTHNAME'; @@ -1069,6 +1070,8 @@ MULTIPOINTFROMTEXT: 'MULTIPOINTFROMTEXT'; MULTIPOINTFROMWKB: 'MULTIPOINTFROMWKB'; MULTIPOLYGONFROMTEXT: 'MULTIPOLYGONFROMTEXT'; MULTIPOLYGONFROMWKB: 'MULTIPOLYGONFROMWKB'; +M_MAX: 'M_MAX'; +M_MAX_0: 'M_MAX_0'; NAME_CONST: 'NAME_CONST'; NULLIF: 'NULLIF'; NUMGEOMETRIES: 'NUMGEOMETRIES'; @@ -1091,6 +1094,7 @@ POLYGONFROMWKB: 'POLYGONFROMWKB'; POW: 'POW'; POWER: 'POWER'; QUOTE: 'QUOTE'; +RABITQ_NUM_EX_BITS: 'RABITQ_NUM_EX_BITS'; RADIANS: 'RADIANS'; RAND: 'RAND'; RANDOM_BYTES: 'RANDOM_BYTES'; @@ -1101,13 +1105,14 @@ ROW_COUNT: 'ROW_COUNT'; RPAD: 'RPAD'; RTRIM: 'RTRIM'; RUN: 'RUN'; -SEC_TO_TIME: 'SEC_TO_TIME'; +SAMPLE_VECTOR_STATS_PROBABILITY: 'SAMPLE_VECTOR_STATS_PROBABILITY'; +SCHEMA_NAME: 'SCHEMA_NAME'; SECONDARY_ENGINE_ATTRIBUTE: 'SECONDARY_ENGINE_ATTRIBUTE'; +SEC_TO_TIME: 'SEC_TO_TIME'; SESSION_USER: 'SESSION_USER'; -SHA: 'SHA'; SHA1: 'SHA1'; SHA2: 'SHA2'; -SCHEMA_NAME: 'SCHEMA_NAME'; +SHA: 'SHA'; SIGN: 'SIGN'; SIN: 'SIN'; SLEEP: 'SLEEP'; @@ -1116,6 +1121,7 @@ SQL_THREAD_WAIT_AFTER_GTIDS: 'SQL_THREAD_WAIT_AFTER_GTIDS'; SQRT: 'SQRT'; SRID: 'SRID'; STARTPOINT: 'STARTPOINT'; +STATS_THRESHOLD: 'STATS_THRESHOLD'; STORE_ROW_VERSIONS: 'STORE_ROW_VERSIONS'; STRCMP: 'STRCMP'; STR_TO_DATE: 'STR_TO_DATE'; @@ -1198,6 +1204,7 @@ UNHEX: 'UNHEX'; UNIX_TIMESTAMP: 'UNIX_TIMESTAMP'; UPDATEXML: 'UPDATEXML'; UPPER: 'UPPER'; +USE_RABITQ: 'USE_RABITQ'; UUID: 'UUID'; UUID_SHORT: 'UUID_SHORT'; VALIDATE_PASSWORD_STRENGTH: 'VALIDATE_PASSWORD_STRENGTH'; @@ -1207,9 +1214,9 @@ WEEKDAY: 'WEEKDAY'; WEEKOFYEAR: 'WEEKOFYEAR'; WEIGHT_STRING: 'WEIGHT_STRING'; WITHIN: 'WITHIN'; +X_FUNCTION: 'X'; YEARWEEK: 'YEARWEEK'; Y_FUNCTION: 'Y'; -X_FUNCTION: 'X'; // Calling conventions diff --git a/fdb-relational-core/src/main/antlr/RelationalParser.g4 b/fdb-relational-core/src/main/antlr/RelationalParser.g4 index 84caf692ed..38698aa19e 100644 --- a/fdb-relational-core/src/main/antlr/RelationalParser.g4 +++ b/fdb-relational-core/src/main/antlr/RelationalParser.g4 @@ -168,9 +168,9 @@ enumDefinition ; indexDefinition - : (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 + : (UNIQUE)? INDEX indexName=uid AS queryTerm indexAttributes? #indexAsSelectDefinition + | (UNIQUE)? INDEX indexName=uid ON source=fullId indexColumnList includeClause? indexOptions? #indexOnSourceDefinition + | VECTOR INDEX indexName=uid USING HNSW ON source=fullId indexColumnList includeClause? partitionClause? vectorIndexOptions? #vectorIndexDefinition ; indexColumnList @@ -198,19 +198,28 @@ indexOption ; vectorIndexOptions - : OPTIONS '(' vectorIndexOption (COMMA vectorIndexOptions)* ')' + : OPTIONS '(' vectorIndexOption (COMMA vectorIndexOption)* ')' ; vectorIndexOption - : HNSW_EF_CONSTRUCTION '=' efConstruction=decimalLiteral - | HNSW_M '=' m=decimalLiteral - | HNSW_M_MAX '=' mMax=decimalLiteral - | HNSW_MAINTAIN_STATS_PROBABILITY '=' maintainStatsProbability=decimalLiteral - | HNSW_METRIC '=' metric=stringLiteral - | HNSW_RABITQ_NUM_EX_BITS '=' rabitQNumExBits=decimalLiteral - | HNSW_SAMPLE_VECTOR_STATS_PROBABILITY '=' statsProbability=decimalLiteral - | HNSW_STATS_THRESHOLD '=' statsThreshold=decimalLiteral - | HNSW_USE_RABITQ '=' useRabitQ=booleanLiteral + : EF_CONSTRUCTION '=' efConstruction=DECIMAL_LITERAL + | CONNECTIVITY '=' connectivity=DECIMAL_LITERAL + | M_MAX '=' mMax=DECIMAL_LITERAL + | M_MAX_0 '=' mMaxZero=DECIMAL_LITERAL + | MAINTAIN_STATS_PROBABILITY '=' maintainStatsProbability=REAL_LITERAL + | METRIC '=' metric=hnswMetric + | RABITQ_NUM_EX_BITS '=' rabitQNumExBits=DECIMAL_LITERAL + | SAMPLE_VECTOR_STATS_PROBABILITY '=' statsProbability=REAL_LITERAL + | STATS_THRESHOLD '=' statsThreshold=DECIMAL_LITERAL + | USE_RABITQ '=' useRabitQ=booleanLiteral + ; + +hnswMetric + : MANHATTAN_METRIC + | EUCLIDEAN_METRIC + | EUCLIDEAN_SQUARE_METRIC + | COSINE_METRIC + | DOT_PRODUCT_METRIC ; indexAttributes diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/OnSourceIndexGenerator.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/OnSourceIndexGenerator.java index caee3148f1..5c9b3fb7fa 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/OnSourceIndexGenerator.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/OnSourceIndexGenerator.java @@ -366,6 +366,11 @@ public Builder addValueColumn(@Nonnull final IndexedColumn keyColumn) { return this; } + @Nonnull + public List getValueColumns() { + return valueColumns; + } + @Nonnull public Builder addIndexOption(@Nonnull final String key, @Nonnull final String value) { indexOptions.put(key, value); 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 a069767b84..9cdac8996b 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 @@ -48,7 +48,6 @@ import com.apple.foundationdb.relational.recordlayer.query.Expressions; import com.apple.foundationdb.relational.recordlayer.query.Identifier; import com.apple.foundationdb.relational.recordlayer.query.LogicalOperators; -import com.apple.foundationdb.relational.recordlayer.query.ParseHelpers; import com.apple.foundationdb.relational.recordlayer.query.ddl.OnSourceIndexGenerator; import com.apple.foundationdb.relational.recordlayer.query.ddl.MaterializedViewIndexGenerator; import com.apple.foundationdb.relational.recordlayer.query.LogicalOperator; @@ -61,6 +60,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import org.antlr.v4.runtime.ParserRuleContext; import javax.annotation.Nonnull; @@ -266,6 +266,8 @@ public RecordLayerIndex visitVectorIndexDefinition(final RelationalParser.Vector var logicalOperator = generateSourceAccessForIndex(sourceIdentifier); getDelegate().getCurrentPlanFragment().setOperator(logicalOperator); + + final Identifier indexId = visitUid(indexDefinitionContext.indexName); final var indexOptions = parseVectorOptions(indexDefinitionContext.vectorIndexOptions()); final var indexGeneratorBuilder = OnSourceIndexGenerator.newBuilder() @@ -279,6 +281,21 @@ public RecordLayerIndex visitVectorIndexDefinition(final RelationalParser.Vector indexGeneratorBuilder.addValueColumn(OnSourceIndexGenerator.IndexedColumn .parseColSpec(colSpec, getDelegate().getIdentifierVisitor()))); + // parse the number of dimensions. + final var indexedColumns = indexGeneratorBuilder.getValueColumns(); + Assert.thatUnchecked(indexedColumns.size() == 1, ErrorCode.UNSUPPORTED_OPERATION, + () -> "invalid number of indexed columns, only one column is supported, found " + indexedColumns.size() + " columns"); + final var indexedCol = Iterables.getOnlyElement(indexedColumns).getIdentifier(); + final var type = getDelegate().getSemanticAnalyzer().resolveIdentifier(indexedCol, getDelegate().getCurrentPlanFragment()) + .getDataType(); + Assert.thatUnchecked(type.getCode() == DataType.Code.VECTOR, ErrorCode.SYNTAX_ERROR, + () -> "indexed column must be of vector type, found '" + type.getCode() + "' instead"); + final var numberOfDimensions = ((DataType.VectorType)type).getDimensions(); + indexGeneratorBuilder.addIndexOption(IndexOptions.HNSW_NUM_DIMENSIONS, String.valueOf(numberOfDimensions)); + + Assert.isNullUnchecked(indexDefinitionContext.includeClause(), ErrorCode.UNSUPPORTED_OPERATION, + "INCLUDE clause is not supported for vector indexes"); + if (indexDefinitionContext.partitionClause() != null) { indexDefinitionContext.partitionClause().indexColumnSpec().forEach(colSpec -> indexGeneratorBuilder.addKeyColumn(OnSourceIndexGenerator.IndexedColumn @@ -313,25 +330,24 @@ private Map parseVectorOptions(@Nullable final RelationalParser. return indexOptionsBuilder.build(); } - for (final var vectorIndexOption : indexOptionsContext.vectorIndexOptions()) { - final var option = vectorIndexOption.vectorIndexOption(); - if (option.HNSW_EF_CONSTRUCTION() != null) { + for (final var option : indexOptionsContext.vectorIndexOption()) { + if (option.EF_CONSTRUCTION() != null) { indexOptionsBuilder.put(IndexOptions.HNSW_EF_CONSTRUCTION, option.efConstruction.getText()); - } else if (option.HNSW_M() != null) { - indexOptionsBuilder.put(IndexOptions.HNSW_M, option.m.getText()); - } else if (option.HNSW_M_MAX() != null) { + } else if (option.CONNECTIVITY() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_M, option.connectivity.getText()); + } else if (option.M_MAX() != null) { indexOptionsBuilder.put(IndexOptions.HNSW_M_MAX, option.mMax.getText()); - } else if (option.HNSW_MAINTAIN_STATS_PROBABILITY() != null) { + } else if (option.MAINTAIN_STATS_PROBABILITY() != null) { indexOptionsBuilder.put(IndexOptions.HNSW_MAINTAIN_STATS_PROBABILITY, option.maintainStatsProbability.getText()); - } else if (option.HNSW_METRIC() != null) { + } else if (option.METRIC() != null) { indexOptionsBuilder.put(IndexOptions.HNSW_METRIC, option.metric.getText()); - } else if (option.HNSW_RABITQ_NUM_EX_BITS() != null) { + } else if (option.RABITQ_NUM_EX_BITS() != null) { indexOptionsBuilder.put(IndexOptions.HNSW_RABITQ_NUM_EX_BITS, option.rabitQNumExBits.getText()); - } else if (option.HNSW_SAMPLE_VECTOR_STATS_PROBABILITY() != null) { + } else if (option.SAMPLE_VECTOR_STATS_PROBABILITY() != null) { indexOptionsBuilder.put(IndexOptions.HNSW_SAMPLE_VECTOR_STATS_PROBABILITY, option.statsProbability.getText()); - } else if (option.HNSW_STATS_THRESHOLD() != null) { + } else if (option.STATS_THRESHOLD() != null) { indexOptionsBuilder.put(IndexOptions.HNSW_STATS_THRESHOLD, option.statsThreshold.getText()); - } else if (option.HNSW_USE_RABITQ() != null) { + } else if (option.USE_RABITQ() != null) { indexOptionsBuilder.put(IndexOptions.HNSW_USE_RABITQ, option.useRabitQ.getText()); } } 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 069f6369d7..943d0873e5 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 @@ -267,6 +267,11 @@ public Object visitVectorIndexOption(final RelationalParser.VectorIndexOptionCon return getDelegate().visitVectorIndexOption(ctx); } + @Override + public Object visitHnswMetric(final RelationalParser.HnswMetricContext ctx) { + return getDelegate().visitHnswMetric(ctx); + } + @Nonnull @Override public Object visitIndexAttributes(@Nonnull RelationalParser.IndexAttributesContext ctx) { diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java index 5588e0106d..59c9cc6676 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java @@ -20,8 +20,7 @@ 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.IndexOptions; import com.apple.foundationdb.record.metadata.IndexTypes; import com.apple.foundationdb.record.metadata.expressions.GroupingKeyExpression; import com.apple.foundationdb.record.metadata.expressions.KeyExpression; @@ -55,6 +54,8 @@ import java.util.Locale; import java.util.function.Consumer; +import static com.apple.foundationdb.record.RecordMetaDataProto.*; +import static com.apple.foundationdb.record.expressions.RecordKeyExpressionProto.*; import static com.apple.foundationdb.record.metadata.Key.Expressions.concat; import static com.apple.foundationdb.record.metadata.Key.Expressions.concatenateFields; import static com.apple.foundationdb.record.metadata.Key.Expressions.field; @@ -288,12 +289,12 @@ void createLegacyIndexWithPredicateIsSupported() throws Exception { indexIs(stmt, field("P", KeyExpression.FanType.None), IndexTypes.VALUE, index -> { assertThat(index.isUnique()).isFalse(); assertThat(index.getName()).isEqualTo("MV1"); - assertThat(index.getPredicate()).isEqualTo(RecordMetaDataProto.Predicate.newBuilder() - .setValuePredicate(RecordMetaDataProto.ValuePredicate.newBuilder().addValue("P") - .setComparison(RecordMetaDataProto.Comparison.newBuilder() - .setSimpleComparison(RecordMetaDataProto.SimpleComparison.newBuilder() - .setType(RecordMetaDataProto.ComparisonType.GREATER_THAN) - .setOperand(RecordKeyExpressionProto.Value.newBuilder().setLongValue(10L).build()) + assertThat(index.getPredicate()).isEqualTo(Predicate.newBuilder() + .setValuePredicate(ValuePredicate.newBuilder().addValue("P") + .setComparison(Comparison.newBuilder() + .setSimpleComparison(SimpleComparison.newBuilder() + .setType(ComparisonType.GREATER_THAN) + .setOperand(Value.newBuilder().setLongValue(10L).build()) .build()) .build()) .build()) @@ -312,12 +313,12 @@ void createIndexWithPredicateIsSupported() throws Exception { // todo (yhatem) verify the predicate. indexIs(stmt, field("P", KeyExpression.FanType.None), IndexTypes.VALUE, index -> { assertThat(index.isUnique()).isFalse(); - assertThat(index.getPredicate()).isEqualTo(RecordMetaDataProto.Predicate.newBuilder() - .setValuePredicate(RecordMetaDataProto.ValuePredicate.newBuilder().addValue("P") - .setComparison(RecordMetaDataProto.Comparison.newBuilder() - .setSimpleComparison(RecordMetaDataProto.SimpleComparison.newBuilder() - .setType(RecordMetaDataProto.ComparisonType.GREATER_THAN) - .setOperand(RecordKeyExpressionProto.Value.newBuilder().setLongValue(10L).build()) + assertThat(index.getPredicate()).isEqualTo(Predicate.newBuilder() + .setValuePredicate(ValuePredicate.newBuilder().addValue("P") + .setComparison(Comparison.newBuilder() + .setSimpleComparison(SimpleComparison.newBuilder() + .setType(ComparisonType.GREATER_THAN) + .setOperand(Value.newBuilder().setLongValue(10L).build()) .build()) .build()) .build()) @@ -1011,14 +1012,179 @@ void createIndexWithOrderByMixedDirection() throws Exception { @Test void createVectorIndexWorksCorrectly() throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + - "CREATE TYPE AS STRUCT A(x bigint) " + - "CREATE TYPE AS STRUCT C(z bigint, k bigint) " + - "CREATE TYPE AS STRUCT B(a A array, c C array) " + "CREATE TABLE T(p bigint, b vector(3, float), c bigint, z bigint, primary key(p))" + "CREATE VIEW V1 AS SELECT p, b, c, z from T where c > 50 " + - "CREATE VECTOR INDEX MV1 ON V1(b) PARTITION BY(z)"; + "CREATE VECTOR INDEX MV1 USING HNSW ON V1(b) PARTITION BY(z)"; indexIs(stmt, keyWithValue(concat(field("Z"), field("B")), 1), - IndexTypes.VECTOR); + IndexTypes.VECTOR, + idx -> { + final var predicate = idx.getPredicate(); + assertThat(predicate).isEqualTo(Predicate.newBuilder() + .setValuePredicate(ValuePredicate + .newBuilder() + .addValue("C") + .setComparison(Comparison + .newBuilder() + .setSimpleComparison(SimpleComparison.newBuilder() + .setType(ComparisonType.GREATER_THAN) + .setOperand(Value.newBuilder().setLongValue(50).build()) + .build()) + .build()) + .build()) + .build()); + }); + } + + @Test + void createVectorIndexWithOptionsWorksCorrectly() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(3, float), c bigint, primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) PARTITION BY (p) " + + "OPTIONS (CONNECTIVITY = 16, M_MAX = 32, EF_CONSTRUCTION = 200, METRIC = COSINE_METRIC)"; + indexIs(stmt, + keyWithValue(concat(field("P"), field("B")), 1), + IndexTypes.VECTOR, + idx -> { + final var options = idx.getOptions(); + Assertions.assertEquals("3", options.get(IndexOptions.HNSW_NUM_DIMENSIONS)); + Assertions.assertEquals("16", options.get(IndexOptions.HNSW_M)); + Assertions.assertEquals("32", options.get(IndexOptions.HNSW_M_MAX)); + Assertions.assertEquals("200", options.get(IndexOptions.HNSW_EF_CONSTRUCTION)); + Assertions.assertEquals("COSINE_METRIC", options.get(IndexOptions.HNSW_METRIC)); + validateVectorIndex(idx); + }); + } + + @Test + void createVectorIndexWithRabitQOptionsWorksCorrectly() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(128, float), primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) PARTITION BY (p) " + + "OPTIONS (USE_RABITQ = true, RABITQ_NUM_EX_BITS = 4, MAINTAIN_STATS_PROBABILITY = 0.01)"; + indexIs(stmt, + keyWithValue(concat(field("P"), field("B")), 1), + IndexTypes.VECTOR, + idx -> { + final var options = idx.getOptions(); + Assertions.assertEquals("128", options.get(IndexOptions.HNSW_NUM_DIMENSIONS)); + Assertions.assertEquals("true", options.get(IndexOptions.HNSW_USE_RABITQ)); + Assertions.assertEquals("4", options.get(IndexOptions.HNSW_RABITQ_NUM_EX_BITS)); + Assertions.assertEquals("0.01", options.get(IndexOptions.HNSW_MAINTAIN_STATS_PROBABILITY)); + validateVectorIndex(idx); + }); + } + + @ParameterizedTest + @ValueSource(ints = {2, 16, 256, 1024}) + void createVectorIndexWithVariousDimensionsWorksCorrectly(int dimensions) throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(" + dimensions + ", float), primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) PARTITION BY (p)"; + indexIs(stmt, keyWithValue(concat(field("P"), field("B")), 1), IndexTypes.VECTOR, + idx -> { + Assertions.assertEquals(String.valueOf(dimensions), idx.getOptions().get(IndexOptions.HNSW_NUM_DIMENSIONS)); + validateVectorIndex(idx); + }); + } + + @ParameterizedTest + @ValueSource(strings = {"EUCLIDEAN_METRIC", "MANHATTAN_METRIC", "DOT_PRODUCT_METRIC", "EUCLIDEAN_SQUARE_METRIC"}) + void createVectorIndexWithAllMetricTypesWorksCorrectly(String metric) throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(512, float), primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) PARTITION BY (p) OPTIONS (METRIC = " + metric + ")"; + + indexIs(stmt, keyWithValue(concat(field("P"), field("B")), 1), IndexTypes.VECTOR, + idx -> { + Assertions.assertEquals("512", idx.getOptions().get(IndexOptions.HNSW_NUM_DIMENSIONS)); + Assertions.assertEquals(metric, idx.getOptions().get(IndexOptions.HNSW_METRIC)); + // Validate using VectorIndexMaintainerFactory validator + validateVectorIndex(idx); + }); + } + + private void validateVectorIndex(RecordLayerIndex recordLayerIndex) { + // Convert RecordLayerIndex to core Index + final var coreIndex = new com.apple.foundationdb.record.metadata.Index( + recordLayerIndex.getName(), + recordLayerIndex.getKeyExpression(), + recordLayerIndex.getIndexType(), + recordLayerIndex.getOptions(), + recordLayerIndex.getPredicate() != null + ? com.apple.foundationdb.record.metadata.IndexPredicate.fromProto(recordLayerIndex.getPredicate()) + : null + ); + + // Validate using VectorIndexHelper - this validates the configuration options + // VectorIndexHelper.getConfig() will throw IllegalArgumentException if options are invalid + Assertions.assertDoesNotThrow(() -> + com.apple.foundationdb.record.provider.foundationdb.indexes.VectorIndexHelper.getConfig(coreIndex), + "Vector index configuration should be valid"); + } + + @Test + void createVectorIndexWithStatsOptionsWorksCorrectly() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(64, float), primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) PARTITION BY (p) " + + "OPTIONS (SAMPLE_VECTOR_STATS_PROBABILITY = 0.05)"; + indexIs(stmt, + keyWithValue(concat(field("P"), field("B")), 1), + IndexTypes.VECTOR, + idx -> { + final var options = idx.getOptions(); + Assertions.assertEquals("64", options.get(IndexOptions.HNSW_NUM_DIMENSIONS)); + Assertions.assertEquals("0.05", options.get(IndexOptions.HNSW_SAMPLE_VECTOR_STATS_PROBABILITY)); + validateVectorIndex(idx); + }); + } + + @Test + void createVectorIndexOnMultipleColumnsFails() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(3, float), c vector(3, float), primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b, c) PARTITION BY (p)"; + shouldFailWith(stmt, ErrorCode.UNSUPPORTED_OPERATION, "invalid number of indexed columns, only one column is supported"); + } + + @Test + void createVectorIndexOnNonVectorColumnFails() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b bigint, primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) PARTITION BY (p)"; + shouldFailWith(stmt, ErrorCode.SYNTAX_ERROR, "indexed column must be of vector type"); + } + + @Test + void createVectorIndexOnStringColumnFails() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b string, primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) PARTITION BY (p)"; + shouldFailWith(stmt, ErrorCode.SYNTAX_ERROR, "indexed column must be of vector type"); + } + + @Test + void createVectorIndexWithIncludeClauseFails() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(3, float), c bigint, d string, primary key(p)) " + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) INCLUDE (c, d) PARTITION BY (p) "; + shouldFailWith(stmt, ErrorCode.UNSUPPORTED_OPERATION, "INCLUDE clause is not supported for vector indexes"); + } + + @Test + void createVectorIndexWithIncludeClauseAndPartitionFails() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(3, float), c bigint, z bigint, primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) INCLUDE (c) PARTITION BY(z)"; + shouldFailWith(stmt, ErrorCode.UNSUPPORTED_OPERATION, "INCLUDE clause is not supported for vector indexes"); + } + + @Test + void createVectorIndexWithIncludeClauseAndOptionsFails() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(3, float), c bigint, primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) INCLUDE (c) PARTITION BY (p) OPTIONS (CONNECTIVITY = 16)"; + shouldFailWith(stmt, ErrorCode.UNSUPPORTED_OPERATION, "INCLUDE clause is not supported for vector indexes"); } } From 2d4fc70c6837ce6b10875e543017ae031cd683e2 Mon Sep 17 00:00:00 2001 From: Youssef Hatem Date: Thu, 27 Nov 2025 18:20:52 +0000 Subject: [PATCH 4/7] address post merge conflicts, fix code style violations. --- .../GenerateVisitorAnnotationHelper.java | 119 +--- .../query/plan/cascades/values/Value.java | 27 - .../metadata/RecordLayerIndex.java | 11 - .../ddl/MaterializedViewIndexGenerator.java | 10 +- .../query/ddl/OnSourceIndexGenerator.java | 39 +- .../query/visitors/BaseVisitor.java | 23 - .../query/visitors/DdlVisitor.java | 11 +- .../query/visitors/DelegatingVisitor.java | 2 +- .../relational/api/ddl/IndexTest.java | 20 +- .../query/DelegatingVisitorTest.java | 536 ++++++++++++------ .../src/test/java/YamlIntegrationTests.java | 6 + .../index-ddl-values-only.metrics.binpb | 411 ++++++++++++++ .../index-ddl-values-only.metrics.yaml | 429 ++++++++++++++ .../resources/index-ddl-values-only.yamsql | 323 +++++++++++ .../test/resources/index-ddl.metrics.binpb | 219 +++++-- .../src/test/resources/index-ddl.metrics.yaml | 244 ++++++-- .../src/test/resources/index-ddl.yamsql | 88 +-- 17 files changed, 2012 insertions(+), 506 deletions(-) create mode 100644 yaml-tests/src/test/resources/index-ddl-values-only.metrics.binpb create mode 100644 yaml-tests/src/test/resources/index-ddl-values-only.metrics.yaml create mode 100644 yaml-tests/src/test/resources/index-ddl-values-only.yamsql 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 93171a7776..956fbfce87 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,7 +20,6 @@ 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; @@ -44,13 +43,11 @@ 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; @@ -58,7 +55,6 @@ 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 @@ -106,8 +102,8 @@ static boolean process(final ProcessingEnvironment processingEnv, Set packageElement.getEnclosedElements().stream()) - .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)) + .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)) @@ -156,11 +152,10 @@ 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(packageName, interfaceName), WildcardTypeName.subtypeOf(Object.class)), + ParameterizedTypeName.get(ClassName.get(packageElement.getQualifiedName().toString(), interfaceName), WildcardTypeName.subtypeOf(Object.class)), TypeName.get(rootTypeMirror), WildcardTypeName.subtypeOf(Object.class))), "jumpMap", Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL); @@ -168,7 +163,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(" + getRawTypeName(typeMirror, packageName) + ".class, (visitor, element) -> visitor." + methodNameOfVisitMethod(generateVisitor, typeElement) + "((" + getWildcardTypeName(typeMirror, packageName) + ")element))"; + return "Map.entry(" + typeElement.getSimpleName() + ".class, (visitor, element) -> visitor." + methodNameOfVisitMethod(generateVisitor, typeElement) + "((" + typeElement.getSimpleName() + ")element))"; }) .collect(Collectors.joining(", \n")); @@ -177,7 +172,6 @@ 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()); @@ -189,7 +183,7 @@ private static void generateInterface(@Nonnull final Types typeUtils, .methodBuilder(methodName) .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) .addAnnotation(Nonnull.class) - .addParameter(ParameterSpec.builder(getWildcardTypeName(typeMirror, packageName), parameterName).addAnnotation(Nonnull.class).build()) + .addParameter(ParameterSpec.builder(TypeName.get(typeMirror), parameterName).addAnnotation(Nonnull.class).build()) .returns(typeVariableName); typeBuilder.addMethod(specificVisitMethodBuilder.build()); } @@ -199,7 +193,7 @@ private static void generateInterface(@Nonnull final Types typeUtils, .methodBuilder(defaultMethodName) .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) .addAnnotation(Nonnull.class) - .addParameter(ParameterSpec.builder(getWildcardTypeName(rootTypeMirror, packageName), parameterName).addAnnotation(Nonnull.class).build()) + .addParameter(ParameterSpec.builder(TypeName.get(rootTypeMirror), parameterName).addAnnotation(Nonnull.class).build()) .returns(typeVariableName); typeBuilder.addMethod(visitDefaultMethodBuilder.build()); @@ -222,105 +216,6 @@ 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, @@ -345,7 +240,7 @@ private static void generateImplementationWithDefaults(@Nonnull final Types type .addModifiers(Modifier.PUBLIC, Modifier.DEFAULT) .addAnnotation(Nonnull.class) .addAnnotation(Override.class) - .addParameter(ParameterSpec.builder(getWildcardTypeName(typeMirror, packageElement.getQualifiedName().toString()), parameterName).addAnnotation(Nonnull.class).build()) + .addParameter(ParameterSpec.builder(TypeName.get(typeMirror), 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/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 aba20fe2c1..0b5da02fef 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,7 +21,6 @@ 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; @@ -92,7 +91,6 @@ * A scalar value type. */ @API(API.Status.EXPERIMENTAL) -@GenerateVisitor public interface Value extends Correlated, TreeLike, UsesValueEquivalence, PlanHashable, Typed, Narrowable, PlanSerializable { @Nonnull @@ -311,31 +309,6 @@ default Value translateCorrelations(@Nonnull final TranslationMap translationMap }, false).orElseThrow(() -> new RecordCoreException("unable to map tree")); } - @Nonnull - @SuppressWarnings("PMD.CompareObjectsWithEquals") - default Value translateCorrelationsRecursively(@Nonnull final TranslationMap translationMap) { - if (translationMap.definesOnlyIdentities()) { - return this; - } - return replaceLeavesMaybe(value -> { - if (value instanceof LeafValue) { - final var leafValue = (LeafValue)value; - final var correlatedTo = value.getCorrelatedTo(); - if (correlatedTo.isEmpty()) { - return leafValue; - } - - Verify.verify(correlatedTo.size() == 1); - final var sourceAlias = Iterables.getOnlyElement(correlatedTo); - return translationMap.containsSourceAlias(sourceAlias) - ? translationMap.applyTranslationFunction(sourceAlias, leafValue) - : leafValue; - } - Verify.verify(value.getCorrelatedTo().isEmpty()); - return value; - }, true).orElseThrow(() -> new RecordCoreException("unable to map tree")); - } - @Nonnull default V narrow(@Nonnull Class narrowedClass) { return narrowedClass.cast(this); 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 7cb8a7ac2f..e4b2aa8492 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 @@ -123,17 +123,6 @@ public Map getOptions() { return options; } - @Nonnull - public Builder toBuilder() { - return newBuilder().setName(getName()) - .setIndexType(getIndexType()) - .setTableName(getTableName()) - .setUnique(isUnique()) - .setKeyExpression(getKeyExpression()) - .setPredicate(getPredicate()) - .setOptions(getOptions()); - } - @Nonnull public static RecordLayerIndex from(@Nonnull final String tableName, @Nonnull String tableStorageName, @Nonnull final com.apple.foundationdb.record.metadata.Index index) { final var indexProto = index.toProto(); diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/MaterializedViewIndexGenerator.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/MaterializedViewIndexGenerator.java index ad111e5201..52df78a46e 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/MaterializedViewIndexGenerator.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/MaterializedViewIndexGenerator.java @@ -144,7 +144,8 @@ private MaterializedViewIndexGenerator(@Nonnull RelationalExpression relationalE } @Nonnull - public RecordLayerIndex.Builder generate(@Nonnull RecordLayerSchemaTemplate.Builder schemaTemplateBuilder, @Nonnull String indexName, boolean isUnique, boolean containsNullableArray) { + public RecordLayerIndex.Builder generate(@Nonnull RecordLayerSchemaTemplate.Builder schemaTemplateBuilder, @Nonnull String indexName, + boolean isUnique, boolean containsNullableArray, boolean generateKeyValueExpressionWithEmptyKey) { final String recordTypeName = getRecordTypeName(); // Have to use the storage name here because the index generator uses it final Type.Record tableType = schemaTemplateBuilder.findTableByStorageName(recordTypeName).getType(); @@ -189,7 +190,10 @@ public RecordLayerIndex.Builder generate(@Nonnull RecordLayerSchemaTemplate.Buil } final var reordered = reorderValues(fieldValues, orderByValues); final var expression = generate(reordered, orderingFunctions); - final var splitPoint = orderByValues.isEmpty() ? -1 : orderByValues.size(); + var splitPoint = orderByValues.size(); + if (orderByValues.isEmpty() && !generateKeyValueExpressionWithEmptyKey) { + splitPoint = -1; + } if (splitPoint != -1 && splitPoint < fieldValues.size()) { indexBuilder.setKeyExpression(KeyExpression.fromProto(NullableArrayUtils.wrapArray(keyWithValue(expression, splitPoint).toKeyExpression(), tableType, containsNullableArray))); } else { @@ -232,7 +236,7 @@ public RecordLayerIndex.Builder generate(@Nonnull RecordLayerSchemaTemplate.Buil if (IndexTypes.PERMUTED_MIN.equals(indexType) || IndexTypes.PERMUTED_MAX.equals(indexType)) { int permutedSize = aggregateOrderIndex < 0 ? 0 : (fieldValues.size() - aggregateOrderIndex); indexBuilder.setOption(IndexOptions.PERMUTED_SIZE_OPTION, permutedSize); - } else if (aggregateOrderIndex >= 0) { + } else if (aggregateOrderIndex > 0) { Assert.failUnchecked(ErrorCode.UNSUPPORTED_OPERATION, "Unsupported index definition. Cannot order " + indexType + " index by aggregate value"); } } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/OnSourceIndexGenerator.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/OnSourceIndexGenerator.java index 5c9b3fb7fa..19e700e197 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/OnSourceIndexGenerator.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/OnSourceIndexGenerator.java @@ -33,7 +33,6 @@ import com.apple.foundationdb.relational.generated.RelationalParser; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerIndex; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerSchemaTemplate; -import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerTable; import com.apple.foundationdb.relational.recordlayer.query.Expression; import com.apple.foundationdb.relational.recordlayer.query.Expressions; import com.apple.foundationdb.relational.recordlayer.query.Identifier; @@ -126,13 +125,19 @@ public final class OnSourceIndexGenerator { private final boolean useNullableArrays; + private final boolean generateKeyValueExpressionWithEmptyKey; + @Nonnull private final Map indexOptions; + @Nonnull + private final RecordLayerSchemaTemplate.Builder metadataBuilder; + public OnSourceIndexGenerator(@Nonnull final Identifier indexName, @Nonnull final LogicalPlanFragment source, @Nonnull final List keyColumns, @Nonnull final List valueColumns, final boolean isUnique, final boolean useLegacyExtremum, final boolean useNullableArrays, - @Nonnull final Map indexOptions) { + final boolean generateKeyValueExpressionWithEmptyKey, @Nonnull final Map indexOptions, + @Nonnull final RecordLayerSchemaTemplate.Builder metadataBuilder) { this.indexName = indexName; this.source = source; this.keyColumns = ImmutableList.copyOf(keyColumns); @@ -140,7 +145,9 @@ public OnSourceIndexGenerator(@Nonnull final Identifier indexName, @Nonnull fina this.isUnique = isUnique; this.useLegacyExtremum = useLegacyExtremum; this.useNullableArrays = useNullableArrays; + this.generateKeyValueExpressionWithEmptyKey = generateKeyValueExpressionWithEmptyKey; this.indexOptions = ImmutableMap.copyOf(indexOptions); + this.metadataBuilder = metadataBuilder; } /** @@ -159,11 +166,10 @@ public OnSourceIndexGenerator(@Nonnull final Identifier indexName, @Nonnull fina * The generated index will be ordered according to the key columns and can optionally enforce uniqueness * if configured via {@link Builder#setUnique(boolean)}. * - * @param catalog the schema catalog used to resolve table types for the index * @return a fully configured {@link RecordLayerIndex} ready to be added to the schema */ @Nonnull - public RecordLayerIndex.Builder generate(@Nonnull final RecordLayerSchemaTemplate catalog) { + public RecordLayerIndex.Builder generate() { final var keyIdentifiers = keyColumns.stream().map(IndexedColumn::getIdentifier).collect(ImmutableList.toImmutableList()); final var keyIdentifiersAsSet = ImmutableSet.copyOf(keyIdentifiers); final var valueIdentifiers = valueColumns.stream().map(IndexedColumn::getIdentifier) @@ -218,8 +224,7 @@ public RecordLayerIndex.Builder generate(@Nonnull final RecordLayerSchemaTemplat Quantifier.forEach(Reference.initialOf(newSelectExpression))); final var indexPlan = LogicalOperator.generateSort(resultingOperator, orderByExpressions, ImmutableSet.of(), Optional.empty()); final var indexGenerator = MaterializedViewIndexGenerator.from(indexPlan.getQuantifier().getRangesOver().get(), useLegacyExtremum); - final var tableType = Assert.castUnchecked(catalog.findTableByName(indexGenerator.getRecordTypeName()).get(), RecordLayerTable.class); - final var indexBuilder = indexGenerator.generate(indexName.toString(), isUnique, tableType.getType(), useNullableArrays); + final var indexBuilder = indexGenerator.generate(metadataBuilder, indexName.toString(), isUnique, useNullableArrays, generateKeyValueExpressionWithEmptyKey); indexBuilder.addAllOptions(indexOptions); return indexBuilder; } @@ -330,6 +335,10 @@ public static final class Builder { private boolean useNullableArrays; + private boolean generateKeyValueExpressionWithEmptyKey; + + private RecordLayerSchemaTemplate.Builder metadataBuilder; + private Builder() { this.keyColumns = new ArrayList<>(); this.valueColumns = new ArrayList<>(); @@ -401,14 +410,28 @@ public Builder setUseNullableArrays(boolean useNullableArrays) { return this; } + @Nonnull + public Builder setGenerateKeyValueExpressionWithEmptyKey(boolean generateKeyValueExpressionWithEmptyKey) { + this.generateKeyValueExpressionWithEmptyKey = generateKeyValueExpressionWithEmptyKey; + return this; + } + + + @Nonnull + public Builder setMetadataBuilder(final RecordLayerSchemaTemplate.Builder metadataBuilder) { + this.metadataBuilder = metadataBuilder; + return this; + } + @Nonnull public OnSourceIndexGenerator build() { Assert.notNullUnchecked(indexName); Assert.notNullUnchecked(indexSource); Assert.notNullUnchecked(semanticAnalyzer); - Assert.thatUnchecked(!keyColumns.isEmpty()); + Assert.notNullUnchecked(metadataBuilder); return new OnSourceIndexGenerator(indexName, indexSource, keyColumns, valueColumns, - isUnique, useLegacyExtremum, useNullableArrays, indexOptions); + isUnique, useLegacyExtremum, useNullableArrays, generateKeyValueExpressionWithEmptyKey, + indexOptions, metadataBuilder); } } } 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 4e0c75f00a..caee5b1a1e 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 @@ -419,24 +419,6 @@ public RecordLayerIndex visitVectorIndexDefinition(final RelationalParser.Vector return ddlVisitor.visitVectorIndexDefinition(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 public Object visitIndexAttributes(RelationalParser.IndexAttributesContext ctx) { return visitChildren(ctx); @@ -1721,9 +1703,4 @@ 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 d501ee985a..6fef957b4c 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 @@ -214,7 +214,7 @@ public RecordLayerIndex visitIndexAsSelectDefinition(@Nonnull RelationalParser.I final var isUnique = indexDefinitionContext.UNIQUE() != null; final var generator = MaterializedViewIndexGenerator.from(viewPlan, useLegacyBasedExtremumEver); 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(metadataBuilder, indexId.getName(), isUnique, containsNullableArray).build(); + return generator.generate(metadataBuilder, indexId.getName(), isUnique, containsNullableArray, false).build(); } @Nonnull @@ -238,6 +238,7 @@ public RecordLayerIndex visitIndexOnSourceDefinition(@Nonnull final RelationalPa .setSemanticAnalyzer(getDelegate().getSemanticAnalyzer()) .setUseLegacyExtremum(useLegacyExtremum) .setUseNullableArrays(containsNullableArray) + .setMetadataBuilder(metadataBuilder) .setUnique(isUnique); indexDefinitionContext.indexColumnList().indexColumnSpec().forEach(colSpec -> @@ -252,7 +253,7 @@ public RecordLayerIndex visitIndexOnSourceDefinition(@Nonnull final RelationalPa } getDelegate().popPlanFragment(); - return indexGeneratorBuilder.build().generate(ddlCatalog).build(); + return indexGeneratorBuilder.build().generate().build(); } @Nonnull @@ -265,8 +266,6 @@ public RecordLayerIndex visitVectorIndexDefinition(final RelationalParser.Vector var logicalOperator = generateSourceAccessForIndex(sourceIdentifier); getDelegate().getCurrentPlanFragment().setOperator(logicalOperator); - - final Identifier indexId = visitUid(indexDefinitionContext.indexName); final var indexOptions = parseVectorOptions(indexDefinitionContext.vectorIndexOptions()); final var indexGeneratorBuilder = OnSourceIndexGenerator.newBuilder() @@ -274,6 +273,8 @@ public RecordLayerIndex visitVectorIndexDefinition(final RelationalParser.Vector .setIndexSource(getDelegate().getCurrentPlanFragment()) .setSemanticAnalyzer(getDelegate().getSemanticAnalyzer()) .addAllIndexOptions(indexOptions) + .setMetadataBuilder(metadataBuilder) + .setGenerateKeyValueExpressionWithEmptyKey(true) .setUseNullableArrays(containsNullableArray); indexDefinitionContext.indexColumnList().indexColumnSpec().forEach(colSpec -> @@ -302,7 +303,7 @@ public RecordLayerIndex visitVectorIndexDefinition(final RelationalParser.Vector } getDelegate().popPlanFragment(); - return indexGeneratorBuilder.build().generate(ddlCatalog).setIndexType(IndexTypes.VECTOR).build(); + return indexGeneratorBuilder.build().generate().setIndexType(IndexTypes.VECTOR).build(); } @Nonnull 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 943d0873e5..7661f768b8 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 @@ -1426,7 +1426,7 @@ public Object visitWindowName(@Nonnull RelationalParser.WindowNameContext ctx) { @Override public Object visitPartitionClause(final RelationalParser.PartitionClauseContext ctx) { - return null; + return getDelegate().visitPartitionClause(ctx); } @Nonnull diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java index 59c9cc6676..2fcda7611a 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java @@ -54,8 +54,12 @@ import java.util.Locale; import java.util.function.Consumer; -import static com.apple.foundationdb.record.RecordMetaDataProto.*; -import static com.apple.foundationdb.record.expressions.RecordKeyExpressionProto.*; +import static com.apple.foundationdb.record.RecordMetaDataProto.Comparison; +import static com.apple.foundationdb.record.RecordMetaDataProto.ComparisonType; +import static com.apple.foundationdb.record.RecordMetaDataProto.Predicate; +import static com.apple.foundationdb.record.RecordMetaDataProto.SimpleComparison; +import static com.apple.foundationdb.record.RecordMetaDataProto.ValuePredicate; +import static com.apple.foundationdb.record.expressions.RecordKeyExpressionProto.Value; import static com.apple.foundationdb.record.metadata.Key.Expressions.concat; import static com.apple.foundationdb.record.metadata.Key.Expressions.concatenateFields; import static com.apple.foundationdb.record.metadata.Key.Expressions.field; @@ -1036,6 +1040,18 @@ void createVectorIndexWorksCorrectly() throws Exception { }); } + + @Test + void createVectorIndexWithoutPartitionClauseWorksCorrectly() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(3, float), c bigint, z bigint, primary key(p))" + + "CREATE VIEW V1 AS SELECT p, b, c, z from T " + + "CREATE VECTOR INDEX MV1 USING HNSW ON V1(b)"; + indexIs(stmt, + keyWithValue(field("B"), 0), + IndexTypes.VECTOR); + } + @Test void createVectorIndexWithOptionsWorksCorrectly() throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/DelegatingVisitorTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/DelegatingVisitorTest.java index 211086ee23..1898e33214 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/DelegatingVisitorTest.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/DelegatingVisitorTest.java @@ -29,6 +29,7 @@ import com.apple.foundationdb.relational.generated.RelationalParser; import com.apple.foundationdb.relational.recordlayer.ddl.NoOpMetadataOperationsFactory; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerColumn; +import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerIndex; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerSchemaTemplate; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerTable; import com.apple.foundationdb.relational.recordlayer.query.visitors.BaseVisitor; @@ -42,6 +43,8 @@ import javax.annotation.Nonnull; import java.net.URI; +import java.util.function.BiConsumer; +import java.util.function.Function; import java.util.stream.Stream; /** @@ -57,10 +60,6 @@ */ public class DelegatingVisitorTest { - static Stream traversalStrings() { - return Stream.of("TRAVERSAL ORDER PRE_ORDER", "TRAVERSAL ORDER LEVEL_ORDER"); - } - @Nonnull private static RecordLayerSchemaTemplate generateMetadata() { return RecordLayerSchemaTemplate @@ -79,208 +78,401 @@ private static RecordLayerSchemaTemplate generateMetadata() { .build(); } - @Test - void visitPredicatedExpressionTest() { - final var query = "X BETWEEN 32 AND 43"; - final MutableBoolean baseVisitorCalled = new MutableBoolean(false); - final var baseVisitor = new BaseVisitor( - new MutablePlanGenerationContext(PreparedParams.empty(), - PlanHashable.PlanHashMode.VC0, query, query, 42), - generateMetadata(), - NoOpQueryFactory.INSTANCE, - NoOpMetadataOperationsFactory.INSTANCE, - URI.create("/FDB/FRL1"), - false) { - @Nonnull - @Override - public Expression visitPredicatedExpression(@Nonnull final RelationalParser.PredicatedExpressionContext ctx) { - baseVisitorCalled.setTrue(); - return Expression.ofUnnamed(LiteralValue.ofScalar(42)); - } - }; - final var delegatingVisitor = new DelegatingVisitor<>(baseVisitor); + /** + * Generic test helper for visitor methods that return Object or void. + * + * @param query the SQL query to parse + * @param parseMethod parser method to extract context + * @param visitMethod delegating visitor method to test + * @param visitorOverride function to create a BaseVisitor with overridden method + */ + private void testVisitor(String query, + Function parseMethod, + BiConsumer, T> visitMethod, + Function visitorOverride) { + final MutableBoolean called = new MutableBoolean(false); + final var visitor = visitorOverride.apply(called); + final var delegatingVisitor = new DelegatingVisitor<>(visitor); + final var context = parseQuery(query, parseMethod); + visitMethod.accept(delegatingVisitor, context); + Assertions.assertThat(called.booleanValue()).as("Expecting the method to be called").isTrue(); + } + + /** + * Simplified helper for most common case: parse, visit, assert called. + */ + private void testSimple(String query, + Function parseMethod, + BiConsumer, T> visitMethod, + Function visitorOverride) { + testVisitor(query, parseMethod, visitMethod, visitorOverride); + } + + private T parseQuery(String query, Function parseMethod) { final var tokenSource = new RelationalLexer(new CaseInsensitiveCharStream(query)); final var parser = new RelationalParser(new CommonTokenStream(tokenSource)); - final var predicatedExpression = (RelationalParser.PredicatedExpressionContext)parser.expression(); - delegatingVisitor.visitPredicatedExpression(predicatedExpression); - Assertions.assertThat(baseVisitorCalled.booleanValue()).isTrue(); + return parseMethod.apply(parser); + } + + private BaseVisitor createBaseVisitor(String query, MutableBoolean called) { + return new BaseVisitor( + new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, query, query, 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, + URI.create("/FDB/FRL1"), false); } @Test - void visitSubscriptExpressionTest() { - final var query = "X[42]"; - final MutableBoolean baseVisitorCalled = new MutableBoolean(false); - final var baseVisitor = new BaseVisitor( - new MutablePlanGenerationContext(PreparedParams.empty(), - PlanHashable.PlanHashMode.VC0, query, query, 42), - generateMetadata(), - NoOpQueryFactory.INSTANCE, - NoOpMetadataOperationsFactory.INSTANCE, - URI.create("/FDB/FRL1"), - false) { + void visitPredicatedExpressionTest() { + testSimple("X BETWEEN 32 AND 43", + RelationalParser::expression, + (visitor, ctx) -> visitor.visitPredicatedExpression((RelationalParser.PredicatedExpressionContext) ctx), + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Nonnull + @Override + public Expression visitPredicatedExpression(@Nonnull RelationalParser.PredicatedExpressionContext ctx) { + called.setTrue(); + return Expression.ofUnnamed(LiteralValue.ofScalar(42)); + } + }); + } - @Override - public Expression visitSubscriptExpression(@Nonnull final RelationalParser.SubscriptExpressionContext ctx) { - baseVisitorCalled.setTrue(); - return Expression.ofUnnamed(LiteralValue.ofScalar(42)); - } - }; - final var delegatingVisitor = new DelegatingVisitor<>(baseVisitor); - final var tokenSource = new RelationalLexer(new CaseInsensitiveCharStream(query)); - final var parser = new RelationalParser(new CommonTokenStream(tokenSource)); - final var predicatedExpression = (RelationalParser.SubscriptExpressionContext)parser.expressionAtom(); - delegatingVisitor.visitSubscriptExpression(predicatedExpression); - Assertions.assertThat(baseVisitorCalled.booleanValue()).isTrue(); + @Test + void visitSubscriptExpressionTest() { + testSimple("X[42]", + RelationalParser::expressionAtom, + (visitor, ctx) -> visitor.visitSubscriptExpression((RelationalParser.SubscriptExpressionContext) ctx), + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Expression visitSubscriptExpression(@Nonnull RelationalParser.SubscriptExpressionContext ctx) { + called.setTrue(); + return Expression.ofUnnamed(LiteralValue.ofScalar(42)); + } + }); } @Test void visitUserDefinedScalarFunctionStatementBodyTest() { final var query = "AS testIdentifier"; - final MutableBoolean baseVisitorCalled = new MutableBoolean(false); - final var baseVisitor = new BaseVisitor( - new MutablePlanGenerationContext(PreparedParams.empty(), - PlanHashable.PlanHashMode.VC0, query, query, 42), - generateMetadata(), - NoOpQueryFactory.INSTANCE, - NoOpMetadataOperationsFactory.INSTANCE, - URI.create("/FDB/FRL1"), - false) { - + final MutableBoolean called = new MutableBoolean(false); + final var visitor = new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, query, query, 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { @Nonnull @Override - public Identifier visitUserDefinedScalarFunctionStatementBody(@Nonnull final RelationalParser.UserDefinedScalarFunctionStatementBodyContext ctx) { - baseVisitorCalled.setTrue(); + public Identifier visitUserDefinedScalarFunctionStatementBody(@Nonnull RelationalParser.UserDefinedScalarFunctionStatementBodyContext ctx) { + called.setTrue(); return Identifier.of("testIdentifier"); } }; - final var delegatingVisitor = new DelegatingVisitor<>(baseVisitor); - final var tokenSource = new RelationalLexer(new CaseInsensitiveCharStream(query)); - final var parser = new RelationalParser(new CommonTokenStream(tokenSource)); - final var routineBodyContext = parser.routineBody(); - - if (routineBodyContext instanceof RelationalParser.UserDefinedScalarFunctionStatementBodyContext) { - final var userDefinedScalarFunctionStatementBodyContext = (RelationalParser.UserDefinedScalarFunctionStatementBodyContext)routineBodyContext; - final var result = delegatingVisitor.visitUserDefinedScalarFunctionStatementBody(userDefinedScalarFunctionStatementBodyContext); - Assertions.assertThat(result).isEqualTo(Identifier.of("testIdentifier")); - } else { - Assertions.fail("Expected UserDefinedScalarFunctionStatementBodyContext but got " + routineBodyContext.getClass().getSimpleName()); - } - - Assertions.assertThat(baseVisitorCalled.booleanValue()).isTrue(); + final var delegatingVisitor = new DelegatingVisitor<>(visitor); + final var routineBodyContext = parseQuery(query, RelationalParser::routineBody); + Assertions.assertThat(routineBodyContext).isInstanceOf(RelationalParser.UserDefinedScalarFunctionStatementBodyContext.class); + final var result = delegatingVisitor.visitUserDefinedScalarFunctionStatementBody( + (RelationalParser.UserDefinedScalarFunctionStatementBodyContext) routineBodyContext); + Assertions.assertThat(result).isEqualTo(Identifier.of("testIdentifier")); + Assertions.assertThat(called.booleanValue()).isTrue(); } @Test void visitUserDefinedScalarFunctionNameTest() { - final var query = "fake query"; - final MutableBoolean baseVisitorCalled = new MutableBoolean(false); - final var baseVisitor = new BaseVisitor( - new MutablePlanGenerationContext(PreparedParams.empty(), - PlanHashable.PlanHashMode.VC0, query, query, 42), - generateMetadata(), - NoOpQueryFactory.INSTANCE, - NoOpMetadataOperationsFactory.INSTANCE, - URI.create("/FDB/FRL1"), - false) { - - @Nonnull - @Override - public String visitUserDefinedScalarFunctionName(@Nonnull final RelationalParser.UserDefinedScalarFunctionNameContext ctx) { - baseVisitorCalled.setTrue(); - return "testFunction"; - } - }; - final var delegatingVisitor = new DelegatingVisitor<>(baseVisitor); - final var tokenSource = new RelationalLexer(new CaseInsensitiveCharStream(query)); - final var parser = new RelationalParser(new CommonTokenStream(tokenSource)); - Assertions.assertThat(delegatingVisitor.visitUserDefinedScalarFunctionName(parser.userDefinedScalarFunctionName())).isEqualTo("testFunction"); - Assertions.assertThat(baseVisitorCalled.booleanValue()).isTrue(); + testSimple("fake query", + RelationalParser::userDefinedScalarFunctionName, + (visitor, ctx) -> Assertions.assertThat(visitor.visitUserDefinedScalarFunctionName(ctx)).isEqualTo("testFunction"), + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Nonnull + @Override + public String visitUserDefinedScalarFunctionName(@Nonnull RelationalParser.UserDefinedScalarFunctionNameContext ctx) { + called.setTrue(); + return "testFunction"; + } + }); } + static Stream traversalStrings() { + return Stream.of("TRAVERSAL ORDER PRE_ORDER", "TRAVERSAL ORDER LEVEL_ORDER"); + } @ParameterizedTest @MethodSource("traversalStrings") void visitTraversalExpressionTest(String query) { - final MutableBoolean baseVisitorCalled = new MutableBoolean(false); - final var baseVisitor = new BaseVisitor( - new MutablePlanGenerationContext(PreparedParams.empty(), - PlanHashable.PlanHashMode.VC0, query, query, 42), - generateMetadata(), - NoOpQueryFactory.INSTANCE, - NoOpMetadataOperationsFactory.INSTANCE, - URI.create("/FDB/FRL1"), - false) { - - public Object visitTraversalOrderClause(final RelationalParser.TraversalOrderClauseContext ctx) { - baseVisitorCalled.setTrue(); - if (query.equals("TRAVERSAL ORDER LEVEL_ORDER")) { - return RecursiveUnionExpression.TraversalStrategy.LEVEL; - } else { - return RecursiveUnionExpression.TraversalStrategy.PREORDER; - } - } - }; - - final var delegatingVisitor = new DelegatingVisitor<>(baseVisitor); - final var tokenSource = new RelationalLexer(new CaseInsensitiveCharStream(query)); - final var parser = new RelationalParser(new CommonTokenStream(tokenSource)); - final var predicatedExpression = parser.traversalOrderClause(); - delegatingVisitor.visitTraversalOrderClause(predicatedExpression); - Assertions.assertThat(baseVisitorCalled.booleanValue()).isTrue(); + testSimple(query, + RelationalParser::traversalOrderClause, + DelegatingVisitor::visitTraversalOrderClause, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, query, query, 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Object visitTraversalOrderClause(RelationalParser.TraversalOrderClauseContext ctx) { + called.setTrue(); + return query.equals("TRAVERSAL ORDER LEVEL_ORDER") + ? RecursiveUnionExpression.TraversalStrategy.LEVEL + : RecursiveUnionExpression.TraversalStrategy.PREORDER; + } + }); } @Test void visitViewDefinitionTest() { - final var query = "VIEW V AS SELECT * FROM T"; - final MutableBoolean baseVisitorCalled = new MutableBoolean(false); - final var baseVisitor = new BaseVisitor( - new MutablePlanGenerationContext(PreparedParams.empty(), - PlanHashable.PlanHashMode.VC0, query, query, 42), - generateMetadata(), - NoOpQueryFactory.INSTANCE, - NoOpMetadataOperationsFactory.INSTANCE, - URI.create("/FDB/FRL1"), - false) { - - public Object visitViewDefinition(final RelationalParser.ViewDefinitionContext ctx) { - baseVisitorCalled.setTrue(); - return null; - } - }; - - final var delegatingVisitor = new DelegatingVisitor<>(baseVisitor); - final var tokenSource = new RelationalLexer(new CaseInsensitiveCharStream(query)); - final var parser = new RelationalParser(new CommonTokenStream(tokenSource)); - final var viewDefinitionContext = parser.viewDefinition(); - delegatingVisitor.visitViewDefinition(viewDefinitionContext); - Assertions.assertThat(baseVisitorCalled.booleanValue()).isTrue(); + testSimple("VIEW V AS SELECT * FROM T", + RelationalParser::viewDefinition, + DelegatingVisitor::visitViewDefinition, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Object visitViewDefinition(RelationalParser.ViewDefinitionContext ctx) { + called.setTrue(); + return null; + } + }); } @Test void visitUserDefinedScalarFunctionCallTest() { - final var query = "myFunction(123)"; - final MutableBoolean baseVisitorCalled = new MutableBoolean(false); - final var baseVisitor = new BaseVisitor( - new MutablePlanGenerationContext(PreparedParams.empty(), - PlanHashable.PlanHashMode.VC0, query, query, 42), - generateMetadata(), - NoOpQueryFactory.INSTANCE, - NoOpMetadataOperationsFactory.INSTANCE, - URI.create("/FDB/FRL1"), - false) { + testSimple("myFunction(123)", + RelationalParser::functionCall, + (visitor, ctx) -> visitor.visitUserDefinedScalarFunctionCall((RelationalParser.UserDefinedScalarFunctionCallContext) ctx), + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Nonnull + @Override + public Expression visitUserDefinedScalarFunctionCall(@Nonnull RelationalParser.UserDefinedScalarFunctionCallContext ctx) { + called.setTrue(); + return Expression.ofUnnamed(LiteralValue.ofScalar(42)); + } + }); + } - @Nonnull - @Override - public Expression visitUserDefinedScalarFunctionCall(@Nonnull final RelationalParser.UserDefinedScalarFunctionCallContext ctx) { - baseVisitorCalled.setTrue(); - return Expression.ofUnnamed(LiteralValue.ofScalar(42)); - } - }; - final var delegatingVisitor = new DelegatingVisitor<>(baseVisitor); - final var tokenSource = new RelationalLexer(new CaseInsensitiveCharStream(query)); - final var parser = new RelationalParser(new CommonTokenStream(tokenSource)); - final var functionCallExpression = parser.functionCall(); - final var userDefinedScalarFunctionCall = (RelationalParser.UserDefinedScalarFunctionCallContext)functionCallExpression; - delegatingVisitor.visitUserDefinedScalarFunctionCall(userDefinedScalarFunctionCall); - Assertions.assertThat(baseVisitorCalled.booleanValue()).isTrue(); + @Test + void visitIndexOptionsTest() { + testSimple("OPTIONS (LEGACY_EXTREMUM_EVER)", + RelationalParser::indexOptions, + DelegatingVisitor::visitIndexOptions, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Object visitIndexOptions(@Nonnull RelationalParser.IndexOptionsContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitIndexOptionTest() { + testSimple("LEGACY_EXTREMUM_EVER", + RelationalParser::indexOption, + DelegatingVisitor::visitIndexOption, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Object visitIndexOption(@Nonnull RelationalParser.IndexOptionContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitVectorIndexDefinitionTest() { + final var query = "VECTOR INDEX myIndex USING HNSW ON table1 (R)"; + testVisitor(query, + RelationalParser::indexDefinition, + (visitor, ctx) -> { + Assertions.assertThat(ctx).isInstanceOf(RelationalParser.VectorIndexDefinitionContext.class); + visitor.visitVectorIndexDefinition((RelationalParser.VectorIndexDefinitionContext) ctx); + }, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, query, query, 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + @SuppressWarnings({"NullableProblems", "DataFlowIssue"}) + public RecordLayerIndex visitVectorIndexDefinition(@Nonnull RelationalParser.VectorIndexDefinitionContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitPartitionClauseTest() { + final var query = "PARTITION BY (col1)"; + testVisitor(query, + RelationalParser::partitionClause, + (visitor, ctx) -> { + Assertions.assertThat(ctx).isInstanceOf(RelationalParser.PartitionClauseContext.class); + visitor.visitPartitionClause(ctx); + }, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, query, query, 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + + @Override + public Object visitPartitionClause(final RelationalParser.PartitionClauseContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitIndexColumnListTest() { + testSimple("(col1, col2)", + RelationalParser::indexColumnList, + DelegatingVisitor::visitIndexColumnList, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + @SuppressWarnings({"NullableProblems", "DataFlowIssue"}) + public Object visitIndexColumnList(@Nonnull RelationalParser.IndexColumnListContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitIncludeClauseTest() { + testSimple("INCLUDE (col1, col2)", + RelationalParser::includeClause, + DelegatingVisitor::visitIncludeClause, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + @SuppressWarnings({"NullableProblems", "DataFlowIssue"}) + public Object visitIncludeClause(@Nonnull RelationalParser.IncludeClauseContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitIndexOnSourceDefinitionTest() { + final var query = "INDEX myIndex ON table1 (R)"; + testVisitor(query, + RelationalParser::indexDefinition, + (visitor, ctx) -> { + Assertions.assertThat(ctx).isInstanceOf(RelationalParser.IndexOnSourceDefinitionContext.class); + visitor.visitIndexOnSourceDefinition((RelationalParser.IndexOnSourceDefinitionContext) ctx); + }, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, query, query, 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + @SuppressWarnings({"NullableProblems", "DataFlowIssue"}) + public RecordLayerIndex visitIndexOnSourceDefinition(@Nonnull RelationalParser.IndexOnSourceDefinitionContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitIndexTypeTest() { + testSimple("UNIQUE", + RelationalParser::indexType, + DelegatingVisitor::visitIndexType, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Object visitIndexType(@Nonnull RelationalParser.IndexTypeContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitIndexColumnSpecTest() { + testSimple("col1 ASC", + RelationalParser::indexColumnSpec, + DelegatingVisitor::visitIndexColumnSpec, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + @SuppressWarnings({"NullableProblems", "DataFlowIssue"}) + public Object visitIndexColumnSpec(@Nonnull RelationalParser.IndexColumnSpecContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitIndexAsSelectDefinitionTest() { + final var query = "INDEX myIndex AS SELECT * FROM table1"; + testVisitor(query, + RelationalParser::indexDefinition, + (visitor, ctx) -> { + Assertions.assertThat(ctx).isInstanceOf(RelationalParser.IndexAsSelectDefinitionContext.class); + visitor.visitIndexAsSelectDefinition((RelationalParser.IndexAsSelectDefinitionContext) ctx); + }, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, query, query, 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + @SuppressWarnings({"NullableProblems", "DataFlowIssue"}) + public RecordLayerIndex visitIndexAsSelectDefinition(@Nonnull RelationalParser.IndexAsSelectDefinitionContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitOrderClauseTest() { + testSimple("ASC NULLS FIRST", + RelationalParser::orderClause, + DelegatingVisitor::visitOrderClause, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Object visitOrderClause(@Nonnull RelationalParser.OrderClauseContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitVectorIndexOptionsTest() { + testSimple("OPTIONS (EF_CONSTRUCTION = 100)", + RelationalParser::vectorIndexOptions, + DelegatingVisitor::visitVectorIndexOptions, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Object visitVectorIndexOptions(@Nonnull RelationalParser.VectorIndexOptionsContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitHnswMetricTest() { + testSimple("EUCLIDEAN_METRIC", + RelationalParser::hnswMetric, + DelegatingVisitor::visitHnswMetric, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Object visitHnswMetric(@Nonnull RelationalParser.HnswMetricContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitVectorIndexOptionTest() { + testSimple("EF_CONSTRUCTION = 100", + RelationalParser::vectorIndexOption, + DelegatingVisitor::visitVectorIndexOption, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Object visitVectorIndexOption(@Nonnull RelationalParser.VectorIndexOptionContext ctx) { + called.setTrue(); + return null; + } + }); } } diff --git a/yaml-tests/src/test/java/YamlIntegrationTests.java b/yaml-tests/src/test/java/YamlIntegrationTests.java index d8d3f4ae5f..a8508977c2 100644 --- a/yaml-tests/src/test/java/YamlIntegrationTests.java +++ b/yaml-tests/src/test/java/YamlIntegrationTests.java @@ -359,6 +359,12 @@ public void indexDdl(YamlTest.Runner runner) throws Exception { runner.runYamsql("index-ddl.yamsql"); } + @TestTemplate + @MaintainYamlTestConfig(YamlTestConfigFilters.CORRECT_EXPLAIN_AND_METRICS) + public void indexDdlValuesOnly(YamlTest.Runner runner) throws Exception { + runner.runYamsql("index-ddl-values-only.yamsql"); + } + @TestTemplate public void validIdentifiersTest(YamlTest.Runner runner) throws Exception { runner.runYamsql("valid-identifiers.yamsql"); diff --git a/yaml-tests/src/test/resources/index-ddl-values-only.metrics.binpb b/yaml-tests/src/test/resources/index-ddl-values-only.metrics.binpb new file mode 100644 index 0000000000..25b7e1a1c9 --- /dev/null +++ b/yaml-tests/src/test/resources/index-ddl-values-only.metrics.binpb @@ -0,0 +1,411 @@ +™ +c +simple_value_indexMEXPLAIN select price from products_mat_view where price > 18.0 order by price± +‹ûº‚!Ú Ö¾”(y0ܪÆ8X@ÉCOVERING(IDX_MV_PRICE_DESC [[LESS_THAN to_ordered_bytes(promote(@c8 AS DOUBLE), DESC_NULLS_LAST)]] REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)Å digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q172.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[LESS_THAN to_ordered_bytes(promote(@c8 AS DOUBLE), DESC_NULLS_LAST)]]
direction: reversed
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_PRICE_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q172> label="q172" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}œ +c +simple_value_indexMEXPLAIN select price from products_index_on where price > 18.0 order by price´ +‹ÿç°£Ú ÄÙ™h(y0朜 +8X@ÊCOVERING(IDX_IOT_PRICE_DESC [[LESS_THAN to_ordered_bytes(promote(@c8 AS DOUBLE), DESC_NULLS_LAST)]] REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)Æ digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q172.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[LESS_THAN to_ordered_bytes(promote(@c8 AS DOUBLE), DESC_NULLS_LAST)]]
direction: reversed
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_PRICE_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q172> label="q172" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}ú +c +simple_value_indexMEXPLAIN select category from products_mat_view where category = 'Electronics'’ +º’™†š ®Ý(Â0¿Ùæ8†@zCOVERING(IDX_MV_CATEGORY [EQUALS promote(@c8 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[2]]) | MAP (_.CATEGORY AS CATEGORY)ò digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q358.CATEGORY AS CATEGORY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY)" ]; + 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, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q358> label="q358" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}ü +c +simple_value_indexMEXPLAIN select category from products_index_on where category = 'Electronics'” +ºÓòÇ„š ÝÜú”(Â0³‡à8†@{COVERING(IDX_IOT_CATEGORY [EQUALS promote(@c8 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[2]]) | MAP (_.CATEGORY AS CATEGORY)ó digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q358.CATEGORY AS CATEGORY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY)" ]; + 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, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q358> label="q358" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}è +‰ +multi_column_value_indexmEXPLAIN select category, price from products_mat_view where category = 'Electronics' order by category, priceÙ +‘®•ç Ç ¶ù¨(t0©½V8H@COVERING(IDX_MV_CAT_PRICE [EQUALS promote(@c10 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE)š digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q152.CATEGORY AS CATEGORY, q152.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c10 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_CAT_PRICE
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q152> label="q152" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}ë +‰ +multi_column_value_indexmEXPLAIN select category, price from products_index_on where category = 'Electronics' order by category, priceÜ +‘ô“0Ç ë—’(t0Îç…8H@žCOVERING(IDX_IOT_CAT_PRICE [EQUALS promote(@c10 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE)› digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q152.CATEGORY AS CATEGORY, q152.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c10 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_CAT_PRICE
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q152> label="q152" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}¹ +f +multi_column_value_indexJEXPLAIN select name, rating from products_mat_view where name = 'Widget A'Î +‡õÝÂq€ ¯Ë¡5(²0ú­œ8ö@–COVERING(IDX_MV_NAME_RATING [EQUALS promote(@c10 AS STRING)] -> [ID: KEY[3], NAME: KEY[0], RATING: KEY[1]]) | MAP (_.NAME AS NAME, _.RATING AS RATING)“ digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q364.NAME AS NAME, q364.RATING AS RATING)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, DOUBLE AS RATING)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c10 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_NAME_RATING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q364> label="q364" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}» +f +multi_column_value_indexJEXPLAIN select name, rating from products_index_on where name = 'Widget A'Ð +‡¬§’q€ „¤ü0(²0Þ‹Î8ö@—COVERING(IDX_IOT_NAME_RATING [EQUALS promote(@c10 AS STRING)] -> [ID: KEY[3], NAME: KEY[0], RATING: KEY[1]]) | MAP (_.NAME AS NAME, _.RATING AS RATING)” digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q364.NAME AS NAME, q364.RATING AS RATING)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, DOUBLE AS RATING)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c10 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_NAME_RATING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q364> label="q364" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}² +u +covering_value_index]EXPLAIN select price, name, supplier from products_mat_view where price > 15.0 order by price¸ +•Ç”Ì ñô (q0¢ÜC8J@ÌCOVERING(IDX_MV_PRICE_COVERING [[GREATER_THAN promote(@c12 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.PRICE AS PRICE, _.NAME AS NAME, _.SUPPLIER AS SUPPLIER)Ê digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q166.PRICE AS PRICE, q166.NAME AS NAME, q166.SUPPLIER AS SUPPLIER)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE, STRING AS NAME, STRING AS SUPPLIER)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c12 AS DOUBLE)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_PRICE_COVERING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q166> label="q166" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}´ +u +covering_value_index]EXPLAIN select price, name, supplier from products_index_on where price > 15.0 order by priceº +•ƒÃ·%Ì ι¦(q0Íp8J@ÍCOVERING(IDX_IOT_PRICE_COVERING [[GREATER_THAN promote(@c12 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.PRICE AS PRICE, _.NAME AS NAME, _.SUPPLIER AS SUPPLIER)Ë digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q166.PRICE AS PRICE, q166.NAME AS NAME, q166.SUPPLIER AS SUPPLIER)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE, STRING AS NAME, STRING AS SUPPLIER)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c12 AS DOUBLE)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_PRICE_COVERING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q166> label="q166" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}Œ +’ +covering_value_indexzEXPLAIN select category, price, name, stock from products_mat_view where category = 'Electronics' order by category, priceô +º•Й,¼ ƒÎØ(n0úªO8?@éCOVERING(IDX_MV_CAT_PRICE_COVERING [EQUALS promote(@c14 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[3], NAME: VALUE[0], PRICE: KEY[1], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.NAME AS NAME, _.STOCK AS STOCK)é digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q152.CATEGORY AS CATEGORY, q152.PRICE AS PRICE, q152.NAME AS NAME, q152.STOCK AS STOCK)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, DOUBLE AS PRICE, STRING AS NAME, INT AS STOCK)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c14 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_CAT_PRICE_COVERING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q152> label="q152" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}Ž +’ +covering_value_indexzEXPLAIN select category, price, name, stock from products_index_on where category = 'Electronics' order by category, priceö +ºáÌë-¼ ¥ Æ(n0ÉÑ^8?@êCOVERING(IDX_IOT_CAT_PRICE_COVERING [EQUALS promote(@c14 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[3], NAME: VALUE[0], PRICE: KEY[1], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.NAME AS NAME, _.STOCK AS STOCK)ê digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q152.CATEGORY AS CATEGORY, q152.PRICE AS PRICE, q152.NAME AS NAME, q152.STOCK AS STOCK)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, DOUBLE AS PRICE, STRING AS NAME, INT AS STOCK)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c14 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_CAT_PRICE_COVERING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q152> label="q152" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}Ó +Z +mixed_asc_desc_ordering?EXPLAIN select price from products_mat_view order by price descô +è»€Í | ÂÙ˜(b0™ó!8,@}COVERING(IDX_MV_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)× digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q131.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE)" ]; + 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, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_PRICE_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q131> label="q131" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}Õ +Z +mixed_asc_desc_ordering?EXPLAIN select price from products_index_on order by price descö +èššÖ| §‹Ö (b0ìõS8,@~COVERING(IDX_IOT_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)Ø digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q131.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE)" ]; + 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, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_PRICE_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q131> label="q131" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}Õ +r +mixed_asc_desc_orderingWEXPLAIN select category, price from products_mat_view order by category desc, price ascÞ +Ö¢“©c ˜Æ(X0ãµ8@¯COVERING(IDX_MV_CAT_DESC_PRICE_ASC <,> -> [CATEGORY: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE)Ž digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.CATEGORY AS CATEGORY, q110.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, DOUBLE AS PRICE)" ]; + 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, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_CAT_DESC_PRICE_ASC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 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" ]; +}× +r +mixed_asc_desc_orderingWEXPLAIN select category, price from products_index_on order by category desc, price ascà +Öâ™c †•… (X0þŠ&8@°COVERING(IDX_IOT_CAT_DESC_PRICE_ASC <,> -> [CATEGORY: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.CATEGORY AS CATEGORY, q110.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, DOUBLE AS PRICE)" ]; + 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, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_CAT_DESC_PRICE_ASC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 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" ]; +}Ç +^ +nulls_orderingLEXPLAIN select rating from products_mat_view order by rating asc nulls firstä +ÖÌÈÂc ‹¸˜ +(X0æ¾!8@bCOVERING(IDX_MV_RATING_NULLS_FIRST <,> -> [ID: KEY[2], RATING: KEY[0]]) | MAP (_.RATING AS RATING)â digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.RATING AS RATING)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS RATING)" ]; + 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, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_RATING_NULLS_FIRST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 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" ]; +}É +^ +nulls_orderingLEXPLAIN select rating from products_index_on order by rating asc nulls firstæ +ÖÏ—Î c À¨ø(X0ã‹8@cCOVERING(IDX_IOT_RATING_NULLS_FIRST <,> -> [ID: KEY[2], RATING: KEY[0]]) | MAP (_.RATING AS RATING)ã digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.RATING AS RATING)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS RATING)" ]; + 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, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_RATING_NULLS_FIRST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 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" ]; +}ê +] +nulls_orderingKEXPLAIN select rating from products_mat_view order by rating asc nulls lastˆ +Ö…ݤ c ”ÇŽ(X0ˆë8@†COVERING(IDX_MV_RATING_NULLS_LAST <,> -> [ID: KEY[2], RATING: from_ordered_bytes(KEY:[0], ASC_NULLS_LAST)]) | MAP (_.RATING AS RATING)á digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.RATING AS RATING)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS RATING)" ]; + 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, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_RATING_NULLS_LAST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 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" ]; +}ì +] +nulls_orderingKEXPLAIN select rating from products_index_on order by rating asc nulls lastŠ +Ö«ê» c µ¨ì(X0²Á8@‡COVERING(IDX_IOT_RATING_NULLS_LAST <,> -> [ID: KEY[2], RATING: from_ordered_bytes(KEY:[0], ASC_NULLS_LAST)]) | MAP (_.RATING AS RATING)â digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.RATING AS RATING)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS RATING)" ]; + 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, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_RATING_NULLS_LAST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 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" ]; +}Ž +c +nulls_orderingQEXPLAIN select supplier from products_mat_view order by supplier desc nulls first¦ +Ö«§™c Ÿª¹ (X0»º8@–COVERING(IDX_MV_SUPPLIER_DESC_NULLS_FIRST <,> -> [ID: KEY[2], SUPPLIER: from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST)]) | MAP (_.SUPPLIER AS SUPPLIER)ï digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.SUPPLIER AS SUPPLIER)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS SUPPLIER)" ]; + 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, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_SUPPLIER_DESC_NULLS_FIRST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 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" ]; +} +c +nulls_orderingQEXPLAIN select supplier from products_index_on order by supplier desc nulls first¨ +Ö”¼Ó c Í·ª(X0²€8@—COVERING(IDX_IOT_SUPPLIER_DESC_NULLS_FIRST <,> -> [ID: KEY[2], SUPPLIER: from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST)]) | MAP (_.SUPPLIER AS SUPPLIER)ð digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.SUPPLIER AS SUPPLIER)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS SUPPLIER)" ]; + 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, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_SUPPLIER_DESC_NULLS_FIRST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 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" ]; +}Î +U +reverse_scan_tests?EXPLAIN select price from products_mat_view order by price descô +è»€Í | ÂÙ˜(b0™ó!8,@}COVERING(IDX_MV_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)× digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q131.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE)" ]; + 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, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_PRICE_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q131> label="q131" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}Ð +U +reverse_scan_tests?EXPLAIN select price from products_index_on order by price descö +èššÖ| §‹Ö (b0ìõS8,@~COVERING(IDX_IOT_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)Ø digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q131.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE)" ]; + 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, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_PRICE_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q131> label="q131" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}ˆ +T +reverse_scan_tests>EXPLAIN select price from products_mat_view order by price asc¯ +è—έ | Ρé(b0¡§08,@…COVERING(IDX_MV_PRICE_DESC <,> REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)‰ digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q131.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
direction: reversed
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_PRICE_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q131> label="q131" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}Š +T +reverse_scan_tests>EXPLAIN select price from products_index_on order by price asc± +è·ú§ | ã‘´(b0ɤ(8,@†COVERING(IDX_IOT_PRICE_DESC <,> REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)Š digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q131.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
direction: reversed
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_PRICE_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q131> label="q131" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}Û +e +filtered_indexesQEXPLAIN select name, price from products_mat_view where price > 20 order by priceñ +èÅ–Ï'à ðæ¡(w0¸Üo8Q@´COVERING(IDX_MV_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE)› digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q173.NAME AS NAME, q173.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c10 AS DOUBLE)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_PRICE_COVERING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q173> label="q173" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}Ý +e +filtered_indexesQEXPLAIN select name, price from products_index_on where price > 20 order by priceó +ÌóŸ#Ü Çв(u0º¸V8N@µCOVERING(IDX_IOT_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE)œ digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q172.NAME AS NAME, q172.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c10 AS DOUBLE)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_PRICE_COVERING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q172> label="q172" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}ë +y +filtered_indexeseEXPLAIN select name, price, stock from products_mat_view where category = 'Electronics' order by nameí +û©ùÑ • èÓÜ(c0ö‹"8(@¤COVERING(IDX_MV_FILTERED_ELECTRONICS <,> -> [ID: KEY[2], NAME: KEY[0], PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE, _.STOCK AS STOCK)§ digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q126.NAME AS NAME, q126.PRICE AS PRICE, q126.STOCK AS STOCK)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, DOUBLE AS PRICE, INT AS STOCK)" ]; + 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, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_FILTERED_ELECTRONICS
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 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" ]; +}í +y +filtered_indexeseEXPLAIN select name, price, stock from products_index_on where category = 'Electronics' order by nameï +ûüðß• ”§ï(c0μE8(@¥COVERING(IDX_IOT_FILTERED_ELECTRONICS <,> -> [ID: KEY[2], NAME: KEY[0], PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE, _.STOCK AS STOCK)¨ digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q126.NAME AS NAME, q126.PRICE AS PRICE, q126.STOCK AS STOCK)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, DOUBLE AS PRICE, INT AS STOCK)" ]; + 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, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_FILTERED_ELECTRONICS
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 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" ]; +}ƒ +‚ +filtered_indexesnEXPLAIN select category, price, stock from products_mat_view where price > 15 and stock > 60 order by categoryû +Œ æ­ù3  ×Å(‚0¿øÐ8}@ ªCOVERING(IDX_MV_FILTERED_MULTI <,> -> [CATEGORY: KEY[0], ID: KEY[2], PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.STOCK AS STOCK)­ digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q223.CATEGORY AS CATEGORY, q223.PRICE AS PRICE, q223.STOCK AS STOCK)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, DOUBLE AS PRICE, INT AS STOCK)" ]; + 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, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_FILTERED_MULTI
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q223> label="q223" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}… +‚ +filtered_indexesnEXPLAIN select category, price, stock from products_index_on where price > 15 and stock > 60 order by categoryý +Œ Ô•¤5  ¸çŽ(‚0ùÅÑ8}@ «COVERING(IDX_IOT_FILTERED_MULTI <,> -> [CATEGORY: KEY[0], ID: KEY[2], PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.STOCK AS STOCK)® digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q223.CATEGORY AS CATEGORY, q223.PRICE AS PRICE, q223.STOCK AS STOCK)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, DOUBLE AS PRICE, INT AS STOCK)" ]; + 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, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_FILTERED_MULTI
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q223> label="q223" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}Û +e +filtered_indexesQEXPLAIN select name, price from products_mat_view where price > 50 order by priceñ +èÅ–Ï'à ðæ¡(w0¸Üo8Q@´COVERING(IDX_MV_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE)› digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q173.NAME AS NAME, q173.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c10 AS DOUBLE)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_PRICE_COVERING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q173> label="q173" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +}Ý +e +filtered_indexesQEXPLAIN select name, price from products_index_on where price > 50 order by priceó +ÌóŸ#Ü Çв(u0º¸V8N@µCOVERING(IDX_IOT_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE)œ digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q172.NAME AS NAME, q172.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c10 AS DOUBLE)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_PRICE_COVERING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q172> label="q172" 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-values-only.metrics.yaml b/yaml-tests/src/test/resources/index-ddl-values-only.metrics.yaml new file mode 100644 index 0000000000..38528f368b --- /dev/null +++ b/yaml-tests/src/test/resources/index-ddl-values-only.metrics.yaml @@ -0,0 +1,429 @@ +simple_value_index: +- query: EXPLAIN select price from products_mat_view where price > 18.0 order by + price + explain: 'COVERING(IDX_MV_PRICE_DESC [[LESS_THAN to_ordered_bytes(promote(@c8 + AS DOUBLE), DESC_NULLS_LAST)]] REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)' + task_count: 907 + task_total_time_ms: 69 + transform_count: 218 + transform_time_ms: 35 + transform_yield_count: 121 + insert_time_ms: 3 + insert_new_count: 88 + insert_reused_count: 5 +- query: EXPLAIN select price from products_index_on where price > 18.0 order by + price + explain: 'COVERING(IDX_IOT_PRICE_DESC [[LESS_THAN to_ordered_bytes(promote(@c8 + AS DOUBLE), DESC_NULLS_LAST)]] REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)' + task_count: 907 + task_total_time_ms: 342 + transform_count: 218 + transform_time_ms: 218 + transform_yield_count: 121 + insert_time_ms: 21 + insert_new_count: 88 + insert_reused_count: 5 +- query: EXPLAIN select category from products_mat_view where category = 'Electronics' + explain: 'COVERING(IDX_MV_CATEGORY [EQUALS promote(@c8 AS STRING)] -> [CATEGORY: + KEY[0], ID: KEY[2]]) | MAP (_.CATEGORY AS CATEGORY)' + task_count: 2234 + task_total_time_ms: 549 + transform_count: 538 + transform_time_ms: 297 + transform_yield_count: 194 + insert_time_ms: 37 + insert_new_count: 262 + insert_reused_count: 22 +- query: EXPLAIN select category from products_index_on where category = 'Electronics' + explain: 'COVERING(IDX_IOT_CATEGORY [EQUALS promote(@c8 AS STRING)] -> [CATEGORY: + KEY[0], ID: KEY[2]]) | MAP (_.CATEGORY AS CATEGORY)' + task_count: 2234 + task_total_time_ms: 546 + transform_count: 538 + transform_time_ms: 312 + transform_yield_count: 194 + insert_time_ms: 35 + insert_new_count: 262 + insert_reused_count: 22 +multi_column_value_index: +- query: EXPLAIN select category, price from products_mat_view where category = + 'Electronics' order by category, price + explain: 'COVERING(IDX_MV_CAT_PRICE [EQUALS promote(@c10 AS STRING)] -> [CATEGORY: + KEY[0], ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE + AS PRICE)' + task_count: 785 + task_total_time_ms: 68 + transform_count: 199 + transform_time_ms: 34 + transform_yield_count: 116 + insert_time_ms: 1 + insert_new_count: 72 + insert_reused_count: 3 +- query: EXPLAIN select category, price from products_index_on where category = + 'Electronics' order by category, price + explain: 'COVERING(IDX_IOT_CAT_PRICE [EQUALS promote(@c10 AS STRING)] -> [CATEGORY: + KEY[0], ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE + AS PRICE)' + task_count: 785 + task_total_time_ms: 100 + transform_count: 199 + transform_time_ms: 54 + transform_yield_count: 116 + insert_time_ms: 2 + insert_new_count: 72 + insert_reused_count: 3 +- query: EXPLAIN select name, rating from products_mat_view where name = 'Widget + A' + explain: 'COVERING(IDX_MV_NAME_RATING [EQUALS promote(@c10 AS STRING)] -> [ID: + KEY[3], NAME: KEY[0], RATING: KEY[1]]) | MAP (_.NAME AS NAME, _.RATING AS + RATING)' + task_count: 2055 + task_total_time_ms: 238 + transform_count: 512 + transform_time_ms: 111 + transform_yield_count: 178 + insert_time_ms: 13 + insert_new_count: 246 + insert_reused_count: 22 +- query: EXPLAIN select name, rating from products_index_on where name = 'Widget + A' + explain: 'COVERING(IDX_IOT_NAME_RATING [EQUALS promote(@c10 AS STRING)] -> [ID: + KEY[3], NAME: KEY[0], RATING: KEY[1]]) | MAP (_.NAME AS NAME, _.RATING AS + RATING)' + task_count: 2055 + task_total_time_ms: 237 + transform_count: 512 + transform_time_ms: 102 + transform_yield_count: 178 + insert_time_ms: 11 + insert_new_count: 246 + insert_reused_count: 22 +covering_value_index: +- query: EXPLAIN select price, name, supplier from products_mat_view where price + > 15.0 order by price + explain: 'COVERING(IDX_MV_PRICE_COVERING [[GREATER_THAN promote(@c12 AS DOUBLE)]] + -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP + (_.PRICE AS PRICE, _.NAME AS NAME, _.SUPPLIER AS SUPPLIER)' + task_count: 789 + task_total_time_ms: 37 + transform_count: 204 + transform_time_ms: 19 + transform_yield_count: 113 + insert_time_ms: 1 + insert_new_count: 74 + insert_reused_count: 3 +- query: EXPLAIN select price, name, supplier from products_index_on where price + > 15.0 order by price + explain: 'COVERING(IDX_IOT_PRICE_COVERING [[GREATER_THAN promote(@c12 AS DOUBLE)]] + -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP + (_.PRICE AS PRICE, _.NAME AS NAME, _.SUPPLIER AS SUPPLIER)' + task_count: 789 + task_total_time_ms: 78 + transform_count: 204 + transform_time_ms: 42 + transform_yield_count: 113 + insert_time_ms: 1 + insert_new_count: 74 + insert_reused_count: 3 +- query: EXPLAIN select category, price, name, stock from products_mat_view where + category = 'Electronics' order by category, price + explain: 'COVERING(IDX_MV_CAT_PRICE_COVERING [EQUALS promote(@c14 AS STRING)] + -> [CATEGORY: KEY[0], ID: KEY[3], NAME: VALUE[0], PRICE: KEY[1], STOCK: VALUE[1]]) + | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.NAME AS NAME, _.STOCK AS + STOCK)' + task_count: 698 + task_total_time_ms: 92 + transform_count: 188 + transform_time_ms: 53 + transform_yield_count: 110 + insert_time_ms: 1 + insert_new_count: 63 + insert_reused_count: 3 +- query: EXPLAIN select category, price, name, stock from products_index_on where + category = 'Electronics' order by category, price + explain: 'COVERING(IDX_IOT_CAT_PRICE_COVERING [EQUALS promote(@c14 AS STRING)] + -> [CATEGORY: KEY[0], ID: KEY[3], NAME: VALUE[0], PRICE: KEY[1], STOCK: VALUE[1]]) + | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.NAME AS NAME, _.STOCK AS + STOCK)' + task_count: 698 + task_total_time_ms: 96 + transform_count: 188 + transform_time_ms: 55 + transform_yield_count: 110 + insert_time_ms: 1 + insert_new_count: 63 + insert_reused_count: 3 +mixed_asc_desc_ordering: +- query: EXPLAIN select price from products_mat_view order by price desc + explain: 'COVERING(IDX_MV_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)' + task_count: 488 + task_total_time_ms: 24 + transform_count: 124 + transform_time_ms: 15 + transform_yield_count: 98 + insert_time_ms: 0 + insert_new_count: 44 + insert_reused_count: 6 +- query: EXPLAIN select price from products_index_on order by price desc + explain: 'COVERING(IDX_IOT_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)' + task_count: 488 + task_total_time_ms: 51 + transform_count: 124 + transform_time_ms: 26 + transform_yield_count: 98 + insert_time_ms: 1 + insert_new_count: 44 + insert_reused_count: 6 +- query: EXPLAIN select category, price from products_mat_view order by category + desc, price asc + explain: 'COVERING(IDX_MV_CAT_DESC_PRICE_ASC <,> -> [CATEGORY: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST), ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, + _.PRICE AS PRICE)' + task_count: 342 + task_total_time_ms: 42 + transform_count: 99 + transform_time_ms: 29 + transform_yield_count: 88 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN select category, price from products_index_on order by category + desc, price asc + explain: 'COVERING(IDX_IOT_CAT_DESC_PRICE_ASC <,> -> [CATEGORY: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST), ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, + _.PRICE AS PRICE)' + task_count: 342 + task_total_time_ms: 46 + transform_count: 99 + transform_time_ms: 27 + transform_yield_count: 88 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +nulls_ordering: +- query: EXPLAIN select rating from products_mat_view order by rating asc nulls + first + explain: 'COVERING(IDX_MV_RATING_NULLS_FIRST <,> -> [ID: KEY[2], RATING: KEY[0]]) + | MAP (_.RATING AS RATING)' + task_count: 342 + task_total_time_ms: 32 + transform_count: 99 + transform_time_ms: 21 + transform_yield_count: 88 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN select rating from products_index_on order by rating asc nulls + first + explain: 'COVERING(IDX_IOT_RATING_NULLS_FIRST <,> -> [ID: KEY[2], RATING: KEY[0]]) + | MAP (_.RATING AS RATING)' + task_count: 342 + task_total_time_ms: 26 + transform_count: 99 + transform_time_ms: 16 + transform_yield_count: 88 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN select rating from products_mat_view order by rating asc nulls + last + explain: 'COVERING(IDX_MV_RATING_NULLS_LAST <,> -> [ID: KEY[2], RATING: from_ordered_bytes(KEY:[0], + ASC_NULLS_LAST)]) | MAP (_.RATING AS RATING)' + task_count: 342 + task_total_time_ms: 27 + transform_count: 99 + transform_time_ms: 17 + transform_yield_count: 88 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN select rating from products_index_on order by rating asc nulls + last + explain: 'COVERING(IDX_IOT_RATING_NULLS_LAST <,> -> [ID: KEY[2], RATING: from_ordered_bytes(KEY:[0], + ASC_NULLS_LAST)]) | MAP (_.RATING AS RATING)' + task_count: 342 + task_total_time_ms: 26 + transform_count: 99 + transform_time_ms: 16 + transform_yield_count: 88 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN select supplier from products_mat_view order by supplier desc nulls + first + explain: 'COVERING(IDX_MV_SUPPLIER_DESC_NULLS_FIRST <,> -> [ID: KEY[2], SUPPLIER: + from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST)]) | MAP (_.SUPPLIER AS SUPPLIER)' + task_count: 342 + task_total_time_ms: 31 + transform_count: 99 + transform_time_ms: 19 + transform_yield_count: 88 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN select supplier from products_index_on order by supplier desc nulls + first + explain: 'COVERING(IDX_IOT_SUPPLIER_DESC_NULLS_FIRST <,> -> [ID: KEY[2], SUPPLIER: + from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST)]) | MAP (_.SUPPLIER AS SUPPLIER)' + task_count: 342 + task_total_time_ms: 28 + transform_count: 99 + transform_time_ms: 17 + transform_yield_count: 88 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +reverse_scan_tests: +- query: EXPLAIN select price from products_mat_view order by price desc + explain: 'COVERING(IDX_MV_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)' + task_count: 488 + task_total_time_ms: 24 + transform_count: 124 + transform_time_ms: 15 + transform_yield_count: 98 + insert_time_ms: 0 + insert_new_count: 44 + insert_reused_count: 6 +- query: EXPLAIN select price from products_index_on order by price desc + explain: 'COVERING(IDX_IOT_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)' + task_count: 488 + task_total_time_ms: 51 + transform_count: 124 + transform_time_ms: 26 + transform_yield_count: 98 + insert_time_ms: 1 + insert_new_count: 44 + insert_reused_count: 6 +- query: EXPLAIN select price from products_mat_view order by price asc + explain: 'COVERING(IDX_MV_PRICE_DESC <,> REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)' + task_count: 488 + task_total_time_ms: 25 + transform_count: 124 + transform_time_ms: 12 + transform_yield_count: 98 + insert_time_ms: 0 + insert_new_count: 44 + insert_reused_count: 6 +- query: EXPLAIN select price from products_index_on order by price asc + explain: 'COVERING(IDX_IOT_PRICE_DESC <,> REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)' + task_count: 488 + task_total_time_ms: 27 + transform_count: 124 + transform_time_ms: 17 + transform_yield_count: 98 + insert_time_ms: 0 + insert_new_count: 44 + insert_reused_count: 6 +filtered_indexes: +- query: EXPLAIN select name, price from products_mat_view where price > 20 order + by price + explain: 'COVERING(IDX_MV_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] + -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP + (_.NAME AS NAME, _.PRICE AS PRICE)' + task_count: 872 + task_total_time_ms: 83 + transform_count: 224 + transform_time_ms: 42 + transform_yield_count: 119 + insert_time_ms: 1 + insert_new_count: 81 + insert_reused_count: 3 +- query: EXPLAIN select name, price from products_index_on where price > 20 order + by price + explain: 'COVERING(IDX_IOT_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] + -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP + (_.NAME AS NAME, _.PRICE AS PRICE)' + task_count: 844 + task_total_time_ms: 73 + transform_count: 220 + transform_time_ms: 38 + transform_yield_count: 117 + insert_time_ms: 1 + insert_new_count: 78 + insert_reused_count: 3 +- query: EXPLAIN select name, price, stock from products_mat_view where category + = 'Electronics' order by name + explain: 'COVERING(IDX_MV_FILTERED_ELECTRONICS <,> -> [ID: KEY[2], NAME: KEY[0], + PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE, + _.STOCK AS STOCK)' + task_count: 507 + task_total_time_ms: 26 + transform_count: 149 + transform_time_ms: 14 + transform_yield_count: 99 + insert_time_ms: 0 + insert_new_count: 40 + insert_reused_count: 2 +- query: EXPLAIN select name, price, stock from products_index_on where category + = 'Electronics' order by name + explain: 'COVERING(IDX_IOT_FILTERED_ELECTRONICS <,> -> [ID: KEY[2], NAME: KEY[0], + PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE, + _.STOCK AS STOCK)' + task_count: 507 + task_total_time_ms: 58 + transform_count: 149 + transform_time_ms: 33 + transform_yield_count: 99 + insert_time_ms: 1 + insert_new_count: 40 + insert_reused_count: 2 +- query: EXPLAIN select category, price, stock from products_mat_view where price + > 15 and stock > 60 order by category + explain: 'COVERING(IDX_MV_FILTERED_MULTI <,> -> [CATEGORY: KEY[0], ID: KEY[2], + PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE + AS PRICE, _.STOCK AS STOCK)' + task_count: 1164 + task_total_time_ms: 108 + transform_count: 288 + transform_time_ms: 55 + transform_yield_count: 130 + insert_time_ms: 3 + insert_new_count: 125 + insert_reused_count: 9 +- query: EXPLAIN select category, price, stock from products_index_on where price + > 15 and stock > 60 order by category + explain: 'COVERING(IDX_IOT_FILTERED_MULTI <,> -> [CATEGORY: KEY[0], ID: KEY[2], + PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE + AS PRICE, _.STOCK AS STOCK)' + task_count: 1164 + task_total_time_ms: 111 + transform_count: 288 + transform_time_ms: 58 + transform_yield_count: 130 + insert_time_ms: 3 + insert_new_count: 125 + insert_reused_count: 9 +- query: EXPLAIN select name, price from products_mat_view where price > 50 order + by price + explain: 'COVERING(IDX_MV_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] + -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP + (_.NAME AS NAME, _.PRICE AS PRICE)' + task_count: 872 + task_total_time_ms: 83 + transform_count: 224 + transform_time_ms: 42 + transform_yield_count: 119 + insert_time_ms: 1 + insert_new_count: 81 + insert_reused_count: 3 +- query: EXPLAIN select name, price from products_index_on where price > 50 order + by price + explain: 'COVERING(IDX_IOT_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] + -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP + (_.NAME AS NAME, _.PRICE AS PRICE)' + task_count: 844 + task_total_time_ms: 73 + transform_count: 220 + transform_time_ms: 38 + transform_yield_count: 117 + insert_time_ms: 1 + insert_new_count: 78 + insert_reused_count: 3 diff --git a/yaml-tests/src/test/resources/index-ddl-values-only.yamsql b/yaml-tests/src/test/resources/index-ddl-values-only.yamsql new file mode 100644 index 0000000000..8cce2a41e8 --- /dev/null +++ b/yaml-tests/src/test/resources/index-ddl-values-only.yamsql @@ -0,0 +1,323 @@ +# +# index-ddl-values-only.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 VALUES indexes with different orderings +# Compares CREATE INDEX AS SELECT vs CREATE INDEX ON syntax +# Both syntaxes should produce equivalent query plans + +--- +options: + supported_version: !current_version +--- +schema_template: + create table products_mat_view( + id integer, + name string, + price double, + category string, + stock integer, + supplier string, + rating double, + primary key(id) + ) + + create index idx_mv_price as select price from products_mat_view order by price + create index idx_mv_category as select category from products_mat_view order by category + + create index idx_mv_cat_price as select category, price from products_mat_view order by category, price + create index idx_mv_name_rating as select name, rating from products_mat_view order by name, rating + + create index idx_mv_price_covering as select price, name, supplier from products_mat_view order by price + create index idx_mv_cat_price_covering as select category, price, name, stock from products_mat_view order by category, price + + create index idx_mv_cat_desc_price_asc as select category, price from products_mat_view order by category desc, price asc + create index idx_mv_price_desc as select price from products_mat_view order by price desc + + create index idx_mv_rating_nulls_first as select rating from products_mat_view order by rating asc nulls first + create index idx_mv_rating_nulls_last as select rating from products_mat_view order by rating asc nulls last + create index idx_mv_supplier_desc_nulls_first as select supplier from products_mat_view order by supplier desc nulls first + + create index idx_mv_filtered_expensive as select name, price from products_mat_view where price > 20 order by price + create index idx_mv_filtered_electronics as select name, price, stock from products_mat_view where category = 'Electronics' order by name + create index idx_mv_filtered_multi as select category, price, stock from products_mat_view where price > 15 and stock > 60 order by category + + create table products_index_on( + id integer, + name string, + price double, + category string, + stock integer, + supplier string, + rating double, + primary key(id) + ) + + create index idx_iot_price on products_index_on(price) + create index idx_iot_category on products_index_on(category) + + create index idx_iot_cat_price on products_index_on(category, price) + create index idx_iot_name_rating on products_index_on(name, rating) + + create index idx_iot_price_covering on products_index_on(price) include(name, supplier) + create index idx_iot_cat_price_covering on products_index_on(category, price) include(name, stock) + + create index idx_iot_cat_desc_price_asc on products_index_on(category desc, price asc) + create index idx_iot_price_desc on products_index_on(price desc) + + create index idx_iot_rating_nulls_first on products_index_on(rating asc nulls first) + create index idx_iot_rating_nulls_last on products_index_on(rating asc nulls last) + create index idx_iot_supplier_desc_nulls_first on products_index_on(supplier desc nulls first) + + create view v_iot_expensive as select name, price from products_index_on where price > 20 + create index idx_iot_filtered_expensive on v_iot_expensive(price) + + create view v_iot_electronics as select name, price, stock from products_index_on where category = 'Electronics' + create index idx_iot_filtered_electronics on v_iot_electronics(name) include (price, stock) + + create view v_iot_multi_filter as select category, price, stock from products_index_on where price > 15 and stock > 60 + create index idx_iot_filtered_multi on v_iot_multi_filter(category) include(price, stock) + +--- +setup: + steps: + # Insert test data with NULLs to test NULLS ordering + # Additional rows added for filtered index tests + - query: INSERT INTO products_mat_view VALUES + (1, 'Widget A', 19.99, 'Electronics', 100, 'SupplierX', 4.5), + (2, 'Widget B', 29.99, 'Electronics', 50, 'SupplierY', NULL), + (3, 'Gadget C', NULL, 'Home', 75, NULL, 3.8), + (4, 'Tool D', 15.50, NULL, 200, 'SupplierZ', 4.2), + (5, 'Phone X', 799.99, 'Electronics', 120, 'SupplierX', 4.8), + (6, 'Tablet Y', 299.99, 'Electronics', 80, 'SupplierY', 4.3), + (7, 'Speaker Z', 89.99, 'Electronics', 150, 'SupplierZ', 4.1), + (8, 'Lamp A', 25.50, 'Home', 90, 'SupplierX', 3.9), + (9, 'Chair B', 12.99, 'Home', 30, 'SupplierY', 4.0) + + - query: INSERT INTO products_index_on VALUES + (1, 'Widget A', 19.99, 'Electronics', 100, 'SupplierX', 4.5), + (2, 'Widget B', 29.99, 'Electronics', 50, 'SupplierY', NULL), + (3, 'Gadget C', NULL, 'Home', 75, NULL, 3.8), + (4, 'Tool D', 15.50, NULL, 200, 'SupplierZ', 4.2), + (5, 'Phone X', 799.99, 'Electronics', 120, 'SupplierX', 4.8), + (6, 'Tablet Y', 299.99, 'Electronics', 80, 'SupplierY', 4.3), + (7, 'Speaker Z', 89.99, 'Electronics', 150, 'SupplierZ', 4.1), + (8, 'Lamp A', 25.50, 'Home', 90, 'SupplierX', 3.9), + (9, 'Chair B', 12.99, 'Home', 30, 'SupplierY', 4.0) + + +--- +test_block: + name: simple_value_index + tests: + # Test simple single-column value index on price + - + - query: select price from products_mat_view where price > 18.0 order by price + - explain: "COVERING(IDX_MV_PRICE_DESC [[LESS_THAN to_ordered_bytes(promote(@c8 AS DOUBLE), DESC_NULLS_LAST)]] REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)" + - result: [{ 19.99 }, { 25.50 }, { 29.99 }, { 89.99 }, { 299.99 }, { 799.99 }] + - + - query: select price from products_index_on where price > 18.0 order by price + - explain: "COVERING(IDX_IOT_PRICE_DESC [[LESS_THAN to_ordered_bytes(promote(@c8 AS DOUBLE), DESC_NULLS_LAST)]] REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)" + - result: [{ 19.99 }, { 25.50 }, { 29.99 }, { 89.99 }, { 299.99 }, { 799.99 }] + + # Test simple single-column value index on category + - + - query: select category from products_mat_view where category = 'Electronics' + - explain: "COVERING(IDX_MV_CATEGORY [EQUALS promote(@c8 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[2]]) | MAP (_.CATEGORY AS CATEGORY)" + - result: [{ "Electronics" }, { "Electronics" }, { "Electronics" }, { "Electronics" }, { "Electronics" }] + - + - query: select category from products_index_on where category = 'Electronics' + - explain: "COVERING(IDX_IOT_CATEGORY [EQUALS promote(@c8 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[2]]) | MAP (_.CATEGORY AS CATEGORY)" + - result: [{ "Electronics" }, { "Electronics" }, { "Electronics" }, { "Electronics" }, { "Electronics" }] + +--- +test_block: + name: multi_column_value_index + tests: + # Test multi-column index (category, price) + - + - query: select category, price from products_mat_view where category = 'Electronics' order by category, price + - explain: "COVERING(IDX_MV_CAT_PRICE [EQUALS promote(@c10 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE)" + - result: [{ "Electronics", 19.99 }, { "Electronics", 29.99 }, { "Electronics", 89.99 }, { "Electronics", 299.99 }, { "Electronics", 799.99 }] + - + - query: select category, price from products_index_on where category = 'Electronics' order by category, price + - explain: "COVERING(IDX_IOT_CAT_PRICE [EQUALS promote(@c10 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE)" + - result: [{ "Electronics", 19.99 }, { "Electronics", 29.99 }, { "Electronics", 89.99 }, { "Electronics", 299.99 }, { "Electronics", 799.99 }] + + # Test multi-column index (name, rating) + - + - query: select name, rating from products_mat_view where name = 'Widget A' + - explain: "COVERING(IDX_MV_NAME_RATING [EQUALS promote(@c10 AS STRING)] -> [ID: KEY[3], NAME: KEY[0], RATING: KEY[1]]) | MAP (_.NAME AS NAME, _.RATING AS RATING)" + - result: [{ "Widget A", 4.5 }] + - + - query: select name, rating from products_index_on where name = 'Widget A' + - explain: "COVERING(IDX_IOT_NAME_RATING [EQUALS promote(@c10 AS STRING)] -> [ID: KEY[3], NAME: KEY[0], RATING: KEY[1]]) | MAP (_.NAME AS NAME, _.RATING AS RATING)" + - result: [{ "Widget A", 4.5 }] + +--- +test_block: + name: covering_value_index + tests: + # Test covering index - price in key, name and supplier in values + - + - query: select price, name, supplier from products_mat_view where price > 15.0 order by price + - explain: "COVERING(IDX_MV_PRICE_COVERING [[GREATER_THAN promote(@c12 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.PRICE AS PRICE, _.NAME AS NAME, _.SUPPLIER AS SUPPLIER)" + - result: [{ 15.50, "Tool D", "SupplierZ" }, { 19.99, "Widget A", "SupplierX" }, { 25.50, "Lamp A", "SupplierX" }, { 29.99, "Widget B", "SupplierY" }, { 89.99, "Speaker Z", "SupplierZ" }, { 299.99, "Tablet Y", "SupplierY" }, { 799.99, "Phone X", "SupplierX" }] + - + - query: select price, name, supplier from products_index_on where price > 15.0 order by price + - explain: "COVERING(IDX_IOT_PRICE_COVERING [[GREATER_THAN promote(@c12 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.PRICE AS PRICE, _.NAME AS NAME, _.SUPPLIER AS SUPPLIER)" + - result: [{ 15.50, "Tool D", "SupplierZ" }, { 19.99, "Widget A", "SupplierX" }, { 25.50, "Lamp A", "SupplierX" }, { 29.99, "Widget B", "SupplierY" }, { 89.99, "Speaker Z", "SupplierZ" }, { 299.99, "Tablet Y", "SupplierY" }, { 799.99, "Phone X", "SupplierX" }] + + # Test covering index - category and price in key, name and stock in values + - + - query: select category, price, name, stock from products_mat_view where category = 'Electronics' order by category, price + - explain: "COVERING(IDX_MV_CAT_PRICE_COVERING [EQUALS promote(@c14 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[3], NAME: VALUE[0], PRICE: KEY[1], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.NAME AS NAME, _.STOCK AS STOCK)" + - result: [{ "Electronics", 19.99, "Widget A", 100 }, { "Electronics", 29.99, "Widget B", 50 }, { "Electronics", 89.99, "Speaker Z", 150 }, { "Electronics", 299.99, "Tablet Y", 80 }, { "Electronics", 799.99, "Phone X", 120 }] + - + - query: select category, price, name, stock from products_index_on where category = 'Electronics' order by category, price + - explain: "COVERING(IDX_IOT_CAT_PRICE_COVERING [EQUALS promote(@c14 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[3], NAME: VALUE[0], PRICE: KEY[1], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.NAME AS NAME, _.STOCK AS STOCK)" + - result: [{ "Electronics", 19.99, "Widget A", 100 }, { "Electronics", 29.99, "Widget B", 50 }, { "Electronics", 89.99, "Speaker Z", 150 }, { "Electronics", 299.99, "Tablet Y", 80 }, { "Electronics", 799.99, "Phone X", 120 }] + +--- +test_block: + name: mixed_asc_desc_ordering + tests: + # Test DESC ordering on price + - + - query: select price from products_mat_view order by price desc + - explain: "COVERING(IDX_MV_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)" + - result: [{ 799.99 }, { 299.99 }, { 89.99 }, { 29.99 }, { 25.50 }, { 19.99 }, { 15.50 }, { 12.99 }, { !null }] + - + - query: select price from products_index_on order by price desc + - explain: "COVERING(IDX_IOT_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)" + - result: [{ 799.99 }, { 299.99 }, { 89.99 }, { 29.99 }, { 25.50 }, { 19.99 }, { 15.50 }, { 12.99 }, { !null }] + + # Test mixed ASC/DESC (category DESC, price ASC) + - + - query: select category, price from products_mat_view order by category desc, price asc + - explain: "COVERING(IDX_MV_CAT_DESC_PRICE_ASC <,> -> [CATEGORY: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE)" + - result: [{ "Home", !null }, { "Home", 12.99 }, { "Home", 25.50 }, { "Electronics", 19.99 }, { "Electronics", 29.99 }, { "Electronics", 89.99 }, { "Electronics", 299.99 }, { "Electronics", 799.99 }, { !null , 15.50 }] + - + - query: select category, price from products_index_on order by category desc, price asc + - explain: "COVERING(IDX_IOT_CAT_DESC_PRICE_ASC <,> -> [CATEGORY: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE)" + - result: [{ "Home", !null }, { "Home", 12.99 }, { "Home", 25.50 }, { "Electronics", 19.99 }, { "Electronics", 29.99 }, { "Electronics", 89.99 }, { "Electronics", 299.99 }, { "Electronics", 799.99 }, { !null , 15.50 }] + +--- +test_block: + name: nulls_ordering + tests: + # Test NULLS FIRST ordering on rating + - + - query: select rating from products_mat_view order by rating asc nulls first + - explain: "COVERING(IDX_MV_RATING_NULLS_FIRST <,> -> [ID: KEY[2], RATING: KEY[0]]) | MAP (_.RATING AS RATING)" + - result: [{ !null }, { 3.8 }, { 3.9 }, { 4.0 }, { 4.1 }, { 4.2 }, { 4.3 }, { 4.5 }, { 4.8 }] + - + - query: select rating from products_index_on order by rating asc nulls first + - explain: "COVERING(IDX_IOT_RATING_NULLS_FIRST <,> -> [ID: KEY[2], RATING: KEY[0]]) | MAP (_.RATING AS RATING)" + - result: [{ !null }, { 3.8 }, { 3.9 }, { 4.0 }, { 4.1 }, { 4.2 }, { 4.3 }, { 4.5 }, { 4.8 }] + + # Test NULLS LAST ordering on rating + - + - query: select rating from products_mat_view order by rating asc nulls last + - explain: "COVERING(IDX_MV_RATING_NULLS_LAST <,> -> [ID: KEY[2], RATING: from_ordered_bytes(KEY:[0], ASC_NULLS_LAST)]) | MAP (_.RATING AS RATING)" + - result: [{ 3.8 }, { 3.9 }, { 4.0 }, { 4.1 }, { 4.2 }, { 4.3 }, { 4.5 }, { 4.8 }, { !null }] + - + - query: select rating from products_index_on order by rating asc nulls last + - explain: "COVERING(IDX_IOT_RATING_NULLS_LAST <,> -> [ID: KEY[2], RATING: from_ordered_bytes(KEY:[0], ASC_NULLS_LAST)]) | MAP (_.RATING AS RATING)" + - result: [{ 3.8 }, { 3.9 }, { 4.0 }, { 4.1 }, { 4.2 }, { 4.3 }, { 4.5 }, { 4.8 }, { !null }] + + # Test DESC NULLS FIRST on supplier + - + - query: select supplier from products_mat_view order by supplier desc nulls first + - explain: "COVERING(IDX_MV_SUPPLIER_DESC_NULLS_FIRST <,> -> [ID: KEY[2], SUPPLIER: from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST)]) | MAP (_.SUPPLIER AS SUPPLIER)" + - result: [{ !null }, { "SupplierZ" }, { "SupplierZ" }, { "SupplierY" }, { "SupplierY" }, { "SupplierY" }, { "SupplierX" }, { "SupplierX" }, { "SupplierX" }] + - + - query: select supplier from products_index_on order by supplier desc nulls first + - explain: "COVERING(IDX_IOT_SUPPLIER_DESC_NULLS_FIRST <,> -> [ID: KEY[2], SUPPLIER: from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST)]) | MAP (_.SUPPLIER AS SUPPLIER)" + - result: [{ !null }, { "SupplierZ" }, { "SupplierZ" }, { "SupplierY" }, { "SupplierY" }, { "SupplierY" }, { "SupplierX" }, { "SupplierX" }, { "SupplierX" }] + +--- +test_block: + name: reverse_scan_tests + tests: + # Test reverse scan - query DESC on ASC index + - + - query: select price from products_mat_view order by price desc + - explain: "COVERING(IDX_MV_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)" + - result: [{ 799.99 }, { 299.99 }, { 89.99 }, { 29.99 }, { 25.50 }, { 19.99 }, { 15.50 }, { 12.99 }, { !null }] + - + - query: select price from products_index_on order by price desc + - explain: "COVERING(IDX_IOT_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)" + - result: [{ 799.99 }, { 299.99 }, { 89.99 }, { 29.99 }, { 25.50 }, { 19.99 }, { 15.50 }, { 12.99 }, { !null }] + + # Test reverse scan - query ASC on DESC index + - + - query: select price from products_mat_view order by price asc + - explain: "COVERING(IDX_MV_PRICE_DESC <,> REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)" + - result: [{ !null }, { 12.99 }, { 15.50 }, { 19.99 }, { 25.50 }, { 29.99 }, { 89.99 }, { 299.99 }, { 799.99 }] + - + - query: select price from products_index_on order by price asc + - explain: "COVERING(IDX_IOT_PRICE_DESC <,> REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)" + - result: [{ !null }, { 12.99 }, { 15.50 }, { 19.99 }, { 25.50 }, { 29.99 }, { 89.99 }, { 299.99 }, { 799.99 }] + +--- +test_block: + name: filtered_indexes + tests: + # Test filtered index - expensive products (price > 20) + - + - query: select name, price from products_mat_view where price > 20 order by price + - explain: "COVERING(IDX_MV_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE)" + - result: [{ "Lamp A", 25.50 }, { "Widget B", 29.99 }, { "Speaker Z", 89.99 }, { "Tablet Y", 299.99 }, { "Phone X", 799.99 }] + - + - query: select name, price from products_index_on where price > 20 order by price + - explain: "COVERING(IDX_IOT_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE)" + - result: [{ "Lamp A", 25.50 }, { "Widget B", 29.99 }, { "Speaker Z", 89.99 }, { "Tablet Y", 299.99 }, { "Phone X", 799.99 }] + + # Test filtered index - electronics category + - + - query: select name, price, stock from products_mat_view where category = 'Electronics' order by name + - explain: "COVERING(IDX_MV_FILTERED_ELECTRONICS <,> -> [ID: KEY[2], NAME: KEY[0], PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE, _.STOCK AS STOCK)" + - result: [{ "Phone X", 799.99, 120 }, { "Speaker Z", 89.99, 150 }, { "Tablet Y", 299.99, 80 }, { "Widget A", 19.99, 100 }, { "Widget B", 29.99, 50 }] + - + - query: select name, price, stock from products_index_on where category = 'Electronics' order by name + - explain: "COVERING(IDX_IOT_FILTERED_ELECTRONICS <,> -> [ID: KEY[2], NAME: KEY[0], PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE, _.STOCK AS STOCK)" + - result: [{ "Phone X", 799.99, 120 }, { "Speaker Z", 89.99, 150 }, { "Tablet Y", 299.99, 80 }, { "Widget A", 19.99, 100 }, { "Widget B", 29.99, 50 }] + + # Test filtered index - multiple predicates (price > 15 AND stock > 60) + - + - query: select category, price, stock from products_mat_view where price > 15 and stock > 60 order by category + - explain: "COVERING(IDX_MV_FILTERED_MULTI <,> -> [CATEGORY: KEY[0], ID: KEY[2], PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.STOCK AS STOCK)" + - unorderedResult: [{!null _, 15.5, 200}, { "Electronics", 19.99, 100 }, { "Electronics", 89.99, 150 }, { "Electronics", 299.99, 80 }, { "Electronics", 799.99, 120 }, { "Home", 25.50, 90 }] + - + - query: select category, price, stock from products_index_on where price > 15 and stock > 60 order by category + - explain: "COVERING(IDX_IOT_FILTERED_MULTI <,> -> [CATEGORY: KEY[0], ID: KEY[2], PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.STOCK AS STOCK)" + - unorderedResult: [{!null _, 15.5, 200}, { "Electronics", 19.99, 100 }, { "Electronics", 89.99, 150 }, { "Electronics", 299.99, 80 }, { "Electronics", 799.99, 120 }, { "Home", 25.50, 90 }] + + # Test filtered index with additional filter in query + - + - query: select name, price from products_mat_view where price > 50 order by price + - explain: "COVERING(IDX_MV_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE)" + - result: [{ "Speaker Z", 89.99 }, { "Tablet Y", 299.99 }, { "Phone X", 799.99 }] + - + - query: select name, price from products_index_on where price > 50 order by price + - explain: "COVERING(IDX_IOT_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE)" + - result: [{ "Speaker Z", 89.99 }, { "Tablet Y", 299.99 }, { "Phone X", 799.99 }] + +... + diff --git a/yaml-tests/src/test/resources/index-ddl.metrics.binpb b/yaml-tests/src/test/resources/index-ddl.metrics.binpb index de8b4081b3..1ae6b9fe8f 100644 --- a/yaml-tests/src/test/resources/index-ddl.metrics.binpb +++ b/yaml-tests/src/test/resources/index-ddl.metrics.binpb @@ -10,18 +10,18 @@ X 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 { + basic_indexFEXPLAIN select name from customers_index_on_table where name = 'Alice'— +ÑŽòéoÕ ­Ò•=(­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)" ]; + 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_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" ]; + 2 -> 1 [ label=< q347> label="q347" 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å @@ -37,15 +37,15 @@ X }Á U basic_indexFEXPLAIN select email from customers_index_on_table order by email descç -î§Ñ€Wi œÂîE(d0½«©8@yCOVERING(IDX_IOT_EMAIL <,> -> [EMAIL: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[2]]) | MAP (_.EMAIL AS EMAIL)Í digraph G { +Þì…¾ji û„ÊV(\0Ž“8@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)" ]; + 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_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" ]; + 2 -> 1 [ label=< q126> label="q126" 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¯ @@ -58,18 +58,18 @@ j 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(m0ˆùŠ8&@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 { + basic_indexXEXPLAIN select age, city from customers_index_on_table where age > 25 order by age, city° +ø¸ßÎ% ¢•Š(e0³çc8&@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)" ]; + 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_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" ]; + 2 -> 1 [ label=< q141> label="q141" 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'‘ @@ -82,18 +82,18 @@ k 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'” -Žù»½ŒÍ ¯Öý@(Í0žÅÀ8™@»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 { +include_clauseVEXPLAIN select name, email, country from customers_index_on_table where name = 'Alice'“ +–€¦ŒnÑ Ñš´0(©0Òé£8Õ@»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)" ]; + 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_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" ]; + 2 -> 1 [ label=< q346> label="q346" 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Ú @@ -109,15 +109,15 @@ v }Ô 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 { +ø¦ÏÀ Õœ÷ (e0„á*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)" ]; + 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_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" ]; + 2 -> 1 [ label=< q141> label="q141" 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â @@ -133,15 +133,15 @@ w }Ý t mixed_asc_descbEXPLAIN select age, city from customers_index_on_table where age < 40 order by age desc, city descä -ˆû‹Í îò’(m0šÛ28&@’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 { +ø­Ÿó é•ï (e0¡í'8&@’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)" ]; + 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_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" ]; + 2 -> 1 [ label=< q141> label="q141" 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Ž @@ -157,15 +157,15 @@ v }ˆ s mixed_asc_descaEXPLAIN select age, city from customers_index_on_table where age < 50 order by age desc, city asc -ˆˆª• ‹–„(m0ž—18&@»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 { +ø泘 —ðÌ(e0Ùê/8&@»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)" ]; + 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_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" ]; + 2 -> 1 [ label=< q141> label="q141" 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Ö @@ -179,19 +179,18 @@ l 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ã -êºôœ• Ìðþ(t0¢üg8@@ -dCOVERING(IDX_IOT_AGE_MAX_EXTREMUM <,> -> [COUNTRY: KEY[0], ID: KEY[2]]) | MAP (_.COUNTRY AS COUNTRY)Þ digraph G { +nulls_first_lastUEXPLAIN SELECT country FROM customers_index_on_table ORDER BY country ASC NULLS FIRSTØ +Þèªái Ó¾å(\0•Ñ8@_COVERING(IDX_IOT_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 (q150.COUNTRY AS COUNTRY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY)" ]; + 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_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 [ label=<
Index
IDX_IOT_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=< q150> label="q150" color="gray20" style="bold" 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" ]; }ê k nulls_first_lastWEXPLAIN SELECT country FROM customers_materialized_view ORDER BY country ASC NULLS LASTú @@ -207,15 +206,15 @@ k }é h nulls_first_lastTEXPLAIN SELECT country FROM customers_index_on_table ORDER BY country ASC NULLS LASTü -îÁ½íi ú°ñ (d0âœ8@ƒCOVERING(IDX_IOT_NULLS_LAST <,> -> [COUNTRY: from_ordered_bytes(KEY:[0], ASC_NULLS_LAST), ID: KEY[2]]) | MAP (_.COUNTRY AS COUNTRY)Ø digraph G { +ÞÊþŽi ¥û«(\0ÞÔ8@ƒ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)" ]; + 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_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" ]; + 2 -> 1 [ label=< q126> label="q126" 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ì @@ -232,15 +231,15 @@ e }Õ b nulls_first_lastNEXPLAIN SELECT age FROM customers_index_on_table ORDER BY age DESC NULLS FIRSTî -îãºèi ©º (d0ç8@COVERING(IDX_IOT_DESC_NULLS_FIRST <,> -> [AGE: from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST), ID: KEY[2]]) | MAP (_.AGE AS AGE)Ï digraph G { +Þú°èi ¤Ùý (\0§Õ 8@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)" ]; + 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_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" ]; + 2 -> 1 [ label=< q126> label="q126" 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ê @@ -256,15 +255,15 @@ d }Ò 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 { +„åð…… ßËî(h0àåL8.@}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)" ]; + 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_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" ]; + 2 -> 1 [ label=< q146> label="q146" 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 @@ -277,6 +276,18 @@ a 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_optionjEXPLAIN SELECT country, min_ever(age) FROM customers_index_on_table WHERE country = 'USA' GROUP BY country‚ +óó‚¹9 ±’³&(~0—À…8K@ŽAISCAN(IDX_IOT_AGE_MIN_EXTREMUM [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_IOT_AGE_MIN_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=< 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 @@ -289,6 +300,18 @@ a 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_optionmEXPLAIN SELECT country, max_ever(age) FROM customers_index_on_table WHERE country = 'Canada' GROUP BY country‚ +ó¸þ›; ä“Ò&(~0ɧ¤8K@ŽAISCAN(IDX_IOT_AGE_MAX_EXTREMUM [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_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=< 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È @@ -308,6 +331,25 @@ a 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_optionyEXPLAIN SELECT country, min_ever(age), max_ever(age) FROM customers_index_on_table WHERE country = 'USA' GROUP BY countryÌ +‰ +—¯šAÛ ’ìÚ-(’0…½”8y@ÔAISCAN(IDX_IOT_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_IOT_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_IOT_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_IOT_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_without_legacy_optionpEXPLAIN SELECT country, min_ever(age) FROM customers_materialized_view WHERE country = 'France' GROUP BY country @@ -320,6 +362,18 @@ a 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_optionmEXPLAIN SELECT country, min_ever(age) FROM customers_index_on_table WHERE country = 'France' GROUP BY country‚ +óó‚¹9 ±’³&(~0—À…8K@ŽAISCAN(IDX_IOT_AGE_MIN_EXTREMUM [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_IOT_AGE_MIN_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=< 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 @@ -332,6 +386,18 @@ a 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_optionmEXPLAIN SELECT country, max_ever(age) FROM customers_index_on_table WHERE country = 'France' GROUP BY country‚ +ó¸þ›; ä“Ò&(~0ɧ¤8K@ŽAISCAN(IDX_IOT_AGE_MAX_EXTREMUM [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_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=< 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 @@ -344,6 +410,18 @@ a 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_optionjEXPLAIN SELECT country, min_ever(age) FROM customers_index_on_table WHERE country = 'USA' GROUP BY country‚ +óó‚¹9 ±’³&(~0—À…8K@ŽAISCAN(IDX_IOT_AGE_MIN_EXTREMUM [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_IOT_AGE_MIN_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=< 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 @@ -356,6 +434,18 @@ a 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, max_ever(age) FROM customers_index_on_table WHERE country = 'Canada' GROUP BY country‚ +ó¸þ›; ä“Ò&(~0ɧ¤8K@ŽAISCAN(IDX_IOT_AGE_MAX_EXTREMUM [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_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=< 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È @@ -375,6 +465,25 @@ a 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_optionyEXPLAIN SELECT country, min_ever(age), max_ever(age) FROM customers_index_on_table WHERE country = 'USA' GROUP BY countryÌ +‰ +—¯šAÛ ’ìÚ-(’0…½”8y@ÔAISCAN(IDX_IOT_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_IOT_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_IOT_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_IOT_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_with_legacy_optionpEXPLAIN SELECT country, min_ever(age) FROM customers_materialized_view WHERE country = 'France' GROUP BY country @@ -387,6 +496,18 @@ a 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_optionmEXPLAIN SELECT country, min_ever(age) FROM customers_index_on_table WHERE country = 'France' GROUP BY country‚ +óó‚¹9 ±’³&(~0—À…8K@ŽAISCAN(IDX_IOT_AGE_MIN_EXTREMUM [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_IOT_AGE_MIN_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=< 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 @@ -399,4 +520,16 @@ a 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, max_ever(age) FROM customers_index_on_table WHERE country = 'France' GROUP BY country‚ +ó¸þ›; ä“Ò&(~0ɧ¤8K@ŽAISCAN(IDX_IOT_AGE_MAX_EXTREMUM [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_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=< 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 index f0eb42eec3..4605422a8f 100644 --- a/yaml-tests/src/test/resources/index-ddl.metrics.yaml +++ b/yaml-tests/src/test/resources/index-ddl.metrics.yaml @@ -13,14 +13,14 @@ basic_index: - 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 + task_count: 1873 + task_total_time_ms: 234 + transform_count: 469 + transform_time_ms: 128 + transform_yield_count: 173 + insert_time_ms: 16 + insert_new_count: 219 + insert_reused_count: 21 - 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)' @@ -35,12 +35,12 @@ basic_index: - 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 + task_count: 350 + task_total_time_ms: 223 transform_count: 105 - transform_time_ms: 146 - transform_yield_count: 100 - insert_time_ms: 2 + transform_time_ms: 181 + transform_yield_count: 92 + insert_time_ms: 8 insert_new_count: 20 insert_reused_count: 2 - query: EXPLAIN select age, city from customers_materialized_view where age > 25 @@ -59,12 +59,12 @@ basic_index: 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 + task_count: 504 + task_total_time_ms: 78 transform_count: 141 - transform_time_ms: 143 - transform_yield_count: 109 - insert_time_ms: 6 + transform_time_ms: 50 + transform_yield_count: 101 + insert_time_ms: 1 insert_new_count: 38 insert_reused_count: 2 include_clause: @@ -86,14 +86,14 @@ include_clause: 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 + task_count: 1814 + task_total_time_ms: 230 + transform_count: 465 + transform_time_ms: 101 + transform_yield_count: 169 + insert_time_ms: 13 + insert_new_count: 213 + insert_reused_count: 21 mixed_asc_desc: - query: EXPLAIN select age, city from customers_materialized_view where age > 30 order by age asc, city desc @@ -113,11 +113,11 @@ mixed_asc_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 + task_count: 504 + task_total_time_ms: 40 transform_count: 141 - transform_time_ms: 39 - transform_yield_count: 109 + transform_time_ms: 25 + transform_yield_count: 101 insert_time_ms: 0 insert_new_count: 38 insert_reused_count: 2 @@ -137,11 +137,11 @@ mixed_asc_desc: 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 + task_count: 504 + task_total_time_ms: 43 transform_count: 141 - transform_time_ms: 40 - transform_yield_count: 109 + transform_time_ms: 24 + transform_yield_count: 101 insert_time_ms: 0 insert_new_count: 38 insert_reused_count: 2 @@ -163,11 +163,11 @@ mixed_asc_desc: 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 + task_count: 504 + task_total_time_ms: 61 transform_count: 141 - transform_time_ms: 39 - transform_yield_count: 109 + transform_time_ms: 36 + transform_yield_count: 101 insert_time_ms: 0 insert_new_count: 38 insert_reused_count: 2 @@ -186,16 +186,16 @@ nulls_first_last: 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 + explain: 'COVERING(IDX_IOT_NULLS_FIRST <,> -> [COUNTRY: KEY[0], ID: KEY[2]]) | + MAP (_.COUNTRY AS COUNTRY)' + task_count: 350 + task_total_time_ms: 16 + transform_count: 105 + transform_time_ms: 10 + transform_yield_count: 92 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 - 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], @@ -212,11 +212,11 @@ nulls_first_last: 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 + task_count: 350 + task_total_time_ms: 10 transform_count: 105 - transform_time_ms: 24 - transform_yield_count: 100 + transform_time_ms: 7 + transform_yield_count: 92 insert_time_ms: 0 insert_new_count: 20 insert_reused_count: 2 @@ -236,11 +236,11 @@ nulls_first_last: 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_count: 350 task_total_time_ms: 37 transform_count: 105 - transform_time_ms: 24 - transform_yield_count: 100 + transform_time_ms: 27 + transform_yield_count: 92 insert_time_ms: 0 insert_new_count: 20 insert_reused_count: 2 @@ -260,11 +260,11 @@ nulls_first_last: 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 + task_count: 516 + task_total_time_ms: 54 transform_count: 133 - transform_time_ms: 28 - transform_yield_count: 112 + transform_time_ms: 33 + transform_yield_count: 104 insert_time_ms: 1 insert_new_count: 46 insert_reused_count: 6 @@ -281,6 +281,18 @@ extremum_ever_without_legacy_option: insert_time_ms: 1 insert_new_count: 75 insert_reused_count: 4 +- query: EXPLAIN SELECT country, min_ever(age) FROM customers_index_on_table WHERE + country = 'USA' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [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: 120 + transform_count: 257 + transform_time_ms: 80 + transform_yield_count: 126 + insert_time_ms: 2 + 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 @@ -293,6 +305,18 @@ extremum_ever_without_legacy_option: insert_time_ms: 1 insert_new_count: 75 insert_reused_count: 4 +- query: EXPLAIN SELECT country, max_ever(age) FROM customers_index_on_table WHERE + country = 'Canada' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [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: 124 + transform_count: 257 + transform_time_ms: 81 + transform_yield_count: 126 + insert_time_ms: 2 + 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 @@ -308,6 +332,21 @@ extremum_ever_without_legacy_option: insert_time_ms: 5 insert_new_count: 121 insert_reused_count: 6 +- query: EXPLAIN SELECT country, min_ever(age), max_ever(age) FROM customers_index_on_table + WHERE country = 'USA' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_IOT_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: 136 + transform_count: 347 + transform_time_ms: 95 + transform_yield_count: 146 + insert_time_ms: 4 + 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 @@ -320,6 +359,18 @@ extremum_ever_without_legacy_option: insert_time_ms: 1 insert_new_count: 75 insert_reused_count: 4 +- query: EXPLAIN SELECT country, min_ever(age) FROM customers_index_on_table WHERE + country = 'France' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [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: 120 + transform_count: 257 + transform_time_ms: 80 + transform_yield_count: 126 + insert_time_ms: 2 + 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 @@ -332,6 +383,18 @@ extremum_ever_without_legacy_option: insert_time_ms: 1 insert_new_count: 75 insert_reused_count: 4 +- query: EXPLAIN SELECT country, max_ever(age) FROM customers_index_on_table WHERE + country = 'France' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [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: 124 + transform_count: 257 + transform_time_ms: 81 + transform_yield_count: 126 + insert_time_ms: 2 + 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 @@ -345,6 +408,18 @@ extremum_ever_with_legacy_option: insert_time_ms: 1 insert_new_count: 75 insert_reused_count: 4 +- query: EXPLAIN SELECT country, min_ever(age) FROM customers_index_on_table WHERE + country = 'USA' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [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: 120 + transform_count: 257 + transform_time_ms: 80 + transform_yield_count: 126 + insert_time_ms: 2 + 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 @@ -357,6 +432,18 @@ extremum_ever_with_legacy_option: insert_time_ms: 1 insert_new_count: 75 insert_reused_count: 4 +- query: EXPLAIN SELECT country, max_ever(age) FROM customers_index_on_table WHERE + country = 'Canada' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [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: 124 + transform_count: 257 + transform_time_ms: 81 + transform_yield_count: 126 + insert_time_ms: 2 + 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 @@ -372,6 +459,21 @@ extremum_ever_with_legacy_option: insert_time_ms: 5 insert_new_count: 121 insert_reused_count: 6 +- query: EXPLAIN SELECT country, min_ever(age), max_ever(age) FROM customers_index_on_table + WHERE country = 'USA' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_IOT_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: 136 + transform_count: 347 + transform_time_ms: 95 + transform_yield_count: 146 + insert_time_ms: 4 + 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 @@ -384,6 +486,18 @@ extremum_ever_with_legacy_option: insert_time_ms: 1 insert_new_count: 75 insert_reused_count: 4 +- query: EXPLAIN SELECT country, min_ever(age) FROM customers_index_on_table WHERE + country = 'France' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [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: 120 + transform_count: 257 + transform_time_ms: 80 + transform_yield_count: 126 + insert_time_ms: 2 + 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 @@ -396,3 +510,15 @@ extremum_ever_with_legacy_option: insert_time_ms: 1 insert_new_count: 75 insert_reused_count: 4 +- query: EXPLAIN SELECT country, max_ever(age) FROM customers_index_on_table WHERE + country = 'France' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [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: 124 + transform_count: 257 + transform_time_ms: 81 + transform_yield_count: 126 + insert_time_ms: 2 + 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 index d5abfb3fcf..a81eb120db 100644 --- a/yaml-tests/src/test/resources/index-ddl.yamsql +++ b/yaml-tests/src/test/resources/index-ddl.yamsql @@ -76,8 +76,8 @@ schema_template: 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 - 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) + create index idx_iot_age_min_no_legacy on view_iot_age_min_no_legacy(min_age, country) + create index idx_iot_age_max_no_legacy on view_iot_age_max_no_legacy(max_age, country) 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 @@ -85,8 +85,8 @@ schema_template: 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 - 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) + create index idx_iot_age_min_extremum on view_iot_age_min(min_age, country) options (legacy_extremum_ever) + create index idx_iot_age_max_extremum on view_iot_age_max(max_age, country) options (legacy_extremum_ever) --- setup: steps: @@ -97,13 +97,11 @@ setup: (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 @@ -117,7 +115,6 @@ test_block: - 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 @@ -127,7 +124,6 @@ test_block: - 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 @@ -137,7 +133,6 @@ test_block: - 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 @@ -150,7 +145,6 @@ test_block: - 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 @@ -164,7 +158,6 @@ test_block: - 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 @@ -174,7 +167,6 @@ test_block: - 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 @@ -184,7 +176,6 @@ test_block: - 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 @@ -194,42 +185,34 @@ test_block: - 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)" + - explain: "COVERING(IDX_IOT_NULLS_FIRST <,> -> [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 @@ -239,20 +222,16 @@ test_block: - - 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') + - query: INSERT INTO customers_index_on_table VALUES (5, 'David', 'david@example.com', 28, 'Boston', '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') + - query: INSERT INTO customers_materialized_view VALUES (5, 'David', 'david@example.com', 28, 'Boston', 'USA', 'Architect') - count: 1 - - - - query: INSERT INTO customers_index_on_table VALUES (5, 'Eve', 'eve@example.com', 32, 'Seattle', 'USA', 'Architect') + - query: INSERT INTO customers_index_on_table VALUES (5, 'David', 'david@example.com', 28, 'Seattle', 'USA', 'Architect') - count: 1 - --- test_block: name: extremum_ever_without_legacy_option @@ -260,37 +239,51 @@ test_block: # 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}] - + - + - query: SELECT country, min_ever(age) FROM customers_index_on_table WHERE country = 'USA' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [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}] - + - + - query: SELECT country, max_ever(age) FROM customers_index_on_table WHERE country = 'Canada' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [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}] - + - + - query: SELECT country, min_ever(age), max_ever(age) FROM customers_index_on_table WHERE country = 'USA' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_IOT_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}] - + - + - query: SELECT country, min_ever(age) FROM customers_index_on_table WHERE country = 'France' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [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}] - + - + - query: SELECT country, max_ever(age) FROM customers_index_on_table WHERE country = 'France' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [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 @@ -298,34 +291,49 @@ test_block: # 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}] - + - + - query: SELECT country, min_ever(age) FROM customers_index_on_table WHERE country = 'USA' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [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}] - + - + - query: SELECT country, max_ever(age) FROM customers_index_on_table WHERE country = 'Canada' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [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}] - + - + - query: SELECT country, min_ever(age), max_ever(age) FROM customers_index_on_table WHERE country = 'USA' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_IOT_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}] - + - + - query: SELECT country, min_ever(age) FROM customers_index_on_table WHERE country = 'France' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [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}] + - + - query: SELECT country, max_ever(age) FROM customers_index_on_table WHERE country = 'France' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{France, 35}] ... From 9a112fff23aeba53ecef1008f6eccf4fc0612614 Mon Sep 17 00:00:00 2001 From: Youssef Hatem Date: Fri, 28 Nov 2025 17:40:01 +0000 Subject: [PATCH 5/7] add tests. --- .../relational/api/ddl/IndexTest.java | 14 + .../src/test/java/YamlIntegrationTests.java | 26 +- .../index-ddl-aggregates-only.metrics.binpb | 353 +++++++++++++++++ .../index-ddl-aggregates-only.metrics.yaml | 357 ++++++++++++++++++ .../index-ddl-aggregates-only.yamsql | 320 ++++++++++++++++ 5 files changed, 1059 insertions(+), 11 deletions(-) create mode 100644 yaml-tests/src/test/resources/index-ddl-aggregates-only.metrics.binpb create mode 100644 yaml-tests/src/test/resources/index-ddl-aggregates-only.metrics.yaml create mode 100644 yaml-tests/src/test/resources/index-ddl-aggregates-only.yamsql diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java index 2fcda7611a..703d415065 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java @@ -831,6 +831,20 @@ void createAggregateIndexOnMinMaxWithPermutedOrdering(String index) throws Excep ); } + @ParameterizedTest + @ValueSource(strings = {"MIN", "MAX"}) + void createAggregateIndexOnSourceOnMinMaxWithPermutedOrdering(String index) throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T1(col1 bigint, col2 bigint, col3 bigint, col4 bigint, primary key(col1)) " + + String.format(Locale.ROOT, "CREATE VIEW v1 AS SELECT col1, col2, col3, %s(col4) as agg FROM T1 group by col1, col2, col3 ", index) + + "CREATE INDEX mv1 ON v1(col1, col2, agg, col3)"; + indexIs(stmt, + field("COL4").groupBy(concatenateFields("COL1", "COL2", "COL3")), + "MIN".equals(index) ? IndexTypes.PERMUTED_MIN : IndexTypes.PERMUTED_MAX, + idx -> Assertions.assertEquals("1", idx.getOptions().get("permutedSize")) + ); + } + @ParameterizedTest @ValueSource(strings = {"MIN", "MAX"}) void createAggregateIndexOnMinMaxWithGroupingColumnsMissingInOrdering(String index) throws Exception { diff --git a/yaml-tests/src/test/java/YamlIntegrationTests.java b/yaml-tests/src/test/java/YamlIntegrationTests.java index a8508977c2..56b3eef85a 100644 --- a/yaml-tests/src/test/java/YamlIntegrationTests.java +++ b/yaml-tests/src/test/java/YamlIntegrationTests.java @@ -232,6 +232,21 @@ void arrays(YamlTest.Runner runner) throws Exception { runner.runYamsql("arrays.yamsql"); } + @TestTemplate + public void indexDdl(YamlTest.Runner runner) throws Exception { + runner.runYamsql("index-ddl.yamsql"); + } + + @TestTemplate + public void indexDdlValuesOnly(YamlTest.Runner runner) throws Exception { + runner.runYamsql("index-ddl-values-only.yamsql"); + } + + @TestTemplate + public void indexDdlAggregatesOnly(YamlTest.Runner runner) throws Exception { + runner.runYamsql("index-ddl-aggregates-only.yamsql"); + } + @TestTemplate public void insertEnum(YamlTest.Runner runner) throws Exception { runner.runYamsql("insert-enum.yamsql"); @@ -354,17 +369,6 @@ public void validIdentifierTests(YamlTest.Runner runner) throws Exception { runner.runYamsql("valid-identifiers.yamsql"); } - @TestTemplate - public void indexDdl(YamlTest.Runner runner) throws Exception { - runner.runYamsql("index-ddl.yamsql"); - } - - @TestTemplate - @MaintainYamlTestConfig(YamlTestConfigFilters.CORRECT_EXPLAIN_AND_METRICS) - public void indexDdlValuesOnly(YamlTest.Runner runner) throws Exception { - runner.runYamsql("index-ddl-values-only.yamsql"); - } - @TestTemplate public void validIdentifiersTest(YamlTest.Runner runner) throws Exception { runner.runYamsql("valid-identifiers.yamsql"); diff --git a/yaml-tests/src/test/resources/index-ddl-aggregates-only.metrics.binpb b/yaml-tests/src/test/resources/index-ddl-aggregates-only.metrics.binpb new file mode 100644 index 0000000000..d6ec0c7b4f --- /dev/null +++ b/yaml-tests/src/test/resources/index-ddl-aggregates-only.metrics.binpb @@ -0,0 +1,353 @@ +ß +† +sum_aggregate_single_groupingeEXPLAIN select category, sum(amount) as total from sales_mat_view group by category order by categoryÓ +ŠÖÀŅ̃ ûí½ (Œ0÷ÊÊ8b@ +sAISCAN(IDX_MV_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS TOTAL)» digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_MV_SUM_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}á +† +sum_aggregate_single_groupingeEXPLAIN select category, sum(amount) as total from sales_index_on group by category order by categoryÕ +Š£Ûëͧ ˜Ñ±Ÿ(Œ0²×8b@ +tAISCAN(IDX_IOT_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS TOTAL)¼ digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_SUM_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}Î +€ +sum_aggregate_single_grouping_EXPLAIN select region, sum(amount) as total from sales_mat_view group by region order by regionÈ +®ˆ«œ¨¶ ™¿ä(w0‰­8$@oAISCAN(IDX_MV_SUM_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS TOTAL)µ digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS REGION, q6._1 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS REGION, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_MV_SUM_BY_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}Í +€ +sum_aggregate_single_grouping_EXPLAIN select region, sum(amount) as total from sales_index_on group by region order by regionÇ +®ÆìF¶ ûÑ´9(w0¾¬G8$@pAISCAN(IDX_IOT_SUM_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS TOTAL)¶ digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS REGION, q6._1 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS REGION, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_SUM_BY_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}Á + +sum_aggregate_multi_grouping}EXPLAIN select category, region, sum(amount) as total from sales_mat_view group by category, region order by category, regionž +¯›äØø Õ‹ía(~0Á‹Ø8E@AISCAN(IDX_MV_CAT_REGION_SUM <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL)ë digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS REGION, q6._2 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, STRING AS REGION, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1, LONG AS _2)" ]; + 3 [ label=<
Index
IDX_MV_CAT_REGION_SUM
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}à + +sum_aggregate_multi_grouping}EXPLAIN select category, region, sum(amount) as total from sales_index_on group by category, region order by category, region  +¯þƒ¼„ø ©üÆd(~0ó£æ8E@AISCAN(IDX_IOT_CAT_REGION_SUM <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL)ì digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS REGION, q6._2 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, STRING AS REGION, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1, LONG AS _2)" ]; + 3 [ label=<
Index
IDX_IOT_CAT_REGION_SUM
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}Š +³ +sum_aggregate_multi_grouping’EXPLAIN select category, region, sum(amount) as total from sales_mat_view where category = 'Electronics' group by category, region order by regionÑ +á“ß÷—” Œð¼j(0ÙÌê8G@¬AISCAN(IDX_MV_CAT_REGION_SUM [EQUALS promote(@c17 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL)€ digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS REGION, q6._2 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, STRING AS REGION, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c17 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1, LONG AS _2)" ]; + 3 [ label=<
Index
IDX_MV_CAT_REGION_SUM
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}‹ +³ +sum_aggregate_multi_grouping’EXPLAIN select category, region, sum(amount) as total from sales_index_on where category = 'Electronics' group by category, region order by regionÒ +á·×æs” Œò»O(0­ž¼8G@­AISCAN(IDX_IOT_CAT_REGION_SUM [EQUALS promote(@c17 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS REGION, q6._2 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, STRING AS REGION, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c17 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1, LONG AS _2)" ]; + 3 [ label=<
Index
IDX_IOT_CAT_REGION_SUM
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}² + +sum_aggregate_three_grouping¡EXPLAIN select category, region, product_id, sum(amount) as total from sales_mat_view group by category, region, product_id order by category, region, product_idê +Ô¨H£ ‘Ý…<(d0õ .8@¬AISCAN(IDX_MV_SUM_BY_ALL <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: KEY:[2], _3: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS PRODUCT_ID, _._3 AS TOTAL)œ digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS REGION, q6._2 AS PRODUCT_ID, q6._3 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, STRING AS REGION, LONG AS PRODUCT_ID, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1, LONG AS _2, LONG AS _3)" ]; + 3 [ label=<
Index
IDX_MV_SUM_BY_ALL
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}´ + +sum_aggregate_three_grouping¡EXPLAIN select category, region, product_id, sum(amount) as total from sales_index_on group by category, region, product_id order by category, region, product_idì +ÔƒŠÇH£ Ýà™=(d0 æ)8@­AISCAN(IDX_IOT_SUM_BY_ALL <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: KEY:[2], _3: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS PRODUCT_ID, _._3 AS TOTAL) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS REGION, q6._2 AS PRODUCT_ID, q6._3 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, STRING AS REGION, LONG AS PRODUCT_ID, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1, LONG AS _2, LONG AS _3)" ]; + 3 [ label=<
Index
IDX_IOT_SUM_BY_ALL
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}Ë +x +count_star_aggregate`EXPLAIN select category, count(*) as cnt from sales_mat_view group by category order by categoryÎ +ï æŒ>¾ ©í¶)(n0Ùµ‹88@sAISCAN(IDX_MV_COUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS CNT)¹ digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS CNT)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS CNT)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_MV_COUNT_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}Í +x +count_star_aggregate`EXPLAIN select category, count(*) as cnt from sales_index_on group by category order by categoryÐ +ï‹óî=¾ Ãóä&(n0Ѓ…88@tAISCAN(IDX_IOT_COUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS CNT)º digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS CNT)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS CNT)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_COUNT_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}Ë +{ +count_column_aggregateaEXPLAIN select region, count(quantity) as cnt from sales_mat_view group by region order by regionË +ÿ“Å0­ ø²•"(h0ß¾^8$@sAISCAN(IDX_MV_COUNT_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS CNT)· digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS REGION, q6._1 AS CNT)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS REGION, LONG AS CNT)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_MV_COUNT_QTY_BY_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}Í +{ +count_column_aggregateaEXPLAIN select region, count(quantity) as cnt from sales_index_on group by region order by regionÍ +ÈØü0­ …ŸŠ"(h0ˆáS8$@tAISCAN(IDX_IOT_COUNT_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS CNT)¸ digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS REGION, q6._1 AS CNT)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS REGION, LONG AS CNT)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_COUNT_QTY_BY_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}— +˜ +aggregate_with_havingEXPLAIN select category, sum(amount) as total from sales_mat_view group by category having sum(amount) > 2000 order by categoryù +åŠç±›¹ ß…q(0—Žè8o@ +¤AISCAN(IDX_MV_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | FILTER _._1 GREATER_THAN promote(@c21 AS LONG) | MAP (_._0 AS CATEGORY, _._1 AS TOTAL)°digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS TOTAL)" ]; + 2 [ label=<
Predicate Filter
WHERE q6._1 GREATER_THAN promote(@c21 AS LONG)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 4 [ label=<
Index
IDX_MV_SUM_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 3 [ 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" ]; +}™ +˜ +aggregate_with_havingEXPLAIN select category, sum(amount) as total from sales_index_on group by category having sum(amount) > 2000 order by categoryû +åþ©ï›¹ íчr(0§øÎ8o@ +¥AISCAN(IDX_IOT_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | FILTER _._1 GREATER_THAN promote(@c21 AS LONG) | MAP (_._0 AS CATEGORY, _._1 AS TOTAL)±digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS TOTAL)" ]; + 2 [ label=<
Predicate Filter
WHERE q6._1 GREATER_THAN promote(@c21 AS LONG)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 4 [ label=<
Index
IDX_IOT_SUM_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 3 [ 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" ]; +}ß +x + min_aggregategEXPLAIN select category, min(amount) as min_amt from sales_mat_view group by category order by categoryâ +ŒÂÙCå ÄÇã.(z0Æã‘8H@zAISCAN(IDX_MV_MIN_AMOUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_AMT)Æ digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS MIN_AMT)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS MIN_AMT)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_MV_MIN_AMOUNT_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}à +x + min_aggregategEXPLAIN select category, min(amount) as min_amt from sales_index_on group by category order by categoryã +ÃŒí;Å §ÌÏ)(u0êõf8:@{AISCAN(IDX_IOT_MIN_AMOUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_AMT)Ç digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS MIN_AMT)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS MIN_AMT)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_MIN_AMOUNT_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}Î +r + max_aggregateaEXPLAIN select region, max(amount) as max_amt from sales_mat_view group by region order by region× +®„È»)´ ãˆÊ(o0‰Á.8&@vAISCAN(IDX_MV_MAX_AMOUNT_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1]]) | MAP (_._0 AS REGION, _._1 AS MAX_AMT)À digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS REGION, q6._1 AS MAX_AMT)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS REGION, LONG AS MAX_AMT)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_MV_MAX_AMOUNT_BY_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}Ð +r + max_aggregateaEXPLAIN select region, max(amount) as max_amt from sales_index_on group by region order by regionÙ +®¾»Ë*´ ¡œ‚ (o0åí78&@wAISCAN(IDX_IOT_MAX_AMOUNT_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1]]) | MAP (_._0 AS REGION, _._1 AS MAX_AMT)Á digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS REGION, q6._1 AS MAX_AMT)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS REGION, LONG AS MAX_AMT)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_MAX_AMOUNT_BY_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}ñ +„ +min_ever_aggregatenEXPLAIN select category, min_ever(quantity) as min_qty from sales_mat_view group by category order by categoryç +Ñœ”ý(» ÝÀî(l0‘˜E82@~AISCAN(IDX_MV_MIN_EVER_QTY_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_QTY)È digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS MIN_QTY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS MIN_QTY)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_MV_MIN_EVER_QTY_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}ó +„ +min_ever_aggregatenEXPLAIN select category, min_ever(quantity) as min_qty from sales_index_on group by category order by categoryé +Ñ–¬ý(» †Ç¨(l0Ö M82@AISCAN(IDX_IOT_MIN_EVER_QTY_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_QTY)É digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS MIN_QTY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS MIN_QTY)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_MIN_EVER_QTY_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}à +~ +max_ever_aggregatehEXPLAIN select region, max_ever(quantity) as max_qty from sales_mat_view group by region order by regionÝ +Ï»•!« ê¢Ó(g0Ÿá+8!@zAISCAN(IDX_MV_MAX_EVER_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS MAX_QTY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS REGION, q6._1 AS MAX_QTY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS REGION, LONG AS MAX_QTY)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_MV_MAX_EVER_QTY_BY_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}â +~ +max_ever_aggregatehEXPLAIN select region, max_ever(quantity) as max_qty from sales_index_on group by region order by regionß +ÁÀŒ!« ȱÿ(g0«²+8!@{AISCAN(IDX_IOT_MAX_EVER_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS MAX_QTY)à digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS REGION, q6._1 AS MAX_QTY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS REGION, LONG AS MAX_QTY)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_MAX_EVER_QTY_BY_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}Ä + +aggregate_in_middle_of_keyEXPLAIN select category, max(amount) as max_amt, region from sales_mat_view group by category, region order by category, region¡ +Çòªš:¸ ùÇ *(l0Šå²8.@AISCAN(IDX_MV_CAT_MAX_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: KEY:[2]]) | MAP (_._0 AS CATEGORY, _._2 AS MAX_AMT, _._1 AS REGION)ï digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._2 AS MAX_AMT, q6._1 AS REGION)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS MAX_AMT, STRING AS REGION)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1, LONG AS _2)" ]; + 3 [ label=<
Index
IDX_MV_CAT_MAX_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}¡ + +aggregate_in_middle_of_keyEXPLAIN select category, max(amount) as max_amt, region from sales_index_on group by category, region order by category, regionþ +ʰ®ì3– ¹§©&(e0‘è‡8!@ÇISCAN(IDX_IOT_MIN_CAT_REGION <,>) | MAP (_ AS _0) | AGG (max_l(_._0.AMOUNT) AS _0) GROUP BY (_._0.CATEGORY AS _0, _._0.REGION AS _1) | MAP (_._0._0 AS CATEGORY, _._1._0 AS MAX_AMT, _._0._1 AS REGION)”digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0._0 AS CATEGORY, q6._1._0 AS MAX_AMT, q6._0._1 AS REGION)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS MAX_AMT, STRING AS REGION)" ]; + 2 [ label=<
Streaming Aggregate
COLLECT (max_l(q206._0.AMOUNT) AS _0)
GROUP BY (q206._0.CATEGORY AS _0, q206._0.REGION AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1 AS _0, LONG AS _0 AS _1)" ]; + 3 [ label=<
Value Computation
MAP (q2 AS _0)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY AS _0)" ]; + 4 [ label=<
Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 5 [ label=<
Index
IDX_IOT_MIN_CAT_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ label=< q206> label="q206" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 3 [ label=< q2> label="q2" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 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" ]; +}Ä + +aggregate_in_middle_of_keyEXPLAIN select min(amount) as min_amt, category, region from sales_mat_view group by category, region order by category, region¡ +Çü«9¸ ¦õ™*(l0ÅÄ8.@AISCAN(IDX_MV_MIN_CAT_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: KEY:[2]]) | MAP (_._2 AS MIN_AMT, _._0 AS CATEGORY, _._1 AS REGION)ï digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._2 AS MIN_AMT, q6._0 AS CATEGORY, q6._1 AS REGION)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS MIN_AMT, STRING AS CATEGORY, STRING AS REGION)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1, LONG AS _2)" ]; + 3 [ label=<
Index
IDX_MV_MIN_CAT_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 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" ]; +}  + +aggregate_in_middle_of_keyEXPLAIN select min(amount) as min_amt, category, region from sales_index_on group by category, region order by category, regioný +ÊÁŸ§– õµø(e0ܵ)8!@ÇISCAN(IDX_IOT_MIN_CAT_REGION <,>) | MAP (_ AS _0) | AGG (min_l(_._0.AMOUNT) AS _0) GROUP BY (_._0.CATEGORY AS _0, _._0.REGION AS _1) | MAP (_._1._0 AS MIN_AMT, _._0._0 AS CATEGORY, _._0._1 AS REGION)”digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._1._0 AS MIN_AMT, q6._0._0 AS CATEGORY, q6._0._1 AS REGION)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS MIN_AMT, STRING AS CATEGORY, STRING AS REGION)" ]; + 2 [ label=<
Streaming Aggregate
COLLECT (min_l(q206._0.AMOUNT) AS _0)
GROUP BY (q206._0.CATEGORY AS _0, q206._0.REGION AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1 AS _0, LONG AS _0 AS _1)" ]; + 3 [ label=<
Value Computation
MAP (q2 AS _0)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY AS _0)" ]; + 4 [ label=<
Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 5 [ label=<
Index
IDX_IOT_MIN_CAT_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ label=< q206> label="q206" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 3 [ label=< q2> label="q2" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 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" ]; +} \ No newline at end of file diff --git a/yaml-tests/src/test/resources/index-ddl-aggregates-only.metrics.yaml b/yaml-tests/src/test/resources/index-ddl-aggregates-only.metrics.yaml new file mode 100644 index 0000000000..7f4c42fb71 --- /dev/null +++ b/yaml-tests/src/test/resources/index-ddl-aggregates-only.metrics.yaml @@ -0,0 +1,357 @@ +sum_aggregate_single_grouping: +- query: EXPLAIN select category, sum(amount) as total from sales_mat_view group + by category order by category + explain: 'AISCAN(IDX_MV_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) + | MAP (_._0 AS CATEGORY, _._1 AS TOTAL)' + task_count: 1034 + task_total_time_ms: 429 + transform_count: 295 + transform_time_ms: 336 + transform_yield_count: 140 + insert_time_ms: 7 + insert_new_count: 98 + insert_reused_count: 10 +- query: EXPLAIN select category, sum(amount) as total from sales_index_on group + by category order by category + explain: 'AISCAN(IDX_IOT_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) + | MAP (_._0 AS CATEGORY, _._1 AS TOTAL)' + task_count: 1034 + task_total_time_ms: 431 + transform_count: 295 + transform_time_ms: 334 + transform_yield_count: 140 + insert_time_ms: 7 + insert_new_count: 98 + insert_reused_count: 10 +- query: EXPLAIN select region, sum(amount) as total from sales_mat_view group by + region order by region + explain: 'AISCAN(IDX_MV_SUM_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) + | MAP (_._0 AS REGION, _._1 AS TOTAL)' + task_count: 558 + task_total_time_ms: 352 + transform_count: 182 + transform_time_ms: 297 + transform_yield_count: 119 + insert_time_ms: 4 + insert_new_count: 36 + insert_reused_count: 3 +- query: EXPLAIN select region, sum(amount) as total from sales_index_on group by + region order by region + explain: 'AISCAN(IDX_IOT_SUM_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) + | MAP (_._0 AS REGION, _._1 AS TOTAL)' + task_count: 558 + task_total_time_ms: 148 + transform_count: 182 + transform_time_ms: 120 + transform_yield_count: 119 + insert_time_ms: 1 + insert_new_count: 36 + insert_reused_count: 3 +sum_aggregate_multi_grouping: +- query: EXPLAIN select category, region, sum(amount) as total from sales_mat_view + group by category, region order by category, region + explain: 'AISCAN(IDX_MV_CAT_REGION_SUM <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], + _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL)' + task_count: 815 + task_total_time_ms: 271 + transform_count: 248 + transform_time_ms: 205 + transform_yield_count: 126 + insert_time_ms: 3 + insert_new_count: 69 + insert_reused_count: 7 +- query: EXPLAIN select category, region, sum(amount) as total from sales_index_on + group by category, region order by category, region + explain: 'AISCAN(IDX_IOT_CAT_REGION_SUM <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], + _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL)' + task_count: 815 + task_total_time_ms: 277 + transform_count: 248 + transform_time_ms: 210 + transform_yield_count: 126 + insert_time_ms: 3 + insert_new_count: 69 + insert_reused_count: 7 +- query: EXPLAIN select category, region, sum(amount) as total from sales_mat_view + where category = 'Electronics' group by category, region order by region + explain: 'AISCAN(IDX_MV_CAT_REGION_SUM [EQUALS promote(@c17 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 + AS REGION, _._2 AS TOTAL)' + task_count: 865 + task_total_time_ms: 318 + transform_count: 276 + transform_time_ms: 223 + transform_yield_count: 129 + insert_time_ms: 3 + insert_new_count: 71 + insert_reused_count: 3 +- query: EXPLAIN select category, region, sum(amount) as total from sales_index_on + where category = 'Electronics' group by category, region order by region + explain: 'AISCAN(IDX_IOT_CAT_REGION_SUM [EQUALS promote(@c17 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 + AS REGION, _._2 AS TOTAL)' + task_count: 865 + task_total_time_ms: 242 + transform_count: 276 + transform_time_ms: 166 + transform_yield_count: 129 + insert_time_ms: 3 + insert_new_count: 71 + insert_reused_count: 3 +sum_aggregate_three_grouping: +- query: EXPLAIN select category, region, product_id, sum(amount) as total from + sales_mat_view group by category, region, product_id order by category, region, + product_id + explain: 'AISCAN(IDX_MV_SUM_BY_ALL <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], + _2: KEY:[2], _3: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 + AS PRODUCT_ID, _._3 AS TOTAL)' + task_count: 468 + task_total_time_ms: 151 + transform_count: 163 + transform_time_ms: 125 + transform_yield_count: 100 + insert_time_ms: 0 + insert_new_count: 24 + insert_reused_count: 1 +- query: EXPLAIN select category, region, product_id, sum(amount) as total from + sales_index_on group by category, region, product_id order by category, region, + product_id + explain: 'AISCAN(IDX_IOT_SUM_BY_ALL <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], + _2: KEY:[2], _3: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 + AS PRODUCT_ID, _._3 AS TOTAL)' + task_count: 468 + task_total_time_ms: 152 + transform_count: 163 + transform_time_ms: 128 + transform_yield_count: 100 + insert_time_ms: 0 + insert_new_count: 24 + insert_reused_count: 1 +count_star_aggregate: +- query: EXPLAIN select category, count(*) as cnt from sales_mat_view group by category + order by category + explain: 'AISCAN(IDX_MV_COUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) + | MAP (_._0 AS CATEGORY, _._1 AS CNT)' + task_count: 623 + task_total_time_ms: 130 + transform_count: 190 + transform_time_ms: 86 + transform_yield_count: 110 + insert_time_ms: 2 + insert_new_count: 56 + insert_reused_count: 7 +- query: EXPLAIN select category, count(*) as cnt from sales_index_on group by category + order by category + explain: 'AISCAN(IDX_IOT_COUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) + | MAP (_._0 AS CATEGORY, _._1 AS CNT)' + task_count: 623 + task_total_time_ms: 129 + transform_count: 190 + transform_time_ms: 81 + transform_yield_count: 110 + insert_time_ms: 2 + insert_new_count: 56 + insert_reused_count: 7 +count_column_aggregate: +- query: EXPLAIN select region, count(quantity) as cnt from sales_mat_view group + by region order by region + explain: 'AISCAN(IDX_MV_COUNT_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: + VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS CNT)' + task_count: 528 + task_total_time_ms: 101 + transform_count: 173 + transform_time_ms: 71 + transform_yield_count: 104 + insert_time_ms: 1 + insert_new_count: 36 + insert_reused_count: 3 +- query: EXPLAIN select region, count(quantity) as cnt from sales_index_on group + by region order by region + explain: 'AISCAN(IDX_IOT_COUNT_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: + VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS CNT)' + task_count: 528 + task_total_time_ms: 102 + transform_count: 173 + transform_time_ms: 71 + transform_yield_count: 104 + insert_time_ms: 1 + insert_new_count: 36 + insert_reused_count: 3 +aggregate_with_having: +- query: EXPLAIN select category, sum(amount) as total from sales_mat_view group + by category having sum(amount) > 2000 order by category + explain: 'AISCAN(IDX_MV_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) + | FILTER _._1 GREATER_THAN promote(@c21 AS LONG) | MAP (_._0 AS CATEGORY, + _._1 AS TOTAL)' + task_count: 1125 + task_total_time_ms: 325 + transform_count: 313 + transform_time_ms: 236 + transform_yield_count: 144 + insert_time_ms: 7 + insert_new_count: 111 + insert_reused_count: 10 +- query: EXPLAIN select category, sum(amount) as total from sales_index_on group + by category having sum(amount) > 2000 order by category + explain: 'AISCAN(IDX_IOT_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) + | FILTER _._1 GREATER_THAN promote(@c21 AS LONG) | MAP (_._0 AS CATEGORY, + _._1 AS TOTAL)' + task_count: 1125 + task_total_time_ms: 326 + transform_count: 313 + transform_time_ms: 239 + transform_yield_count: 144 + insert_time_ms: 9 + insert_new_count: 111 + insert_reused_count: 10 +min_aggregate: +- query: EXPLAIN select category, min(amount) as min_amt from sales_mat_view group + by category order by category + explain: 'AISCAN(IDX_MV_MIN_AMOUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: + KEY:[1]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_AMT)' + task_count: 780 + task_total_time_ms: 140 + transform_count: 229 + transform_time_ms: 98 + transform_yield_count: 122 + insert_time_ms: 2 + insert_new_count: 72 + insert_reused_count: 8 +- query: EXPLAIN select category, min(amount) as min_amt from sales_index_on group + by category order by category + explain: 'AISCAN(IDX_IOT_MIN_AMOUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], + _1: KEY:[1]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_AMT)' + task_count: 653 + task_total_time_ms: 125 + transform_count: 197 + transform_time_ms: 87 + transform_yield_count: 117 + insert_time_ms: 1 + insert_new_count: 58 + insert_reused_count: 7 +max_aggregate: +- query: EXPLAIN select region, max(amount) as max_amt from sales_mat_view group + by region order by region + explain: 'AISCAN(IDX_MV_MAX_AMOUNT_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: + KEY:[1]]) | MAP (_._0 AS REGION, _._1 AS MAX_AMT)' + task_count: 558 + task_total_time_ms: 86 + transform_count: 180 + transform_time_ms: 64 + transform_yield_count: 111 + insert_time_ms: 0 + insert_new_count: 38 + insert_reused_count: 3 +- query: EXPLAIN select region, max(amount) as max_amt from sales_index_on group + by region order by region + explain: 'AISCAN(IDX_IOT_MAX_AMOUNT_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: + KEY:[1]]) | MAP (_._0 AS REGION, _._1 AS MAX_AMT)' + task_count: 558 + task_total_time_ms: 89 + transform_count: 180 + transform_time_ms: 67 + transform_yield_count: 111 + insert_time_ms: 0 + insert_new_count: 38 + insert_reused_count: 3 +min_ever_aggregate: +- query: EXPLAIN select category, min_ever(quantity) as min_qty from sales_mat_view + group by category order by category + explain: 'AISCAN(IDX_MV_MIN_EVER_QTY_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], + _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_QTY)' + task_count: 593 + task_total_time_ms: 85 + transform_count: 187 + transform_time_ms: 60 + transform_yield_count: 108 + insert_time_ms: 1 + insert_new_count: 50 + insert_reused_count: 7 +- query: EXPLAIN select category, min_ever(quantity) as min_qty from sales_index_on + group by category order by category + explain: 'AISCAN(IDX_IOT_MIN_EVER_QTY_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], + _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_QTY)' + task_count: 593 + task_total_time_ms: 85 + transform_count: 187 + transform_time_ms: 53 + transform_yield_count: 108 + insert_time_ms: 1 + insert_new_count: 50 + insert_reused_count: 7 +max_ever_aggregate: +- query: EXPLAIN select region, max_ever(quantity) as max_qty from sales_mat_view + group by region order by region + explain: 'AISCAN(IDX_MV_MAX_EVER_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: + VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS MAX_QTY)' + task_count: 513 + task_total_time_ms: 69 + transform_count: 171 + transform_time_ms: 47 + transform_yield_count: 103 + insert_time_ms: 0 + insert_new_count: 33 + insert_reused_count: 3 +- query: EXPLAIN select region, max_ever(quantity) as max_qty from sales_index_on + group by region order by region + explain: 'AISCAN(IDX_IOT_MAX_EVER_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], + _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS MAX_QTY)' + task_count: 513 + task_total_time_ms: 69 + transform_count: 171 + transform_time_ms: 48 + transform_yield_count: 103 + insert_time_ms: 0 + insert_new_count: 33 + insert_reused_count: 3 +aggregate_in_middle_of_key: +- query: EXPLAIN select category, max(amount) as max_amt, region from sales_mat_view + group by category, region order by category, region + explain: 'AISCAN(IDX_MV_CAT_MAX_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], + _2: KEY:[2]]) | MAP (_._0 AS CATEGORY, _._2 AS MAX_AMT, _._1 AS REGION)' + task_count: 583 + task_total_time_ms: 122 + transform_count: 184 + transform_time_ms: 88 + transform_yield_count: 108 + insert_time_ms: 2 + insert_new_count: 46 + insert_reused_count: 5 +- query: EXPLAIN select category, max(amount) as max_amt, region from sales_index_on + group by category, region order by category, region + explain: ISCAN(IDX_IOT_MIN_CAT_REGION <,>) | MAP (_ AS _0) | AGG (max_l(_._0.AMOUNT) + AS _0) GROUP BY (_._0.CATEGORY AS _0, _._0.REGION AS _1) | MAP (_._0._0 AS + CATEGORY, _._1._0 AS MAX_AMT, _._0._1 AS REGION) + task_count: 458 + task_total_time_ms: 108 + transform_count: 150 + transform_time_ms: 80 + transform_yield_count: 101 + insert_time_ms: 2 + insert_new_count: 33 + insert_reused_count: 4 +- query: EXPLAIN select min(amount) as min_amt, category, region from sales_mat_view + group by category, region order by category, region + explain: 'AISCAN(IDX_MV_MIN_CAT_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], + _2: KEY:[2]]) | MAP (_._2 AS MIN_AMT, _._0 AS CATEGORY, _._1 AS REGION)' + task_count: 583 + task_total_time_ms: 120 + transform_count: 184 + transform_time_ms: 88 + transform_yield_count: 108 + insert_time_ms: 2 + insert_new_count: 46 + insert_reused_count: 5 +- query: EXPLAIN select min(amount) as min_amt, category, region from sales_index_on + group by category, region order by category, region + explain: ISCAN(IDX_IOT_MIN_CAT_REGION <,>) | MAP (_ AS _0) | AGG (min_l(_._0.AMOUNT) + AS _0) GROUP BY (_._0.CATEGORY AS _0, _._0.REGION AS _1) | MAP (_._1._0 AS + MIN_AMT, _._0._0 AS CATEGORY, _._0._1 AS REGION) + task_count: 458 + task_total_time_ms: 61 + transform_count: 150 + transform_time_ms: 46 + transform_yield_count: 101 + insert_time_ms: 0 + insert_new_count: 33 + insert_reused_count: 4 diff --git a/yaml-tests/src/test/resources/index-ddl-aggregates-only.yamsql b/yaml-tests/src/test/resources/index-ddl-aggregates-only.yamsql new file mode 100644 index 0000000000..3bc59d5f91 --- /dev/null +++ b/yaml-tests/src/test/resources/index-ddl-aggregates-only.yamsql @@ -0,0 +1,320 @@ +# +# index-ddl-aggregates-only.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 AGGREGATE indexes with different aggregation functions +# Compares CREATE INDEX AS SELECT with GROUP BY vs CREATE INDEX ON with aggregate views +# Both syntaxes should produce equivalent query plans + +--- +options: + supported_version: !current_version + noChecks: [metrics] +--- +schema_template: + create table sales_mat_view( + id bigint, + product_id bigint, + category string, + region string, + amount bigint, + quantity bigint, + primary key(id) + ) + + create index idx_mv_sum_by_category as select sum(amount) from sales_mat_view group by category + create index idx_mv_sum_by_region as select sum(amount) from sales_mat_view group by region + create index idx_mv_sum_by_cat_region as select sum(amount) from sales_mat_view group by category, region + create index idx_mv_sum_by_all as select sum(amount) from sales_mat_view group by category, region, product_id + + create index idx_mv_count_by_category as select count(*) from sales_mat_view group by category + create index idx_mv_count_qty_by_region as select count(quantity) from sales_mat_view group by region + + create index idx_mv_cat_region_sum as select category, region, sum(amount) from sales_mat_view group by category, region + + create index idx_mv_min_amount_by_category as select min(amount) from sales_mat_view group by category + create index idx_mv_max_amount_by_region as select max(amount) from sales_mat_view group by region + + create index idx_mv_min_ever_qty_by_category as select min_ever(quantity) from sales_mat_view group by category + create index idx_mv_max_ever_qty_by_region as select max_ever(quantity) from sales_mat_view group by region + + create index idx_mv_cat_max_region as select category, max(amount), region from sales_mat_view group by category, region + create index idx_mv_min_cat_region as select min(amount), category, region from sales_mat_view group by category, region + + create table sales_index_on( + id bigint, + product_id bigint, + category string, + region string, + amount bigint, + quantity bigint, + primary key(id) + ) + + create view v_iot_sum_by_category as select sum(amount) as total_amount, category from sales_index_on group by category + create index idx_iot_sum_by_category on v_iot_sum_by_category(category) include(total_amount) + + create view v_iot_sum_by_region as select sum(amount) as total_amount, region from sales_index_on group by region + create index idx_iot_sum_by_region on v_iot_sum_by_region(region) include(total_amount) + + create view v_iot_sum_by_cat_region as select sum(amount) as total_amount, category, region from sales_index_on group by category, region + create index idx_iot_sum_by_cat_region on v_iot_sum_by_cat_region(category, region) include(total_amount) + + create view v_iot_sum_by_all as select sum(amount) as total_amount, category, region, product_id from sales_index_on group by category, region, product_id + create index idx_iot_sum_by_all on v_iot_sum_by_all(category, region, product_id) include(total_amount) + + create view v_iot_count_by_category as select count(*) as record_count, category from sales_index_on group by category + create index idx_iot_count_by_category on v_iot_count_by_category(category) include(record_count) + + create view v_iot_count_qty_by_region as select count(quantity) as qty_count, region from sales_index_on group by region + create index idx_iot_count_qty_by_region on v_iot_count_qty_by_region(region) include(qty_count) + + create view v_iot_cat_region_sum as select category, region, sum(amount) as total_amount from sales_index_on group by category, region + create index idx_iot_cat_region_sum on v_iot_cat_region_sum(category, region) include(total_amount) + + create view v_iot_min_amount_by_category as select min(amount) as min_amount, category from sales_index_on group by category + create index idx_iot_min_amount_by_category on v_iot_min_amount_by_category(category) include(min_amount) + + create view v_iot_max_amount_by_region as select max(amount) as max_amount, region from sales_index_on group by region + create index idx_iot_max_amount_by_region on v_iot_max_amount_by_region(region) include(max_amount) + + create view v_iot_min_ever_qty_by_category as select min_ever(quantity) as min_qty, category from sales_index_on group by category + create index idx_iot_min_ever_qty_by_category on v_iot_min_ever_qty_by_category(category) include(min_qty) + + create view v_iot_max_ever_qty_by_region as select max_ever(quantity) as max_qty, region from sales_index_on group by region + create index idx_iot_max_ever_qty_by_region on v_iot_max_ever_qty_by_region(region) include(max_qty) + + create view v_iot_cat_max_region as select category, max(amount) as max_amount, region from sales_index_on group by category, region + create index idx_iot_cat_max_region on v_iot_cat_max_region(category, max_amount, region) + + create view v_iot_min_cat_region as select min(amount) as min_amount, category, region from sales_index_on group by category, region + create index idx_iot_min_cat_region on v_iot_min_cat_region(min_amount, category, region) + +--- +setup: + steps: + - query: INSERT INTO sales_mat_view VALUES + (1, 101, 'Electronics', 'North', 1500, 10), + (2, 102, 'Electronics', 'South', 2000, 15), + (3, 103, 'Electronics', 'East', 1200, 8), + (4, 104, 'Electronics', 'North', 1800, 12), + (5, 201, 'Furniture', 'North', 800, 5), + (6, 202, 'Furniture', 'South', 950, 7), + (7, 203, 'Furniture', 'West', 1100, 6), + (8, 301, 'Clothing', 'East', 500, 20), + (9, 302, 'Clothing', 'West', 600, 25), + (10, 303, 'Clothing', 'North', 450, 18) + + - query: INSERT INTO sales_index_on VALUES + (1, 101, 'Electronics', 'North', 1500, 10), + (2, 102, 'Electronics', 'South', 2000, 15), + (3, 103, 'Electronics', 'East', 1200, 8), + (4, 104, 'Electronics', 'North', 1800, 12), + (5, 201, 'Furniture', 'North', 800, 5), + (6, 202, 'Furniture', 'South', 950, 7), + (7, 203, 'Furniture', 'West', 1100, 6), + (8, 301, 'Clothing', 'East', 500, 20), + (9, 302, 'Clothing', 'West', 600, 25), + (10, 303, 'Clothing', 'North', 450, 18) + +--- +test_block: + name: sum_aggregate_single_grouping + tests: + # Test SUM aggregate with single GROUP BY column - category + - + - query: select category, sum(amount) as total from sales_mat_view group by category order by category + - explain: "AISCAN(IDX_MV_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS TOTAL)" + - result: [{ "Clothing", 1550 }, { "Electronics", 6500 }, { "Furniture", 2850 }] + - + - query: select category, sum(amount) as total from sales_index_on group by category order by category + - explain: "AISCAN(IDX_IOT_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS TOTAL)" + - result: [{ "Clothing", 1550 }, { "Electronics", 6500 }, { "Furniture", 2850 }] + + # Test SUM aggregate with single GROUP BY column - region + - + - query: select region, sum(amount) as total from sales_mat_view group by region order by region + - explain: "AISCAN(IDX_MV_SUM_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS TOTAL)" + - result: [{ "East", 1700 }, { "North", 4550 }, { "South", 2950 }, { "West", 1700 }] + - + - query: select region, sum(amount) as total from sales_index_on group by region order by region + - explain: "AISCAN(IDX_IOT_SUM_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS TOTAL)" + - result: [{ "East", 1700 }, { "North", 4550 }, { "South", 2950 }, { "West", 1700 }] + +--- +test_block: + name: sum_aggregate_multi_grouping + tests: + # Test SUM aggregate with multiple GROUP BY columns + - + - query: select category, region, sum(amount) as total from sales_mat_view group by category, region order by category, region + - explain: "AISCAN(IDX_MV_CAT_REGION_SUM <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL)" + - result: [{ "Clothing", "East", 500 }, { "Clothing", "North", 450 }, { "Clothing", "West", 600 }, { "Electronics", "East", 1200 }, { "Electronics", "North", 3300 }, { "Electronics", "South", 2000 }, { "Furniture", "North", 800 }, { "Furniture", "South", 950 }, { "Furniture", "West", 1100 }] + - + - query: select category, region, sum(amount) as total from sales_index_on group by category, region order by category, region + - explain: "AISCAN(IDX_IOT_CAT_REGION_SUM <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL)" + - result: [{ "Clothing", "East", 500 }, { "Clothing", "North", 450 }, { "Clothing", "West", 600 }, { "Electronics", "East", 1200 }, { "Electronics", "North", 3300 }, { "Electronics", "South", 2000 }, { "Furniture", "North", 800 }, { "Furniture", "South", 950 }, { "Furniture", "West", 1100 }] + + # Test filtering on specific category + - + - query: select category, region, sum(amount) as total from sales_mat_view where category = 'Electronics' group by category, region order by region + - explain: "AISCAN(IDX_MV_CAT_REGION_SUM [EQUALS promote(@c17 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL)" + - result: [{ "Electronics", "East", 1200 }, { "Electronics", "North", 3300 }, { "Electronics", "South", 2000 }] + - + - query: select category, region, sum(amount) as total from sales_index_on where category = 'Electronics' group by category, region order by region + - explain: "AISCAN(IDX_IOT_CAT_REGION_SUM [EQUALS promote(@c17 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL)" + - result: [{ "Electronics", "East", 1200 }, { "Electronics", "North", 3300 }, { "Electronics", "South", 2000 }] + +--- +test_block: + name: sum_aggregate_three_grouping + tests: + # Test aggregate with three GROUP BY columns + - + - query: select category, region, product_id, sum(amount) as total from sales_mat_view group by category, region, product_id order by category, region, product_id + - explain: "AISCAN(IDX_MV_SUM_BY_ALL <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: KEY:[2], _3: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS PRODUCT_ID, _._3 AS TOTAL)" + - result: [{ "Clothing", "East", 301, 500 }, { "Clothing", "North", 303, 450 }, { "Clothing", "West", 302, 600 }, { "Electronics", "East", 103, 1200 }, { "Electronics", "North", 101, 1500 }, { "Electronics", "North", 104, 1800 }, { "Electronics", "South", 102, 2000 }, { "Furniture", "North", 201, 800 }, { "Furniture", "South", 202, 950 }, { "Furniture", "West", 203, 1100 }] + - + - query: select category, region, product_id, sum(amount) as total from sales_index_on group by category, region, product_id order by category, region, product_id + - explain: "AISCAN(IDX_IOT_SUM_BY_ALL <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: KEY:[2], _3: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS PRODUCT_ID, _._3 AS TOTAL)" + - result: [{ "Clothing", "East", 301, 500 }, { "Clothing", "North", 303, 450 }, { "Clothing", "West", 302, 600 }, { "Electronics", "East", 103, 1200 }, { "Electronics", "North", 101, 1500 }, { "Electronics", "North", 104, 1800 }, { "Electronics", "South", 102, 2000 }, { "Furniture", "North", 201, 800 }, { "Furniture", "South", 202, 950 }, { "Furniture", "West", 203, 1100 }] + +--- +test_block: + name: count_star_aggregate + tests: + # Test COUNT(*) aggregate + - + - query: select category, count(*) as cnt from sales_mat_view group by category order by category + - explain: "AISCAN(IDX_MV_COUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS CNT)" + - result: [{ "Clothing", 3 }, { "Electronics", 4 }, { "Furniture", 3 }] + - + - query: select category, count(*) as cnt from sales_index_on group by category order by category + - explain: "AISCAN(IDX_IOT_COUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS CNT)" + - result: [{ "Clothing", 3 }, { "Electronics", 4 }, { "Furniture", 3 }] + +--- +test_block: + name: count_column_aggregate + tests: + # Test COUNT(column) aggregate + - + - query: select region, count(quantity) as cnt from sales_mat_view group by region order by region + - explain: "AISCAN(IDX_MV_COUNT_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS CNT)" + - result: [{ "East", 2 }, { "North", 4 }, { "South", 2 }, { "West", 2 }] + - + - query: select region, count(quantity) as cnt from sales_index_on group by region order by region + - explain: "AISCAN(IDX_IOT_COUNT_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS CNT)" + - result: [{ "East", 2 }, { "North", 4 }, { "South", 2 }, { "West", 2 }] + +--- +test_block: + name: aggregate_with_having + tests: + # Test HAVING clause with aggregates + - + - query: select category, sum(amount) as total from sales_mat_view group by category having sum(amount) > 2000 order by category + - explain: "AISCAN(IDX_MV_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | FILTER _._1 GREATER_THAN promote(@c21 AS LONG) | MAP (_._0 AS CATEGORY, _._1 AS TOTAL)" + - result: [{ "Electronics", 6500 }, { "Furniture", 2850 }] + - + - query: select category, sum(amount) as total from sales_index_on group by category having sum(amount) > 2000 order by category + - explain: "AISCAN(IDX_IOT_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | FILTER _._1 GREATER_THAN promote(@c21 AS LONG) | MAP (_._0 AS CATEGORY, _._1 AS TOTAL)" + - result: [{ "Electronics", 6500 }, { "Furniture", 2850 }] + +--- +test_block: + name: min_aggregate + tests: + # Test MIN aggregate + - + - query: select category, min(amount) as min_amt from sales_mat_view group by category order by category + - explain: "AISCAN(IDX_MV_MIN_AMOUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_AMT)" + - result: [{ "Clothing", 450 }, { "Electronics", 1200 }, { "Furniture", 800 }] + - + - query: select category, min(amount) as min_amt from sales_index_on group by category order by category + - explain: "AISCAN(IDX_IOT_MIN_AMOUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_AMT)" + - result: [{ "Clothing", 450 }, { "Electronics", 1200 }, { "Furniture", 800 }] + +--- +test_block: + name: max_aggregate + tests: + # Test MAX aggregate + - + - query: select region, max(amount) as max_amt from sales_mat_view group by region order by region + - explain: "AISCAN(IDX_MV_MAX_AMOUNT_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1]]) | MAP (_._0 AS REGION, _._1 AS MAX_AMT)" + - result: [{ "East", 1200 }, { "North", 1800 }, { "South", 2000 }, { "West", 1100 }] + - + - query: select region, max(amount) as max_amt from sales_index_on group by region order by region + - explain: "AISCAN(IDX_IOT_MAX_AMOUNT_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1]]) | MAP (_._0 AS REGION, _._1 AS MAX_AMT)" + - result: [{ "East", 1200 }, { "North", 1800 }, { "South", 2000 }, { "West", 1100 }] + +--- +test_block: + name: min_ever_aggregate + tests: + # Test MIN_EVER aggregate + - + - query: select category, min_ever(quantity) as min_qty from sales_mat_view group by category order by category + - explain: "AISCAN(IDX_MV_MIN_EVER_QTY_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_QTY)" + - result: [{ "Clothing", 18 }, { "Electronics", 8 }, { "Furniture", 5 }] + - + - query: select category, min_ever(quantity) as min_qty from sales_index_on group by category order by category + - explain: "AISCAN(IDX_IOT_MIN_EVER_QTY_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_QTY)" + - result: [{ "Clothing", 18 }, { "Electronics", 8 }, { "Furniture", 5 }] + +--- +test_block: + name: max_ever_aggregate + tests: + # Test MAX_EVER aggregate + - + - query: select region, max_ever(quantity) as max_qty from sales_mat_view group by region order by region + - explain: "AISCAN(IDX_MV_MAX_EVER_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS MAX_QTY)" + - result: [{ "East", 20 }, { "North", 18 }, { "South", 15 }, { "West", 25 }] + - + - query: select region, max_ever(quantity) as max_qty from sales_index_on group by region order by region + - explain: "AISCAN(IDX_IOT_MAX_EVER_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS MAX_QTY)" + - result: [{ "East", 20 }, { "North", 18 }, { "South", 15 }, { "West", 25 }] + +--- +test_block: + name: aggregate_in_middle_of_key + tests: + # Test aggregate value in the middle of the key - category, max(amount), region + - + - query: select category, max(amount) as max_amt, region from sales_mat_view group by category, region order by category, region + - explain: "AISCAN(IDX_MV_CAT_MAX_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: KEY:[2]]) | MAP (_._0 AS CATEGORY, _._2 AS MAX_AMT, _._1 AS REGION)" + - result: [{ "Clothing", 500, "East" }, { "Clothing", 450, "North" }, { "Clothing", 600, "West" }, { "Electronics", 1200, "East" }, { "Electronics", 1800, "North" }, { "Electronics", 2000, "South" }, { "Furniture", 800, "North" }, { "Furniture", 950, "South" }, { "Furniture", 1100, "West" }] + - + - query: select category, max(amount) as max_amt, region from sales_index_on group by category, region order by category, region + - explain: "ISCAN(IDX_IOT_MIN_CAT_REGION <,>) | MAP (_ AS _0) | AGG (max_l(_._0.AMOUNT) AS _0) GROUP BY (_._0.CATEGORY AS _0, _._0.REGION AS _1) | MAP (_._0._0 AS CATEGORY, _._1._0 AS MAX_AMT, _._0._1 AS REGION)" + - result: [{ "Clothing", 500, "East" }, { "Clothing", 450, "North" }, { "Clothing", 600, "West" }, { "Electronics", 1200, "East" }, { "Electronics", 1800, "North" }, { "Electronics", 2000, "South" }, { "Furniture", 800, "North" }, { "Furniture", 950, "South" }, { "Furniture", 1100, "West" }] + + # Test aggregate value at the beginning of the key - min(amount), category, region + - + - query: select min(amount) as min_amt, category, region from sales_mat_view group by category, region order by category, region + - explain: "AISCAN(IDX_MV_MIN_CAT_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: KEY:[2]]) | MAP (_._2 AS MIN_AMT, _._0 AS CATEGORY, _._1 AS REGION)" + - result: [{ 500, "Clothing", "East" }, { 450, "Clothing", "North" }, { 600, "Clothing", "West" }, { 1200, "Electronics", "East" }, { 1500, "Electronics", "North" }, { 2000, "Electronics", "South" }, { 800, "Furniture", "North" }, { 950, "Furniture", "South" }, { 1100, "Furniture", "West" }] + - + - query: select min(amount) as min_amt, category, region from sales_index_on group by category, region order by category, region + - explain: "ISCAN(IDX_IOT_MIN_CAT_REGION <,>) | MAP (_ AS _0) | AGG (min_l(_._0.AMOUNT) AS _0) GROUP BY (_._0.CATEGORY AS _0, _._0.REGION AS _1) | MAP (_._1._0 AS MIN_AMT, _._0._0 AS CATEGORY, _._0._1 AS REGION)" + - result: [{ 500, "Clothing", "East" }, { 450, "Clothing", "North" }, { 600, "Clothing", "West" }, { 1200, "Electronics", "East" }, { 1500, "Electronics", "North" }, { 2000, "Electronics", "South" }, { 800, "Furniture", "North" }, { 950, "Furniture", "South" }, { 1100, "Furniture", "West" }] + +... + From c1a91de2aa10f7a64505995f52292b08d3625565 Mon Sep 17 00:00:00 2001 From: Youssef Hatem Date: Mon, 1 Dec 2025 11:47:56 +0000 Subject: [PATCH 6/7] add documentation. --- .../sql_commands/DDL/CREATE/INDEX.diagram | 90 ++- .../sql_commands/DDL/CREATE/INDEX.rst | 695 +++++++++++++++++- .../test/java/DocumentationQueriesTests.java | 5 + .../index-documentation-queries.yamsql | 204 +++++ 4 files changed, 981 insertions(+), 13 deletions(-) create mode 100644 yaml-tests/src/test/resources/documentation-queries/index-documentation-queries.yamsql diff --git a/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.diagram b/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.diagram index dfc11e4aa0..ee846041bd 100644 --- a/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.diagram +++ b/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.diagram @@ -1,8 +1,84 @@ Diagram( - Terminal('CREATE'), - Optional('UNIQUE', 'skip'), - Terminal('INDEX'), - NonTerminal('indexName'), - Terminal('AS'), - NonTerminal('query') - ) + Stack( + Sequence( + Terminal('CREATE'), + Choice(0, + Sequence( + Optional('UNIQUE', 'skip') + ), + Sequence( + Terminal('VECTOR') + ) + ), + Terminal('INDEX'), + NonTerminal('indexName') + ), + Choice(0, + Sequence( + Terminal('AS'), + NonTerminal('query') + ), + Stack( + Sequence( + Optional(Sequence( + Terminal('USING'), + Terminal('HNSW') + )), + Terminal('ON'), + NonTerminal('source'), + Terminal('('), + OneOrMore( + Sequence( + NonTerminal('keyColumn') + ), + ',' + ), + Terminal(')') + ), + Optional( + Sequence( + Terminal('INCLUDE'), + Terminal('('), + OneOrMore(NonTerminal('valueColumn'), ','), + Terminal(')') + ) + ), + Optional( + Sequence( + Terminal('PARTITION'), + Terminal('BY'), + Terminal('('), + OneOrMore(NonTerminal('partitionColumn'), ','), + Terminal(')') + ) + ), + Optional( + Sequence( + Terminal('OPTIONS'), + Terminal('('), + Choice(0, + Sequence( + OneOrMore( + Sequence( + NonTerminal('optionName') + ), + ',' + ) + ), + Sequence( + OneOrMore( + Sequence( + NonTerminal('optionName'), + Terminal('='), + NonTerminal('optionValue') + ), + ',' + ) + ) + ), + Terminal(')')) + ) + ) + ) + ) + ).format(paddingBottom=330, paddingRight=40) diff --git a/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.rst b/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.rst index 2e81e2b54a..be3753f71a 100644 --- a/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.rst +++ b/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.rst @@ -2,11 +2,15 @@ CREATE INDEX ============ -Clause in a :ref:`schema template definition ` to create an index. The syntax here takes -a query and is inspired by materialized view syntax. However, unlike a general `VIEW`, the index must be able to -maintained incrementally. That means that for any given record insert, update, or delete, it must be possible -to construct the difference that needs to be applied to each index in order to update it without needing to -completely rebuild it. That means that there are a number of limitations to the `query` argument. See +Clause in a :ref:`schema template definition ` to create an index. The Record Layer supports +two different syntaxes for creating indexes: + +1. **INDEX AS SELECT** - A query-based syntax inspired by materialized views +2. **INDEX ON** - A traditional syntax that creates indexes on views or tables + +Both syntaxes create indexes that are maintained incrementally. For any given record insert, update, or delete, +the system constructs the difference that needs to be applied to each index in order to update it without needing to +completely rebuild it. This means there are limitations to what kinds of indexes can be created. See :ref:`index_definition` for more details. Syntax @@ -15,8 +19,687 @@ Syntax .. raw:: html :file: INDEX.diagram.svg +INDEX AS SELECT Syntax +====================== + +The ``INDEX AS SELECT`` syntax uses a query to define the index structure. This syntax is inspired by materialized +views and allows you to define indexes using familiar SQL SELECT queries with GROUP BY for aggregate indexes. + +Basic Form +---------- + +.. code-block:: sql + + CREATE INDEX indexName AS query + +The ``query`` must be a SELECT statement that returns the columns to be indexed. The index structure is derived +from the query's SELECT list and ORDER BY clause. + +Examples +-------- + +**Simple Value Index** + +.. code-block:: sql + + CREATE INDEX idx_price AS + SELECT price + FROM products + ORDER BY price + +**Covering Value Index** + +.. code-block:: sql + + CREATE INDEX idx_category_price AS + SELECT category, price, name + FROM products + ORDER BY category, price + +This creates an index with ``category`` and ``price`` in the key, and ``name`` as a covered column. + +**Aggregate Index (SUM)** + +.. code-block:: sql + + CREATE INDEX idx_sales_by_category AS + SELECT category, SUM(amount) + FROM sales + GROUP BY category + +**Aggregate Index (COUNT)** + +COUNT(*) counts all records in each group: + +.. code-block:: sql + + CREATE INDEX idx_count_by_region AS + SELECT region, COUNT(*) + FROM sales + GROUP BY region + +COUNT(column) counts non-NULL values: + +.. code-block:: sql + + CREATE INDEX idx_count_non_null AS + SELECT category, COUNT(quantity) + FROM sales + GROUP BY category + +**Aggregate Index (MIN/MAX)** + +MIN and MAX support permuted ordering, where the aggregate value can appear at different positions in the key: + +.. code-block:: sql + + CREATE INDEX idx_max_by_category AS + SELECT category, MAX(amount) + FROM sales + GROUP BY category + ORDER BY category, MAX(amount) + +The aggregate can also appear in the middle of the key: + +.. code-block:: sql + + CREATE INDEX idx_cat_max_region AS + SELECT category, MAX(amount), region + FROM sales + GROUP BY category, region + ORDER BY category, MAX(amount), region + +**Aggregate Index (MIN_EVER/MAX_EVER)** + +MIN_EVER and MAX_EVER track the minimum/maximum value ever seen, even after deletions. These are useful for +maintaining historical extrema: + +.. code-block:: sql + + CREATE INDEX idx_min_ever_by_category AS + SELECT category, MIN_EVER(price) + FROM products + GROUP BY category + + CREATE INDEX idx_max_ever_by_region AS + SELECT region, MAX_EVER(sales_amount) + FROM transactions + GROUP BY region + +For legacy compatibility with older versions, you can use the LEGACY_EXTREMUM_EVER attribute: + +.. code-block:: sql + + CREATE INDEX idx_min_ever_legacy AS + SELECT category, MIN_EVER(price) + FROM products + GROUP BY category + WITH ATTRIBUTES LEGACY_EXTREMUM_EVER + +**Bitmap Aggregate Index** + +Bitmap indexes use specialized functions for efficient set operations and are particularly useful for filtering +on high-cardinality columns: + +.. code-block:: sql + + CREATE INDEX idx_bitmap_by_category AS + SELECT bitmap_construct_agg(bitmap_bit_position(id)) AS bitmap, + category, + bitmap_bucket_offset(id) AS offset + FROM products + GROUP BY category, bitmap_bucket_offset(id) + +For ungrouped bitmap indexes: + +.. code-block:: sql + + CREATE INDEX idx_bitmap_all AS + SELECT bitmap_construct_agg(bitmap_bit_position(id)) AS bitmap, + bitmap_bucket_offset(id) AS offset + FROM products + GROUP BY bitmap_bucket_offset(id) + +**Aggregate with Expressions in GROUP BY** + +You can use expressions in GROUP BY clauses: + +.. code-block:: sql + + CREATE INDEX idx_sum_by_expression AS + SELECT amount + 100, MAX(quantity) + FROM sales + GROUP BY amount + 100 + + CREATE INDEX idx_multi_expression AS + SELECT category_id + region_id, status + 10, MIN(price) + FROM products + GROUP BY category_id + region_id, status + 10 + +**Filtered Index** + +.. code-block:: sql + + CREATE INDEX idx_expensive_products AS + SELECT name, price + FROM products + WHERE price > 100 + ORDER BY price + +**Multiple Grouping Columns** + +.. code-block:: sql + + CREATE INDEX idx_sales_by_category_region AS + SELECT category, region, SUM(amount) + FROM sales + GROUP BY category, region + +**Descending Order** + +.. code-block:: sql + + CREATE INDEX idx_price_desc AS + SELECT price + FROM products + ORDER BY price DESC + +**Mixed Ordering** + +.. code-block:: sql + + CREATE INDEX idx_category_desc_price_asc AS + SELECT category, price + FROM products + ORDER BY category DESC, price ASC + +**NULL Ordering** + +.. code-block:: sql + + CREATE INDEX idx_rating_nulls_last AS + SELECT rating + FROM products + ORDER BY rating ASC NULLS LAST + + CREATE INDEX idx_supplier_desc_nulls_first AS + SELECT supplier + FROM products + ORDER BY supplier DESC NULLS FIRST + +Aggregate Index Capabilities +----------------------------- + +Aggregate indexes support the following aggregate functions: + +**Supported Aggregate Functions** + +- ``SUM(column)`` - Sum of values +- ``COUNT(*)`` - Count of all rows +- ``COUNT(column)`` - Count of non-NULL values +- ``MIN(column)`` - Minimum value (supports permuted ordering) +- ``MAX(column)`` - Maximum value (supports permuted ordering) +- ``MIN_EVER(column)`` - Historical minimum (persists across deletions) +- ``MAX_EVER(column)`` - Historical maximum (persists across deletions) +- ``BITMAP_CONSTRUCT_AGG(bitmap_bit_position(column))`` - Bitmap construction for set operations + +**Permuted Ordering** + +MIN and MAX indexes support permuted ordering, meaning the aggregate value can appear at any position in the +ORDER BY clause: + +.. code-block:: sql + + -- Aggregate at the end (standard) + CREATE INDEX idx1 AS + SELECT category, MAX(amount) + FROM sales + GROUP BY category + ORDER BY category, MAX(amount) + + -- Aggregate in the middle + CREATE INDEX idx2 AS + SELECT category, MAX(amount), region + FROM sales + GROUP BY category, region + ORDER BY category, MAX(amount), region + + -- Aggregate at the beginning + CREATE INDEX idx3 AS + SELECT MIN(amount), category, region + FROM sales + GROUP BY category, region + ORDER BY MIN(amount), category, region + +**Aggregate Index Limitations** + +- Only one aggregate function per index +- Aggregate columns must be integer types (bigint) for most functions (SUM, COUNT, MIN, MAX) +- MIN_EVER and MAX_EVER work with strings and other comparable types +- Expressions in GROUP BY are supported +- WHERE clauses can be used with aggregate indexes for filtered aggregates + +INDEX ON Syntax +=============== + +The ``INDEX ON`` syntax creates an index on an existing view or table using a traditional columnar specification. +This syntax is particularly useful when combined with views that define filtering or aggregation logic. + +Basic Form +---------- + +.. code-block:: sql + + CREATE INDEX indexName ON source(columns) [INCLUDE(valueColumns)] [OPTIONS(...)] + +Where: +- ``source`` is a table or view name +- ``columns`` specifies the index key columns (with optional ordering) +- ``INCLUDE`` clause adds covered columns stored as values +- ``OPTIONS`` clause specifies index-specific options + +Examples +-------- + +**Simple Value Index** + +.. code-block:: sql + + CREATE INDEX idx_price ON products(price) + +**Multi-Column Index** + +.. code-block:: sql + + CREATE INDEX idx_category_price ON products(category, price) + +**Covering Index with INCLUDE** + +.. code-block:: sql + + CREATE INDEX idx_category_price_covering ON products(category, price) + INCLUDE(name, stock) + +This creates an index with ``category`` and ``price`` in the key, and ``name`` and ``stock`` as covered values. + +**Aggregate Index (SUM)** + +First define a view with the aggregation: + +.. code-block:: sql + + CREATE VIEW v_sales_by_category AS + SELECT category, SUM(amount) AS total_amount + FROM sales + GROUP BY category + + CREATE INDEX idx_sales_by_category ON v_sales_by_category(category) + INCLUDE(total_amount) + +**Aggregate Index (COUNT)** + +.. code-block:: sql + + CREATE VIEW v_count_by_region AS + SELECT region, COUNT(*) AS record_count + FROM sales + GROUP BY region + + CREATE INDEX idx_count_by_region ON v_count_by_region(region) + INCLUDE(record_count) + +**Filtered Index** + +First define a view with the filter: + +.. code-block:: sql + + CREATE VIEW v_expensive_products AS + SELECT name, price + FROM products + WHERE price > 100 + + CREATE INDEX idx_expensive_products ON v_expensive_products(price) + +**Custom Ordering** + +.. code-block:: sql + + CREATE INDEX idx_category_desc ON products(category DESC, price ASC) + +**NULL Ordering** + +.. code-block:: sql + + CREATE INDEX idx_rating_nulls_last ON products(rating ASC NULLS LAST) + +Column Ordering and NULL Handling +---------------------------------- + +When creating an index using the ``INDEX ON`` syntax, each key column can specify sorting criteria and null semantics +to control how values are ordered in the index. + +**Sorting Criteria** + +Each key column supports the following sort orders: + +- ``ASC`` (ascending) - Values are sorted from smallest to largest (default if not specified) +- ``DESC`` (descending) - Values are sorted from largest to smallest + +**NULL Semantics** + +You can control where NULL values appear in the sort order: + +- ``NULLS FIRST`` - NULL values appear before non-NULL values +- ``NULLS LAST`` - NULL values appear after non-NULL values + +Default NULL behavior: +- For ``ASC`` ordering: ``NULLS FIRST`` is the default +- For ``DESC`` ordering: ``NULLS LAST`` is the default + +**Syntax Options** + +The ordering clause for each column can take the following forms: + +1. Sort order only: ``columnName ASC`` or ``columnName DESC`` +2. Sort order with null semantics: ``columnName ASC NULLS LAST`` or ``columnName DESC NULLS FIRST`` +3. Null semantics only: ``columnName NULLS FIRST`` or ``columnName NULLS LAST`` (uses default ASC ordering) + +**Examples** + +.. code-block:: sql + + -- Ascending order with nulls last + CREATE INDEX idx_price ON products(price ASC NULLS LAST) + + -- Descending order with nulls first + CREATE INDEX idx_rating ON products(rating DESC NULLS FIRST) + + -- Specify only null semantics (ascending is implicit) + CREATE INDEX idx_stock ON products(stock NULLS LAST) + + -- Mixed ordering across multiple columns + CREATE INDEX idx_complex ON products( + category ASC NULLS FIRST, + price DESC NULLS LAST, + name ASC + ) + +VECTOR INDEX Syntax +=================== + +The ``VECTOR INDEX`` syntax creates an index specifically designed for vector similarity search using the HNSW +(Hierarchical Navigable Small World) algorithm. Vector indexes enable efficient approximate nearest neighbor (ANN) +search on high-dimensional vector data. + +Basic Form +---------- + +.. code-block:: sql + + CREATE VECTOR INDEX indexName USING HNSW ON source(vectorColumn) + [PARTITION BY(partitionColumns)] + [OPTIONS(...)] + +Where: +- ``source`` is a table or view name +- ``vectorColumn`` is a column of type ``vector(dimensions, float)`` +- ``PARTITION BY`` clause specifies partitioning columns (optional but recommended) +- ``OPTIONS`` clause specifies HNSW-specific configuration options + +.. note:: + The ``PARTITION BY`` clause is only applicable to vector indexes. It is not supported for regular value indexes + created with the ``INDEX ON`` syntax. Partitioning helps organize vectors by category or tenant, improving + query performance for vector similarity searches pertaining specific category or tenant. + +Examples +-------- + +**Simple Vector Index** + +.. code-block:: sql + + CREATE TABLE products( + id bigint, + name string, + embedding vector(128, float), + primary key(id) + ) + + CREATE VECTOR INDEX idx_embedding USING HNSW ON products(embedding) + +**Vector Index with Partitioning** + +Partitioning is recommended for better performance and to organize vectors by category or tenant: + +.. code-block:: sql + + CREATE TABLE products( + id bigint, + category string, + embedding vector(256, float), + primary key(id) + ) + + CREATE VECTOR INDEX idx_embedding USING HNSW ON products(embedding) + PARTITION BY(category) + +**Vector Index with Custom Options** + +.. code-block:: sql + + CREATE VECTOR INDEX idx_embedding USING HNSW ON products(embedding) + PARTITION BY(category) + OPTIONS ( + CONNECTIVITY = 16, + M_MAX = 32, + EF_CONSTRUCTION = 200, + METRIC = COSINE_METRIC + ) + +**Vector Index on Filtered View** + +.. code-block:: sql + + CREATE VIEW v_active_products AS + SELECT id, embedding, category + FROM products + WHERE status = 'active' + + CREATE VECTOR INDEX idx_active_embeddings USING HNSW ON v_active_products(embedding) + PARTITION BY(category) + +**Vector Index with RabitQ Quantization** + +RabitQ is a quantization technique that reduces memory usage for high-dimensional vectors: + +.. code-block:: sql + + CREATE VECTOR INDEX idx_embedding USING HNSW ON products(embedding) + PARTITION BY(category) + OPTIONS ( + USE_RABITQ = true, + RABITQ_NUM_EX_BITS = 4, + MAINTAIN_STATS_PROBABILITY = 0.01 + ) + +**Vector Index with Statistics Sampling** + +.. code-block:: sql + + CREATE VECTOR INDEX idx_embedding USING HNSW ON products(embedding) + PARTITION BY(category) + OPTIONS ( + SAMPLE_VECTOR_STATS_PROBABILITY = 0.05 + ) + +Vector Index Options +-------------------- + +HNSW Algorithm Parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``CONNECTIVITY`` (or ``M``) + The number of bi-directional links created for each node during construction. Higher values improve recall + but increase memory usage and construction time. Default: 16. + +``M_MAX`` + Maximum number of connections per layer. Default: derived from CONNECTIVITY. + +``EF_CONSTRUCTION`` + The size of the dynamic candidate list during index construction. Higher values improve index quality + but increase construction time. Default: 200. + +Distance Metrics +~~~~~~~~~~~~~~~~ + +``METRIC`` + The distance metric used for similarity search. Available options: + + - ``EUCLIDEAN_METRIC`` - L2 distance (default) + - ``MANHATTAN_METRIC`` - L1 distance + - ``DOT_PRODUCT_METRIC`` - Dot product (for normalized vectors) + - ``EUCLIDEAN_SQUARE_METRIC`` - Squared L2 distance + - ``COSINE_METRIC`` - Cosine similarity (recommended for embeddings) + +RabitQ Quantization Options +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``USE_RABITQ`` + Enable RabitQ quantization to reduce memory usage. Default: false. + +``RABITQ_NUM_EX_BITS`` + Number of extra bits for RabitQ quantization. Higher values improve accuracy but increase memory usage. + Valid range: 0-8. Default: 4. + +Statistics and Monitoring +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``MAINTAIN_STATS_PROBABILITY`` + Probability of maintaining statistics during updates when using RabitQ. Default: 0.01. + +``SAMPLE_VECTOR_STATS_PROBABILITY`` + Probability of sampling vectors for statistics collection. Default: 0.0 (disabled). + +Vector Index Limitations +------------------------- + +- Only one vector column can be indexed per vector index +- The indexed column must be of type ``vector(dimensions, float)`` +- Vector dimensions must be specified at table creation time +- INCLUDE clause is not supported for vector indexes +- Partitioning improves performance but is optional + +Comparing Both Syntaxes +======================== + +The two syntaxes are functionally equivalent and produce identical index structures. The choice between them +is primarily a matter of style and organizational preference. + +Value Index Comparison +----------------------- + +These two approaches create identical indexes: + +**INDEX AS SELECT:** + +.. code-block:: sql + + CREATE INDEX idx_category_price AS + SELECT category, price, name + FROM products + ORDER BY category, price + +**INDEX ON:** + +.. code-block:: sql + + CREATE INDEX idx_category_price ON products(category, price) + INCLUDE(name) + +Aggregate Index Comparison +--------------------------- + +These two approaches create identical aggregate indexes: + +**INDEX AS SELECT:** + +.. code-block:: sql + + CREATE INDEX idx_sales_by_category AS + SELECT category, SUM(amount) + FROM sales + GROUP BY category + +**INDEX ON:** + +.. code-block:: sql + + CREATE VIEW v_sales_by_category AS + SELECT category, SUM(amount) AS total_amount + FROM sales + GROUP BY category + + CREATE INDEX idx_sales_by_category ON v_sales_by_category(category) + INCLUDE(total_amount) + Parameters ========== +Common Parameters +----------------- + ``indexName`` - The name of the index. Note that the name of the index should be unique in the schema template. + The name of the index. Must be unique within the schema template. + +``UNIQUE`` (optional) + Specifies that the index should enforce uniqueness constraints. + +INDEX AS SELECT Parameters +--------------------------- + +``query`` + A SELECT statement that defines the index structure. The query must be incrementally maintainable. + + - For value indexes: Must include an ORDER BY clause specifying the key columns + - For aggregate indexes: Must include GROUP BY with a single aggregate function + - May include WHERE clause for filtered indexes + +INDEX ON Parameters +------------------- + +``source`` + The name of the table or view to index. + +``columns`` + A comma-separated list of column names that form the index key. Each column can optionally specify: + + - Sort order: ``ASC`` (default) or ``DESC`` + - NULL handling: ``NULLS FIRST`` or ``NULLS LAST`` + + Example: ``category DESC, price ASC NULLS LAST`` + +``INCLUDE(valueColumns)`` (optional) + A comma-separated list of additional columns to store in the index as values (not part of the key). + This creates a covering index that can satisfy queries without accessing the base table. + +``OPTIONS(...)`` (optional) + Index-specific configuration options. Available options depend on the index type. + +Limitations +=========== + +Both syntaxes share the same limitations because they use the same underlying index implementation: + +- Indexes must be incrementally maintainable +- Aggregate indexes support only one aggregate function per index +- Aggregate indexes require integer types for the aggregated column +- The query structure must allow computing index updates from individual record changes +- See :ref:`index_definition` for detailed limitations + +See Also +======== + +- :ref:`create-schema-template` - Schema template definition +- :ref:`index_definition` - Detailed index definition and limitations +- :ref:`CREATE VIEW ` - Creating views for use with INDEX ON diff --git a/yaml-tests/src/test/java/DocumentationQueriesTests.java b/yaml-tests/src/test/java/DocumentationQueriesTests.java index 9ebdef72fc..b9376db885 100644 --- a/yaml-tests/src/test/java/DocumentationQueriesTests.java +++ b/yaml-tests/src/test/java/DocumentationQueriesTests.java @@ -49,6 +49,11 @@ void inOperatorQueriesTests(YamlTest.Runner runner) throws Exception { runner.runYamsql(PREFIX + "/in-operator-queries.yamsql"); } + @TestTemplate + void indexDocumentationQueriesTests(YamlTest.Runner runner) throws Exception { + runner.runYamsql(PREFIX + "/index-documentation-queries.yamsql"); + } + @TestTemplate void isDistinctFromOperatorQueriesTests(YamlTest.Runner runner) throws Exception { runner.runYamsql(PREFIX + "/is-distinct-from-operator-queries.yamsql"); diff --git a/yaml-tests/src/test/resources/documentation-queries/index-documentation-queries.yamsql b/yaml-tests/src/test/resources/documentation-queries/index-documentation-queries.yamsql new file mode 100644 index 0000000000..af5acd6433 --- /dev/null +++ b/yaml-tests/src/test/resources/documentation-queries/index-documentation-queries.yamsql @@ -0,0 +1,204 @@ +--- +options: + supported_version: !current_version +--- +schema_template: + create table products(id bigint, name string, category string, price bigint, rating bigint, supplier string, stock bigint, primary key(id)) + create table sales(id bigint, category string, region string, amount bigint, quantity bigint, primary key(id)) + create index idx_price as select price from products order by price + create index idx_category_price as select category, price, name from products order by category, price + create index idx_sales_by_category as select category, sum(amount) from sales group by category + create index idx_count_by_region as select region, count(*) from sales group by region + create index idx_max_by_category as select category, max(amount) from sales group by category order by category, max(amount) + create index idx_price_desc as select price from products order by price desc + create index idx_category_desc_price_asc as select category, price from products order by category desc, price asc + create index idx_rating_nulls_last as select rating from products order by rating asc nulls last + create index idx_on_price on products(price) + create index idx_on_category_price on products(category, price) + create index idx_on_category_price_covering on products(category, price) include(name, stock) + create index idx_on_category_desc on products(category desc, price asc) + create index idx_on_rating_nulls_last on products(rating asc nulls last) + create index idx_on_supplier_desc_nulls_first on products(supplier desc nulls first) + create index idx_on_stock_nulls_last on products(stock nulls last) + create index idx_on_complex on products(category asc nulls first, price desc nulls last, name asc) +--- +setup: + steps: + # Setup products data + - query: insert into products + values (1, 'Widget A', 'Electronics', 100, 5, 'SupplierA', 50), + (2, 'Widget B', 'Electronics', 150, 4, 'SupplierB', 30), + (3, 'Gadget X', 'Electronics', 200, NULL, 'SupplierA', 20), + (4, 'Tool A', 'Hardware', 80, 3, NULL, 100), + (5, 'Tool B', 'Hardware', 120, NULL, 'SupplierC', NULL) + + # Setup sales data + - query: insert into sales + values (1, 'Electronics', 'North', 500, 10), + (2, 'Electronics', 'South', 300, 5), + (3, 'Hardware', 'North', 200, 3), + (4, 'Electronics', 'North', 700, 15), + (5, 'Hardware', 'South', 400, 8) +--- +test_block: + name: index-documentation-tests + preset: single_repetition_ordered + tests: + # INDEX AS SELECT - Simple Value Index + - + - query: SELECT price + FROM products + ORDER BY price + - result: [{price: 80}, + {price: 100}, + {price: 120}, + {price: 150}, + {price: 200}] + + # INDEX AS SELECT - Covering Value Index + - + - query: SELECT category, price, name + FROM products + ORDER BY category, price + - result: [{category: "Electronics", price: 100, name: "Widget A"}, + {category: "Electronics", price: 150, name: "Widget B"}, + {category: "Electronics", price: 200, name: "Gadget X"}, + {category: "Hardware", price: 80, name: "Tool A"}, + {category: "Hardware", price: 120, name: "Tool B"}] + + # INDEX AS SELECT - Aggregate Index (SUM) + - + - query: SELECT category, SUM(amount) as total + FROM sales + GROUP BY category + ORDER BY category + - result: [{category: "Electronics", total: 1500}, + {category: "Hardware", total: 600}] + + # INDEX AS SELECT - Aggregate Index (COUNT) + - + - query: SELECT region, COUNT(*) as cnt + FROM sales + GROUP BY region + ORDER BY region + - result: [{region: "North", cnt: 3}, + {region: "South", cnt: 2}] + + # INDEX AS SELECT - Aggregate Index (MAX) + - + - query: SELECT category, MAX(amount) as max_amt + FROM sales + GROUP BY category + ORDER BY category, MAX(amount) + - result: [{category: "Electronics", max_amt: 700}, + {category: "Hardware", max_amt: 400}] + + # INDEX AS SELECT - Descending Order + - + - query: SELECT price + FROM products + ORDER BY price DESC + - result: [{price: 200}, + {price: 150}, + {price: 120}, + {price: 100}, + {price: 80}] + + # INDEX AS SELECT - Mixed Ordering + - + - query: SELECT category, price + FROM products + ORDER BY category DESC, price ASC + - result: [{category: "Hardware", price: 80}, + {category: "Hardware", price: 120}, + {category: "Electronics", price: 100}, + {category: "Electronics", price: 150}, + {category: "Electronics", price: 200}] + + # INDEX ON - Simple Value Index + - + - query: SELECT price + FROM products + ORDER BY price + - result: [{price: 80}, + {price: 100}, + {price: 120}, + {price: 150}, + {price: 200}] + + # INDEX ON - Multi-Column Index + - + - query: SELECT category, price + FROM products + ORDER BY category, price + - result: [{category: "Electronics", price: 100}, + {category: "Electronics", price: 150}, + {category: "Electronics", price: 200}, + {category: "Hardware", price: 80}, + {category: "Hardware", price: 120}] + + # INDEX ON - Covering Index with INCLUDE + - + - query: SELECT category, price, name, stock + FROM products + ORDER BY category, price + - result: [{category: "Electronics", price: 100, name: "Widget A", stock: 50}, + {category: "Electronics", price: 150, name: "Widget B", stock: 30}, + {category: "Electronics", price: 200, name: "Gadget X", stock: 20}, + {category: "Hardware", price: 80, name: "Tool A", stock: 100}, + {category: "Hardware", price: 120, name: "Tool B", stock: !null _}] + + # INDEX ON - Custom Ordering (DESC/ASC) + - + - query: SELECT category, price + FROM products + ORDER BY category DESC, price ASC + - result: [{category: "Hardware", price: 80}, + {category: "Hardware", price: 120}, + {category: "Electronics", price: 100}, + {category: "Electronics", price: 150}, + {category: "Electronics", price: 200}] + + # INDEX ON - NULL Ordering (ASC NULLS LAST) + - + - query: SELECT rating + FROM products + ORDER BY rating ASC NULLS LAST + - result: [{rating: 3}, + {rating: 4}, + {rating: 5}, + {rating: !null _}, + {rating: !null _}] + + # INDEX ON - NULL Ordering (DESC NULLS FIRST) + - + - query: SELECT supplier + FROM products + ORDER BY supplier DESC NULLS FIRST + - result: [{supplier: !null _}, + {supplier: "SupplierC"}, + {supplier: "SupplierB"}, + {supplier: "SupplierA"}, + {supplier: "SupplierA"}] + + # INDEX ON - NULL Semantics Only (implicit ASC) + - + - query: SELECT stock + FROM products + ORDER BY stock NULLS LAST + - result: [{stock: 20}, + {stock: 30}, + {stock: 50}, + {stock: 100}, + {stock: !null _}] + + # INDEX ON - Mixed Ordering Across Multiple Columns + - + - query: SELECT category, price, name + FROM products + ORDER BY category ASC NULLS FIRST, price DESC NULLS LAST, name ASC + - result: [{category: "Electronics", price: 200, name: "Gadget X"}, + {category: "Electronics", price: 150, name: "Widget B"}, + {category: "Electronics", price: 100, name: "Widget A"}, + {category: "Hardware", price: 120, name: "Tool B"}, + {category: "Hardware", price: 80, name: "Tool A"}] From 79f625597d971833531bfd02d4e1e277bebd8f28 Mon Sep 17 00:00:00 2001 From: Youssef Hatem Date: Tue, 2 Dec 2025 15:24:03 +0000 Subject: [PATCH 7/7] expand index documentation. --- docs/sphinx/source/reference/Indexes.rst | 151 ++++++++++++++++++ .../sql_commands/DDL/CREATE/INDEX.rst | 1 + 2 files changed, 152 insertions(+) diff --git a/docs/sphinx/source/reference/Indexes.rst b/docs/sphinx/source/reference/Indexes.rst index cd23d57ed0..6033e76537 100644 --- a/docs/sphinx/source/reference/Indexes.rst +++ b/docs/sphinx/source/reference/Indexes.rst @@ -64,6 +64,152 @@ In each case, the fact that index entries are ordered by `fname` is leveraged, e the scan to a smaller range of index entries. The `lname` field can then be returned to the user without having to perform an additional lookup of the underlying row. +Index Syntax Alternatives +########################## + +The Relational Layer supports two equivalent syntaxes for creating indexes: + +1. **INDEX AS SELECT** - A query-based syntax (shown above) inspired by materialized views +2. **INDEX ON** - A traditional columnar syntax that creates indexes on tables or views + +Both syntaxes produce identical index structures and have the same capabilities. The choice between them is primarily +a matter of style and organizational preference. + +INDEX ON Syntax +*************** + +The ``INDEX ON`` syntax provides a more traditional approach to index creation, specifying columns directly rather +than through a SELECT query: + +.. code-block:: sql + + CREATE INDEX ON () [INCLUDE()] [OPTIONS(...)] + +Where: + +* :sql:`indexName` is the name of the index +* :sql:`source` is a table or view name +* :sql:`columns` specifies the index key columns with optional ordering +* :sql:`INCLUDE` clause (optional) adds covered columns stored as values +* :sql:`OPTIONS` clause (optional) specifies index-specific options + +Using the same employee table from above, we can create an equivalent index using the INDEX ON syntax: + +.. code-block:: sql + + CREATE INDEX fnameIdx ON employee(fname) INCLUDE(lname) + +This creates the same index structure as the INDEX AS SELECT example - a VALUE index with ``fname`` in the key +and ``lname`` as a covered value. + +Syntax Comparison +***************** + +These two approaches create identical indexes: + +**INDEX AS SELECT:** + +.. code-block:: sql + + CREATE INDEX fnameIdx AS + SELECT fname, lname + FROM employee + ORDER BY fname + +**INDEX ON:** + +.. code-block:: sql + + CREATE INDEX fnameIdx ON employee(fname) INCLUDE(lname) + +Column Ordering and NULL Handling +################################## + +When creating indexes using either syntax, you can control how values are sorted in the index through ordering +clauses and NULL semantics. + +Sorting Criteria +**************** + +Each key column in an INDEX ON definition supports explicit sort order: + +* :sql:`ASC` (ascending) - Values sorted from smallest to largest (default if not specified) +* :sql:`DESC` (descending) - Values sorted from largest to smallest + +For INDEX AS SELECT, the sort order is specified in the ORDER BY clause. + +NULL Semantics +************** + +You can control where NULL values appear in the sort order: + +* :sql:`NULLS FIRST` - NULL values appear before non-NULL values +* :sql:`NULLS LAST` - NULL values appear after non-NULL values + +**Default NULL behavior:** + +* For ``ASC`` ordering: ``NULLS FIRST`` is the default +* For ``DESC`` ordering: ``NULLS LAST`` is the default + +Ordering Syntax Examples +************************* + +The ordering clause for each column in INDEX ON can take several forms: + +1. Sort order only: ``columnName ASC`` or ``columnName DESC`` +2. Sort order with null semantics: ``columnName ASC NULLS LAST`` or ``columnName DESC NULLS FIRST`` +3. Null semantics only: ``columnName NULLS FIRST`` (uses default ASC ordering) + +Examples: + +.. code-block:: sql + + -- Ascending order with nulls last + CREATE INDEX idx_rating ON products(rating ASC NULLS LAST) + + -- Descending order with nulls first + CREATE INDEX idx_price ON products(price DESC NULLS FIRST) + + -- Specify only null semantics (ascending is implicit) + CREATE INDEX idx_stock ON products(stock NULLS LAST) + + -- Mixed ordering across multiple columns + CREATE INDEX idx_complex ON products( + category ASC NULLS FIRST, + price DESC NULLS LAST, + name ASC + ) + +For INDEX AS SELECT syntax, the same ordering is specified in the ORDER BY clause: + +.. code-block:: sql + + CREATE INDEX idx_rating AS + SELECT rating + FROM products + ORDER BY rating ASC NULLS LAST + +Partitioning for Vector Indexes +################################ + +Vector indexes support an optional ``PARTITION BY`` clause that allows organizing vectors by category or tenant. +This clause is **only applicable to vector indexes** created with the ``VECTOR INDEX`` syntax and is not supported +for regular value indexes. + +Partitioning helps improve query performance for vector similarity searches by limiting the search space to relevant +partitions: + +.. code-block:: sql + + CREATE VECTOR INDEX idx_embedding USING HNSW ON products(embedding) + PARTITION BY(category) + +In this example, vectors are partitioned by product category, so similarity searches can be scoped to specific +categories for better performance. + +**Important:** The ``PARTITION BY`` clause cannot be used with regular (non-vector) indexes created using either +the INDEX AS SELECT or INDEX ON syntax. + Indexes on nested fields ######################## @@ -128,3 +274,8 @@ that resembles the structure of the SQL statement: * Projected fields :sql:`f1`, :sql:`f2`, ... :sql:`fn` in (sub)queries maps to a :sql:`concat(field(f1), field(f2), ... field(fn))`. * Projected nested fields (:sql:`f1`, :sql:`f2`, ... :sql:`fn`) from a repeated field :sql:`rf`, i.e. :sql:`select f1, f2, ... fn, ... from FOO.rf` maps to :sql:`field(rf, FAN_OUT).nest(field(f1), (field(f2), ..., field(fn)))`. + +See Also +######## + +* :doc:`CREATE INDEX ` - Complete CREATE INDEX command reference with detailed syntax and examples diff --git a/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.rst b/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.rst index be3753f71a..a54d314358 100644 --- a/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.rst +++ b/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.rst @@ -530,6 +530,7 @@ RabitQ is a quantization technique that reduces memory usage for high-dimensiona CREATE VECTOR INDEX idx_embedding USING HNSW ON products(embedding) PARTITION BY(category) OPTIONS ( + USE_RABITQ = true, SAMPLE_VECTOR_STATS_PROBABILITY = 0.05 )