From da83c8ef3988d0559af46591347fc992565393b3 Mon Sep 17 00:00:00 2001 From: Pranjal Gupta Date: Wed, 29 Oct 2025 14:25:30 +0000 Subject: [PATCH] Support classic index definitions --- .../plan/IndexKeyValueToPartialRecord.java | 3 + .../EmbeddedRelationalBenchmark.java | 4 +- .../src/main/antlr/RelationalLexer.g4 | 1 + .../src/main/antlr/RelationalParser.g4 | 29 +- .../AbstractEmbeddedStatement.java | 11 +- .../recordlayer/query/ParseHelpers.java | 16 +- .../query/visitors/BaseVisitor.java | 33 ++- .../query/visitors/DdlVisitor.java | 125 +++++++- .../query/visitors/DelegatingVisitor.java | 45 ++- .../query/visitors/ExpressionVisitor.java | 4 +- .../query/visitors/QueryVisitor.java | 4 +- .../query/visitors/TypedVisitor.java | 18 +- .../recordlayer/util/ExceptionUtil.java | 3 +- .../api/ddl/DdlStatementParsingTest.java | 57 ++-- .../relational/api/ddl/IndexTest.java | 177 +++++++++--- .../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 + .../alternate-index-syntax.metrics.binpb | 266 +++++++++++++++++ .../alternate-index-syntax.metrics.yaml | 270 ++++++++++++++++++ .../resources/alternate-index-syntax.yamsql | 245 ++++++++++++++++ 29 files changed, 1267 insertions(+), 132 deletions(-) create mode 100644 yaml-tests/src/test/resources/alternate-index-syntax.metrics.binpb create mode 100644 yaml-tests/src/test/resources/alternate-index-syntax.metrics.yaml create mode 100644 yaml-tests/src/test/resources/alternate-index-syntax.yamsql 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-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 bbd31ecf1a..cd71dd13fd 100644 --- a/fdb-relational-core/src/main/antlr/RelationalLexer.g4 +++ b/fdb-relational-core/src/main/antlr/RelationalLexer.g4 @@ -224,6 +224,7 @@ USAGE: 'USAGE'; USE: 'USE'; USING: 'USING'; VALUES: 'VALUES'; +VECTOR: 'VECTOR'; WHEN: 'WHEN'; WHERE: 'WHERE'; WHILE: 'WHILE'; diff --git a/fdb-relational-core/src/main/antlr/RelationalParser.g4 b/fdb-relational-core/src/main/antlr/RelationalParser.g4 index 9cc2bb001a..63884cabaa 100644 --- a/fdb-relational-core/src/main/antlr/RelationalParser.g4 +++ b/fdb-relational-core/src/main/antlr/RelationalParser.g4 @@ -158,7 +158,24 @@ enumDefinition ; indexDefinition - : (UNIQUE)? INDEX indexName=uid AS queryTerm indexAttributes? + : (UNIQUE)? INDEX indexName=uid AS queryTerm indexAttributes? #indexAsSelectDefinition + | (UNIQUE)? INDEX indexName=uid ON tableName indexColumnList includeClause? partitionClause? indexAttributes? #indexOnSourceDefinition + ; + +indexColumnList + : '(' indexColumnSpec (',' indexColumnSpec)* ')' + ; + +indexColumnSpec + : columnName=uid orderClause? + ; + +includeClause + : INCLUDE '(' uidList ')' + ; + +indexType + : UNIQUE | VECTOR ; indexAttributes @@ -406,7 +423,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 @@ -1107,10 +1129,11 @@ frameRange | expression (PRECEDING | FOLLOWING) ; +*/ + partitionClause : PARTITION BY expression (',' expression)* ; -*/ 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/query/ParseHelpers.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ParseHelpers.java index 086d17bbc2..ee0417a815 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 @@ -39,6 +39,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; @@ -159,13 +160,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/visitors/BaseVisitor.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/BaseVisitor.java index 099a909550..f23605899e 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 @@ -403,8 +403,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 @@ -1696,4 +1720,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 b77c03d907..1254af97ea 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,9 @@ package com.apple.foundationdb.relational.recordlayer.query.visitors; import com.apple.foundationdb.annotation.API; +import com.apple.foundationdb.record.metadata.Key; +import com.apple.foundationdb.record.metadata.IndexTypes; +import com.apple.foundationdb.record.metadata.expressions.KeyExpression; import com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalSortExpression; import com.apple.foundationdb.record.query.plan.cascades.values.PromoteValue; import com.apple.foundationdb.record.query.plan.cascades.values.ThrowsValue; @@ -30,7 +33,6 @@ import com.apple.foundationdb.relational.api.metadata.DataType; import com.apple.foundationdb.relational.api.metadata.InvokedRoutine; import com.apple.foundationdb.relational.generated.RelationalParser; -import com.apple.foundationdb.relational.recordlayer.metadata.DataTypeUtils; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerColumn; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerIndex; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerInvokedRoutine; @@ -42,11 +44,13 @@ import com.apple.foundationdb.relational.recordlayer.query.Identifier; import com.apple.foundationdb.relational.recordlayer.query.IndexGenerator; import com.apple.foundationdb.relational.recordlayer.query.LogicalOperator; +import com.apple.foundationdb.relational.recordlayer.query.ParseHelpers; import com.apple.foundationdb.relational.recordlayer.query.PreparedParams; import com.apple.foundationdb.relational.recordlayer.query.ProceduralPlan; import com.apple.foundationdb.relational.recordlayer.query.QueryParser; import com.apple.foundationdb.relational.recordlayer.query.SemanticAnalyzer; import com.apple.foundationdb.relational.recordlayer.query.functions.CompiledSqlFunction; +import com.apple.foundationdb.relational.recordlayer.metadata.DataTypeUtils; import com.apple.foundationdb.relational.util.Assert; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -184,7 +188,7 @@ public RecordLayerTable visitStructDefinition(@Nonnull RelationalParser.StructDe @Nonnull @Override - public RecordLayerIndex visitIndexDefinition(@Nonnull RelationalParser.IndexDefinitionContext ctx) { + public RecordLayerIndex visitIndexAsSelectDefinition(@Nonnull RelationalParser.IndexAsSelectDefinitionContext ctx) { final var indexId = visitUid(ctx.indexName); final var ddlCatalog = metadataBuilder.build(); @@ -201,6 +205,121 @@ public RecordLayerIndex visitIndexDefinition(@Nonnull RelationalParser.IndexDefi return generator.generate(indexId.getName(), isUnique, table.getType(), containsNullableArray); } + @Nonnull + @Override + public RecordLayerIndex visitIndexOnSourceDefinition(@Nonnull RelationalParser.IndexOnSourceDefinitionContext ctx) { + final var indexId = visitUid(ctx.indexName); + final var tableName = visitUid(ctx.tableName().fullId().uid(0)); + final var isUnique = ctx.UNIQUE() != null; + + // Build KeyExpression directly from DDL syntax - no SQL generation needed + final var keyExpression = buildKeyExpressionFromDdl(ctx); + + return RecordLayerIndex.newBuilder() + .setName(indexId.getName()) + .setTableName(tableName.getName()) + .setIndexType(IndexTypes.VALUE) + .setKeyExpression(keyExpression) + .setUnique(isUnique) + .build(); + } + + private KeyExpression buildKeyExpressionFromDdl(@Nonnull RelationalParser.IndexOnSourceDefinitionContext ctx) { + final var columnSpecs = ctx.indexColumnList().indexColumnSpec(); + final List indexedExpressions = new ArrayList<>(); + final List includeColumns = new ArrayList<>(); + + // Extract indexed columns with ASC/DESC and NULLS FIRST/LAST ordering + for (final var spec : columnSpecs) { + final var columnName = visitUid(spec.columnName).getName(); + final var fieldExpression = Key.Expressions.field(columnName); + + // Check if orderClause is specified + if (spec.orderClause() != null) { + final boolean isDesc = ParseHelpers.isDescending(spec.orderClause()); + final boolean nullsLast = ParseHelpers.isNullsLast(spec.orderClause(), isDesc); + + if (isDesc) { + if (nullsLast) { + indexedExpressions.add(Key.Expressions.function("order_desc_nulls_last", fieldExpression)); + } else { + indexedExpressions.add(Key.Expressions.function("order_desc_nulls_first", fieldExpression)); + } + } else { + if (nullsLast) { + indexedExpressions.add(Key.Expressions.function("order_asc_nulls_last", fieldExpression)); + } else { + indexedExpressions.add(fieldExpression); + } + } + } else { + // No ordering specified - ASC NULLS FIRST (default) + indexedExpressions.add(fieldExpression); + } + } + + // Extract INCLUDE columns + if (ctx.includeClause() != null) { + final var includeColumnCtxs = ctx.includeClause().uidList().uid(); + for (final var includeColumnCtx : includeColumnCtxs) { + final var columnName = visitUid(includeColumnCtx).getName(); + includeColumns.add(columnName); + } + } + + // Combine indexed expressions + include columns for KeyExpression + final List allExpressions = new ArrayList<>(indexedExpressions); + for (final var includeColumn : includeColumns) { + allExpressions.add(Key.Expressions.field(includeColumn)); + } + + if (allExpressions.size() == 1) { + return allExpressions.get(0); + } else { + if (!includeColumns.isEmpty()) { + return Key.Expressions.keyWithValue(Key.Expressions.concat(allExpressions), indexedExpressions.size()); + } else { + return Key.Expressions.concat(allExpressions); + } + } + } + +// public RecordLayerIndex visitDeclarativeIndexDefinition(@Nonnull RelationalParser.DeclarativeIndexDefinitionContext ctx) { +// final var indexId = visitUid(ctx.indexHeader().indexName); +// final var sourceId = visitUid(ctx.indexHeader().source); +// final var indexType = ctx.indexType() == null ? Optional.empty() : Optional.of(ctx.indexType().getText()); +// +// final var ddlCatalog = metadataBuilder.build(); +// // parse the index SQL query using the newly constructed metadata. +// getDelegate().replaceSchemaTemplate(ddlCatalog); +// +// +// LogicalOperator operator; +// if (getDelegate().getSemanticAnalyzer().viewExists(sourceId)) { +// operator = getDelegate().getSemanticAnalyzer().resolveView(sourceId); +// } else if (getDelegate().getSemanticAnalyzer().tableExists(sourceId)) { +// return LogicalOperator.generateAccess(identifier, alias, requestedIndexes, semanticAnalyzer); +// } else if (getDelegate().getSemanticAnalyzer().resolveView() !+ ) { +// +// } +// +// getDelegate().pushPlanFragment(); +// final var operator = LogicalOperator.generateAccess(sourceId, Optional.empty(), Set.of(), getDelegate().getSemanticAnalyzer(), +// getDelegate().getCurrentPlanFragment(), getDelegate().getLogicalOperatorCatalog()); +// +// +// final var result = LogicalOperator.generateSelect(getDelegate().getSemanticAnalyzer().expandStar(), getDelegate().getLogicalOperators(), where, orderBys, +// Optional.empty(), outerCorrelations, getDelegate().isTopLevel(), getDelegate().isForDdl()); +// +// +// final var plan = operator.getQuantifier().getRangesOver().get(); +// +// getDelegate().popPlanFragment(); +// +// +// return null; +// } + @Nonnull @Override public DataType.Named visitEnumDefinition(@Nonnull RelationalParser.EnumDefinitionContext ctx) { @@ -257,7 +376,7 @@ 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()); + final var indexes = indexClauses.build().stream().map(clause -> Assert.castUnchecked(visit(clause), RecordLayerIndex.class)).collect(ImmutableList.toImmutableList()); // TODO: this is currently relying on the lexical order of the functions and views to resolve dependencies which is limited. // https://github.com/FoundationDB/fdb-record-layer/issues/3493 functionClauses.build().forEach(functionClause -> { 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 70d159264a..75085f32d0 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 @@ -230,10 +230,9 @@ 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); } @Nonnull @@ -535,6 +534,36 @@ 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); + } + + @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) { @@ -1351,6 +1380,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) { @@ -1559,6 +1593,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 f5fbc8dca0..359cb52f0b 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/ExpressionVisitor.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/ExpressionVisitor.java @@ -199,8 +199,8 @@ public List visitOrderByClause(@Nonnull RelationalParser.Orde @Override public OrderByExpression visitOrderByExpression(@Nonnull RelationalParser.OrderByExpressionContext orderByExpressionContext) { final var expression = Assert.castUnchecked(orderByExpressionContext.expression().accept(this), Expression.class); - final var descending = ParseHelpers.isDescending(orderByExpressionContext); - final var nullsLast = ParseHelpers.isNullsLast(orderByExpressionContext, descending); + final var descending = ParseHelpers.isDescending(orderByExpressionContext.orderClause()); + final var nullsLast = ParseHelpers.isNullsLast(orderByExpressionContext.orderClause(), descending); return OrderByExpression.of(expression, descending, nullsLast); } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java index 9d7a45dd49..bc76e9404b 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java @@ -595,8 +595,8 @@ public List visitOrderByClauseForSelect(@Nonnull RelationalPa final var matchingExpressionMaybe = isAliasMaybe.flatMap(alias -> semanticAnalyzer.lookupAlias(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 7f15686ea8..06cfd31ec2 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 @@ -168,7 +168,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 a56aa803a2..7d250b1633 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; @@ -67,7 +68,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 d1f1c8263e..c4770e5b1c 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 @@ -24,6 +24,7 @@ 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.IndexTest.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; @@ -50,6 +51,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; @@ -59,6 +61,7 @@ import java.net.URI; 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; @@ -72,6 +75,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(); @@ -100,7 +104,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(IndexTest.IndexSyntax.values()) + .map(syntax -> Arguments.of(syntax, permutation))); } void shouldFailWith(@Nonnull final String query, @Nullable final ErrorCode errorCode) throws Exception { @@ -164,25 +170,28 @@ private static DescriptorProtos.FileDescriptorProto getProtoDescriptor(@Nonnull return asRecordLayerSchemaTemplate.toRecordMetadata().toProto().getRecords(); } - @Test - void indexFailsWithNonExistingTable() throws Exception { + @EnumSource(IndexTest.IndexSyntax.class) + @ParameterizedTest + void indexFailsWithNonExistingTable(IndexTest.IndexSyntax indexSyntax) throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + - "CREATE INDEX t_idx as select a from foo"; + IndexTest.index(indexSyntax, "t_idx", List.of(new IndexedColumn("a")), List.of(), "foo"); shouldFailWith(stmt, ErrorCode.INVALID_SCHEMA_TEMPLATE); } - @Test - void indexFailsWithNonExistingIndexColumn() throws Exception { + @EnumSource(IndexTest.IndexSyntax.class) + @ParameterizedTest + void indexFailsWithNonExistingIndexColumn(IndexTest.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)) " + + IndexTest.index(indexSyntax, "t_idx", List.of(new IndexedColumn("non_existing")), List.of(), "foo"); shouldFailWith(stmt, ErrorCode.UNDEFINED_COLUMN); } - @Test - void indexFailsWithReservedKeywordAsName() throws Exception { + @EnumSource(IndexTest.IndexSyntax.class) + @ParameterizedTest + void indexFailsWithReservedKeywordAsName(IndexTest.IndexSyntax indexSyntax) throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + - "CREATE INDEX table as select a from foo"; + IndexTest.index(indexSyntax, "table", List.of(new IndexedColumn("a")), List.of(), "foo"); shouldFailWith(stmt, ErrorCode.SYNTAX_ERROR); } @@ -337,7 +346,7 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @ParameterizedTest @MethodSource("columnTypePermutations") - void createSchemaTemplateWithOutOfOrderDefinitionsWork(List columns) throws Exception { + void createSchemaTemplateWithOutOfOrderDefinitionsWork(IndexTest.IndexSyntax indexSyntax, List columns) throws Exception { final String templateStatement = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TABLE TBL " + makeColumnDefinition(columns, true) + "CREATE TYPE AS STRUCT FOO " + makeColumnDefinition(columns, false); @@ -358,7 +367,7 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat /*Schema Template tests*/ @ParameterizedTest @MethodSource("columnTypePermutations") - void createSchemaTemplates(List columns) throws Exception { + void createSchemaTemplates(IndexTest.IndexSyntax indexSyntax, List columns) throws Exception { 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))"; @@ -384,7 +393,7 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @ParameterizedTest @MethodSource("columnTypePermutations") - void createSchemaTemplateTableWithOnlyRecordType(List columns) throws Exception { + void createSchemaTemplateTableWithOnlyRecordType(IndexTest.IndexSyntax indexSyntax, List columns) throws Exception { final String baseTableDef = makeColumnDefinition(columns, false).replace(")", ", SINGLE ROW ONLY)"); final String columnStatement = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TABLE FOO " + baseTableDef; @@ -411,12 +420,12 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @ParameterizedTest @MethodSource("columnTypePermutations") - void createSchemaTemplateWithDuplicateIndexesFails(List columns) throws Exception { + void createSchemaTemplateWithDuplicateIndexesFails(IndexTest.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 + IndexTest.index(indexSyntax, "foo_idx", List.of(new IndexedColumn("col0")), List.of(), "foo") + + IndexTest.index(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 @@ -432,12 +441,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(IndexTest.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; + IndexTest.index(indexSyntax, "v_idx", indexColumns.stream().map(IndexedColumn::new).collect(Collectors.toList()), List.of(), "tbl"); shouldWorkWithInjectedFactory(templateStatement, new AbstractMetadataOperationsFactory() { @Nonnull @@ -478,14 +487,14 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @ParameterizedTest @MethodSource("columnTypePermutations") - void createSchemaTemplateWithIndexAndInclude(List columns) throws Exception { + void createSchemaTemplateWithIndexAndInclude(IndexTest.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); + IndexTest.index(indexSyntax, "v_idx", indexedColumns.stream().map(IndexedColumn::new).collect(Collectors.toList()), unindexedColumns, "tbl"); shouldWorkWithInjectedFactory(templateStatement, new AbstractMetadataOperationsFactory() { @Nonnull @Override @@ -586,7 +595,7 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @ParameterizedTest @MethodSource("columnTypePermutations") - void createTable(List columns) throws Exception { + void createTable(IndexTest.IndexSyntax indexSyntax, List columns) throws Exception { final String columnStatement = "CREATE SCHEMA TEMPLATE test_template CREATE TABLE FOO " + makeColumnDefinition(columns, true); shouldWorkWithInjectedFactory(columnStatement, new AbstractMetadataOperationsFactory() { @@ -611,7 +620,7 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @ParameterizedTest @MethodSource("columnTypePermutations") - void createTableAndType(List columns) throws Exception { + void createTableAndType(IndexTest.IndexSyntax indexSyntax, List columns) throws Exception { final String tableDef = "CREATE TABLE tbl " + makeColumnDefinition(columns, true); final String typeDef = "CREATE TYPE AS STRUCT typ " + makeColumnDefinition(columns, false); final String templateStatement = "CREATE SCHEMA TEMPLATE test_template " + 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..ae1c072f6e 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 @@ -30,6 +30,7 @@ import com.apple.foundationdb.relational.api.metadata.Index; import com.apple.foundationdb.relational.api.metadata.SchemaTemplate; import com.apple.foundationdb.relational.api.metadata.Table; +import com.apple.foundationdb.relational.generated.RelationalParser; import com.apple.foundationdb.relational.recordlayer.EmbeddedRelationalExtension; import com.apple.foundationdb.relational.recordlayer.RelationalConnectionRule; import com.apple.foundationdb.relational.recordlayer.Utils; @@ -40,6 +41,7 @@ import com.apple.foundationdb.relational.util.NullableArrayUtils; import com.apple.foundationdb.relational.utils.SimpleDatabaseRule; import com.apple.foundationdb.relational.utils.TestSchemas; +import com.google.common.base.Strings; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; @@ -47,11 +49,17 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; import java.util.Locale; import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; import static com.apple.foundationdb.record.metadata.Key.Expressions.concat; import static com.apple.foundationdb.record.metadata.Key.Expressions.concatenateFields; @@ -333,12 +341,13 @@ void createBitMapIndexWithRedundantFunctionsIsSupported() throws Exception { indexIs(stmt, field("P1").groupBy(concat(field("A"), function("bitmap_bucket_offset", concat(field("P1"), value(10000))), field("B"))), IndexTypes.BITMAP_VALUE); } - @Test - void createIndexWithMultipleFunctionsInProjectionIsSupported() throws Exception { - String functions = "a & 2, a | 4, a ^ 8, b + c, b - c, b * c, b / c, b % c"; + @EnumSource(IndexSyntax.class) + @ParameterizedTest + void createIndexWithMultipleFunctionsInProjectionIsSupported(IndexSyntax indexSyntax) throws Exception { + List functions = List.of("a & 2", "a | 4", "a ^ 8", "b + c", "b - c", "b * c", "b / c", "b % c"); final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TABLE T1(p1 bigint, a bigint, b bigint, c bigint, primary key(p1)) " + - "CREATE INDEX mv1 AS SELECT " + functions + " FROM T1 ORDER BY " + functions; + index(indexSyntax, "mv1", functions.stream().map(IndexedColumn::new).collect(Collectors.toList()), List.of(), "T1"); indexIs(stmt, concat( function("bitand", concat(field("A"), value(2))), function("bitor", concat(field("A"), value(4))), @@ -351,12 +360,14 @@ void createIndexWithMultipleFunctionsInProjectionIsSupported() throws Exception ), IndexTypes.VALUE); } - @Test - void createIndexWithSomeFunctionsOnlyCoveringIsSupported() throws Exception { - String functions = "a & 2, a | 2, a ^ 2, b + c, b - c, b * c, b / c, b % c"; + @EnumSource(IndexSyntax.class) + @ParameterizedTest + void createIndexWithSomeFunctionsOnlyCoveringIsSupported(IndexSyntax indexSyntax) throws Exception { + List indexedColumns = List.of("a & 2", "b - c"); + List includedColumns = List.of("a | 2", "a ^ 2", "b + c", "b * c", "b / c", "b % c"); final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TABLE T1(p1 bigint, a bigint, b bigint, c bigint, primary key(p1)) " + - "CREATE INDEX mv1 AS SELECT " + functions + " FROM T1 ORDER BY a & 2, b - c"; + index(indexSyntax, "mv1", indexedColumns.stream().map(IndexedColumn::new).collect(Collectors.toList()), includedColumns, "T1"); indexIs(stmt, new KeyWithValueExpression(concat( function("bitand", concat(field("A"), value(2))), function("sub", concat(field("B"), field("C"))), @@ -378,52 +389,69 @@ void createAggregateIndexWithComplexGroupingExpressionCase1() throws Exception { function("add", concat(field("B"), value(3))))), IndexTypes.PERMUTED_MAX); } - @Test - void createSimpleValueIndex() throws Exception { + @EnumSource(IndexSyntax.class) + @ParameterizedTest + void createSimpleValueIndex(IndexSyntax indexSyntax) throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TABLE T1(p1 bigint, a1 bigint, primary key(p1)) " + - "CREATE INDEX mv1 AS SELECT a1 FROM T1"; + index(indexSyntax, "mv1", List.of(new IndexedColumn("a1")), List.of(), "T1"); indexIs(stmt, field("A1"), IndexTypes.VALUE ); } - @Test - void createSimpleValueIndexOnTwoCols() throws Exception { + @EnumSource(IndexSyntax.class) + @ParameterizedTest + void createSimpleValueIndexOnTwoCols(IndexSyntax indexSyntax) throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TABLE T1(p1 bigint, a1 bigint, a2 bigint, primary key(p1)) " + - "CREATE INDEX mv1 AS SELECT a1, a2 FROM T1 order by a1, a2"; + index(indexSyntax, "mv1", List.of(new IndexedColumn("a1"), new IndexedColumn("a2")), List.of(), "T1"); indexIs(stmt, concat(field("A1"), field("A2")), IndexTypes.VALUE); } - @Test - void createSimpleValueIndexOnNestedCol() throws Exception { + @EnumSource(IndexSyntax.class) + @ParameterizedTest + void createSimpleValueIndexOnNestedCol(IndexSyntax indexSyntax) throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TYPE AS STRUCT S1(S1_1 bigint, S1_2 bigint) " + "CREATE TABLE T1(p1 bigint, a1 bigint, a2 S1, primary key(p1)) " + - "CREATE INDEX mv1 AS SELECT a2.S1_1 FROM T1 order by a2.S1_1"; + index(indexSyntax, "mv1", List.of(new IndexedColumn("a2.S1_1")), List.of(), "T1"); indexIs(stmt, field("A2").nest(field("S1_1")), IndexTypes.VALUE); } - @Test - void createSimpleValueIndexOnTwoColsReverse() throws Exception { + @EnumSource(IndexSyntax.class) + @ParameterizedTest + void createSimpleValueIndexOnTwoColsReverse(IndexSyntax indexSyntax) throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TABLE T1(p1 bigint, a1 bigint, a2 bigint, primary key(p1)) " + - "CREATE INDEX mv1 AS SELECT a1, a2 FROM T1 order by a2, a1"; + index(indexSyntax, "mv1", List.of(new IndexedColumn("a1"), new IndexedColumn("a2")), List.of(), "T1"); indexIs(stmt, concat(field("A2"), field("A1")), IndexTypes.VALUE); } + @EnumSource(IndexSyntax.class) + @ParameterizedTest + void createCoveringValueIndex(IndexSyntax indexSyntax) throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T1(p1 bigint, a1 bigint, a2 bigint, a3 bigint, primary key(p1)) " + + index(indexSyntax, "mv1", List.of(new IndexedColumn("a1"), new IndexedColumn("a2")), List.of("a3"), "T1"); + indexIs(stmt, + keyWithValue(concat(field("A1"), field("A2"), field("A3")), 2), + IndexTypes.VALUE + ); + } + @Test - void createCoveringValueIndex() throws Exception { + void createCoveringValueIndexNewDefinition() throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TABLE T1(p1 bigint, a1 bigint, a2 bigint, a3 bigint, primary key(p1)) " + - "CREATE INDEX mv1 AS SELECT a1, a2, a3 FROM T1 order by a1, a2"; + "CREATE INDEX mv1 AS SELECT a1, a2, a3 FROM T1 order by a1, a2 " + + "CREATE INDEX mv1 AS T1(a1, a2) INCLUDE (a3)"; indexIs(stmt, keyWithValue(concat(field("A1"), field("A2"), field("A3")), 2), IndexTypes.VALUE @@ -446,6 +474,14 @@ void createIndexOrderByUnknownColumns() throws Exception { shouldFailWith(stmt, ErrorCode.UNDEFINED_COLUMN, "non existing column"); } + @Test + void createIndexOnUnknownColumns() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T1(p1 bigint, a1 bigint, a2 bigint, primary key(p1)) " + + "CREATE INDEX mv1 ON T1(a4)"; + shouldFailWith(stmt, ErrorCode.UNDEFINED_COLUMN, "non existing column"); + } + @Test void createIndexOrderByUnprojectedColumn() throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + @@ -499,12 +535,15 @@ void createIndexWithRepeatedNestedCartesianSplitByField() throws Exception { indexIs(stmt, keyWithValue(concat(field("A").nest(field("values", KeyExpression.FanType.FanOut).nest("COL2")), field("COL5"), field("A").nest(field("values", KeyExpression.FanType.FanOut).nest(concatenateFields("COL3", "COL4")))), 3), IndexTypes.VALUE); } - @Test - void createIndexWithNonRepeatedNestedSplitByField() throws Exception { + @EnumSource(IndexSyntax.class) + @ParameterizedTest + void createIndexWithNonRepeatedNestedSplitByField(IndexSyntax indexSyntax) throws Exception { + List indexedColumns = List.of("T1.a.col2", "T1.col5", "T1.a.col3"); + List includedColumns = List.of("T1.a.col4"); final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TYPE AS STRUCT A(col2 string, col3 bigint, col4 bigint) " + "CREATE TABLE T1(col1 bigint, a A, col5 bigint, primary key(col1)) " + - "CREATE INDEX mv1 AS SELECT T1.a.col2, T1.col5, T1.a.col3, T1.a.col4 FROM T1 ORDER BY T1.a.col2, T1.col5, T1.a.col3"; + index(indexSyntax, "mv1", indexedColumns.stream().map(IndexedColumn::new).collect(Collectors.toList()), includedColumns, "T1"); // In theory, this should be fine, as the nested value is not repeated, but this is currently not distinguished by the index generator shouldFailWith(stmt, ErrorCode.UNSUPPORTED_OPERATION, "Index with multiple disconnected references to the same column are not supported"); } @@ -555,21 +594,23 @@ void createIndexAsSelectWithGroupByWithoutExplicitProjectionOfGroupingValuesWork ); } - @Test - void createIndexOnNestedFields() throws Exception { + @EnumSource(IndexSyntax.class) + @ParameterizedTest + void createIndexOnNestedFields(IndexSyntax indexSyntax) throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TYPE AS STRUCT Y(a bigint, b bigint)" + "CREATE TYPE AS STRUCT X(s Y)" + "CREATE TABLE T1(col1 bigint, r X, primary key(col1)) " + - "CREATE INDEX mv1 AS SELECT r.s.a, r.s.b FROM T1 order by r.s.a, r.s.b"; + index(indexSyntax, "mv1", List.of(new IndexedColumn("r.s.a"), new IndexedColumn("r.s.b")), List.of(), "T1"); indexIs(stmt, field("R").nest(field("S").nest(concat(field("A"), field("B")))), IndexTypes.VALUE ); } - @Test - void createIndexOnDeeplyNestedFields() throws Exception { + @EnumSource(IndexSyntax.class) + @ParameterizedTest + void createIndexOnDeeplyNestedFields(IndexSyntax indexSyntax) throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TYPE AS STRUCT A(b B)" + "CREATE TYPE AS STRUCT B(c C)" + @@ -579,7 +620,7 @@ void createIndexOnDeeplyNestedFields() throws Exception { "CREATE TYPE AS STRUCT F(g G)" + "CREATE TYPE AS STRUCT G(x bigint, y bigint)" + "CREATE TABLE T1(col1 bigint, a A, primary key(col1)) " + - "CREATE INDEX mv1 AS SELECT a.b.c.d.e.f.g.x, a.b.c.d.e.f.g.y from T1 order by a.b.c.d.e.f.g.y"; + index(indexSyntax, "mv1", List.of(new IndexedColumn("a.b.c.d.e.f.g.y")), List.of("a.b.c.d.e.f.g.x"), "T1"); indexIs(stmt, keyWithValue( field("A") @@ -597,29 +638,32 @@ void createIndexOnDeeplyNestedFields() throws Exception { IndexTypes.VALUE); } - @Test - void createSimpleVersionIndex() throws Exception { + @EnumSource(IndexSyntax.class) + @ParameterizedTest + void createSimpleVersionIndex(IndexSyntax indexSyntax) throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TABLE T1(col1 bigint, primary key(col1)) " + - "CREATE INDEX mv1 AS SELECT \"__ROW_VERSION\" FROM T1 ORDER BY \"__ROW_VERSION\" " + + index(indexSyntax, "mv1", List.of(new IndexedColumn("\"__ROW_VERSION\"")), List.of(), "T1") + "WITH OPTIONS(store_row_versions=true)"; indexIs(stmt, version(), IndexTypes.VERSION); } - @Test - void createVersionIndexWithAliasedTable() throws Exception { + @EnumSource(IndexSyntax.class) + @ParameterizedTest + void createVersionIndexWithAliasedTable(IndexSyntax indexSyntax) throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TABLE T1(col1 bigint, primary key(col1)) " + - "CREATE INDEX mv1 AS SELECT t.\"__ROW_VERSION\" FROM T1 AS t ORDER BY t.\"__ROW_VERSION\" " + + index(indexSyntax, "mv1", List.of(new IndexedColumn("t.\"__ROW_VERSION\"")), List.of(), "T1") + "WITH OPTIONS(store_row_versions=true)"; indexIs(stmt, version(), IndexTypes.VERSION); } - @Test - void failToCreateVersionIndexWithUnknownTable() throws Exception { + @EnumSource(IndexSyntax.class) + @ParameterizedTest + void failToCreateVersionIndexWithUnknownTable(IndexSyntax indexSyntax) throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TABLE T1(col1 bigint, primary key(col1)) " + - "CREATE INDEX mv1 AS SELECT t2.\"__ROW_VERSION\" FROM T1 AS t ORDER BY t2.\"__ROW_VERSION\" " + + index(indexSyntax, "mv1", List.of(new IndexedColumn("t2.\"__ROW_VERSION\"")), List.of(), "T1") + "WITH OPTIONS(store_row_versions=true)"; shouldFailWith(stmt, ErrorCode.UNDEFINED_COLUMN, "Attempting to query non existing column 'T2.__ROW_VERSION'"); } @@ -918,13 +962,60 @@ void createIndexWithOrderByExpression() throws Exception { shouldFailWith(stmt, ErrorCode.INVALID_COLUMN_REFERENCE, "Cannot create index and order by an expression that is not present in the projection list"); } - @Test - void createIndexWithOrderByMixedDirection() throws Exception { + @EnumSource(IndexSyntax.class) + @ParameterizedTest + void createIndexWithOrderByMixedDirection(IndexSyntax indexSyntax) throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TABLE T1(col1 bigint, col2 bigint, col3 bigint, primary key(col1)) " + - "CREATE INDEX mv1 AS SELECT col1, col2, col3 FROM T1 ORDER BY col1 ASC, col2 DESC, col3 NULLS LAST"; + index(indexSyntax, "mv1", List.of( + new IndexedColumn("col1", "ASC", null), + new IndexedColumn("col2", "DESC", null), + new IndexedColumn("col3", null, "NULLS LAST")), + List.of(), "T1"); indexIs(stmt, concat(field("COL1"), function("order_desc_nulls_last", field("COL2")), function("order_asc_nulls_last", field("COL3"))), IndexTypes.VALUE); } + + /** + * Enum to represent the different index creation syntaxes. + */ + public enum IndexSyntax { + MATERIALIZED_VIEW, // CREATE INDEX AS SELECT ... FROM ... ORDER BY ... + CLASSIC_ON_TABLE // CREATE INDEX ON table(columns) ... + } + + public 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; + } + } + + static String index(@Nonnull IndexSyntax indexSyntax, @Nonnull String indexName, @Nonnull List indexedColumns, List includedColumns, String tableName) { + if (indexSyntax == IndexSyntax.MATERIALIZED_VIEW) { + return " CREATE INDEX " + indexName + + " AS SELECT " + Stream.concat(indexedColumns.stream().map(c -> c.column), includedColumns.stream()).collect(Collectors.joining(",")) + + " FROM " + tableName + + (indexedColumns.size() > 1 || !includedColumns.isEmpty() ? + " ORDER BY " + indexedColumns.stream().map(IndexedColumn::toString).collect(Collectors.joining(",")) : + "") + " "; + } else { + return " CREATE INDEX " + indexName + + " ON " + tableName + "(" + indexedColumns.stream().map(IndexedColumn::toString).collect(Collectors.joining(",")) + ") " + + (includedColumns.isEmpty() ? "" : "INCLUDE (" + String.join(",", includedColumns) + ")"); + } + } } 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 (q307.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=< q307> label="q307" 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' +P (0 8@kCOVERING(IDX_IOT_NAME [EQUALS promote(@c8 AS STRING)] -> [ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q313.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_NAME
> 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=< q313> label="q313" 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 +MY ɕ>(L08@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 (q86.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=< q86> label="q86" 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 +LY ޶=(L0࿱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 (q86.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=< q86> label="q86" 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 +O} :(U08&@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 (q101.AGE AS AGE, q101.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=< q101> label="q101" 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 +ÑP} ;(U08&@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 (q101.AGE AS AGE, q101.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=< q101> label="q101" 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' + P $(08@COVERING(IDX_MV_INCLUDE [EQUALS promote(@c12 AS STRING)] -> [COUNTRY: VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME, _.EMAIL AS EMAIL, _.COUNTRY AS COUNTRY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q306.NAME AS NAME, q306.EMAIL AS EMAIL, q306.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=< q306> label="q306" 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' +МR ¶#(08@COVERING(IDX_IOT_INCLUDE [EQUALS promote(@c12 AS STRING)] -> [COUNTRY: KEY[2], EMAIL: KEY[1], ID: KEY[4], 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 (q309.NAME AS NAME, q309.EMAIL AS EMAIL, q309.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=< q309> label="q309" 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 +} Ϗ (U0-8&@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 (q101.AGE AS AGE, q101.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=< q101> label="q101" 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 +} 喽 (U0(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 (q101.AGE AS AGE, q101.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=< q101> label="q101" 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 +ߩ} ƍ (U0+8&@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 (q101.AGE AS AGE, q101.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=< q101> label="q101" 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 +} ժ (U0/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 (q101.AGE AS AGE, q101.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=< q101> label="q101" 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 +} (U0@8&@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 (q101.AGE AS AGE, q101.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=< q101> label="q101" 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 +Ŵ} Ԡ (U0.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 (q101.AGE AS AGE, q101.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=< q101> label="q101" 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 + Y (L08@^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 (q86.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=< q86> label="q86" 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 + Y (L08@_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 (q86.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_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=< q86> label="q86" 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 +ﻧ Y (L0ߠ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 (q86.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=< q86> label="q86" 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 + Y (L08@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 (q86.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=< q86> label="q86" 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 +Y (L0 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 (q86.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=< q86> label="q86" 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 +܉ Y (L08@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 (q86.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=< q86> label="q86" 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 +ȥu Ť +(X028.@|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 (q106.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=< q106> label="q106" 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 +u (X0L8.@}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 (q106.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=< q106> label="q106" 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/alternate-index-syntax.metrics.yaml b/yaml-tests/src/test/resources/alternate-index-syntax.metrics.yaml new file mode 100644 index 0000000000..d79270ef10 --- /dev/null +++ b/yaml-tests/src/test/resources/alternate-index-syntax.metrics.yaml @@ -0,0 +1,270 @@ +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: 1841 + task_total_time_ms: 172 + transform_count: 453 + transform_time_ms: 65 + transform_yield_count: 157 + insert_time_ms: 32 + insert_new_count: 219 + insert_reused_count: 21 +- query: EXPLAIN select name from customers_index_on_table where name = 'Alice' + explain: 'COVERING(IDX_IOT_NAME [EQUALS promote(@c8 AS STRING)] -> [ID: KEY[2], + NAME: KEY[0]]) | MAP (_.NAME AS NAME)' + task_count: 1872 + task_total_time_ms: 168 + transform_count: 458 + transform_time_ms: 68 + transform_yield_count: 159 + insert_time_ms: 19 + insert_new_count: 223 + insert_reused_count: 22 +- 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: 318 + task_total_time_ms: 161 + transform_count: 89 + transform_time_ms: 130 + transform_yield_count: 76 + insert_time_ms: 2 + 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: 318 + task_total_time_ms: 161 + transform_count: 89 + transform_time_ms: 129 + transform_yield_count: 76 + 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: 472 + task_total_time_ms: 166 + transform_count: 125 + transform_time_ms: 123 + transform_yield_count: 85 + 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: 472 + task_total_time_ms: 168 + transform_count: 125 + transform_time_ms: 124 + transform_yield_count: 85 + insert_time_ms: 7 + 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: 1782 + task_total_time_ms: 168 + transform_count: 449 + transform_time_ms: 76 + transform_yield_count: 153 + insert_time_ms: 13 + 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: + KEY[2], EMAIL: KEY[1], ID: KEY[4], NAME: KEY[0]]) | MAP (_.NAME AS NAME, _.EMAIL + AS EMAIL, _.COUNTRY AS COUNTRY)' + task_count: 1813 + task_total_time_ms: 172 + transform_count: 455 + transform_time_ms: 74 + transform_yield_count: 155 + insert_time_ms: 13 + insert_new_count: 216 + 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 + 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: 472 + task_total_time_ms: 47 + transform_count: 125 + transform_time_ms: 26 + transform_yield_count: 85 + 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: 472 + task_total_time_ms: 45 + transform_count: 125 + transform_time_ms: 26 + transform_yield_count: 85 + 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: 472 + task_total_time_ms: 44 + transform_count: 125 + transform_time_ms: 25 + transform_yield_count: 85 + 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: 472 + task_total_time_ms: 47 + transform_count: 125 + transform_time_ms: 27 + transform_yield_count: 85 + 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: 472 + task_total_time_ms: 45 + transform_count: 125 + transform_time_ms: 25 + transform_yield_count: 85 + insert_time_ms: 1 + 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: 472 + task_total_time_ms: 45 + transform_count: 125 + transform_time_ms: 25 + transform_yield_count: 85 + 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: 318 + task_total_time_ms: 27 + transform_count: 89 + transform_time_ms: 17 + transform_yield_count: 76 + 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_NULLS_FIRST <,> -> [COUNTRY: KEY[0], ID: KEY[2]]) | + MAP (_.COUNTRY AS COUNTRY)' + task_count: 318 + task_total_time_ms: 28 + transform_count: 89 + transform_time_ms: 19 + transform_yield_count: 76 + 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], + ASC_NULLS_LAST), ID: KEY[2]]) | MAP (_.COUNTRY AS COUNTRY)' + task_count: 318 + task_total_time_ms: 25 + transform_count: 89 + transform_time_ms: 14 + transform_yield_count: 76 + 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: 318 + task_total_time_ms: 24 + transform_count: 89 + transform_time_ms: 13 + transform_yield_count: 76 + 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: 318 + task_total_time_ms: 6 + transform_count: 89 + transform_time_ms: 4 + transform_yield_count: 76 + 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: 318 + task_total_time_ms: 23 + transform_count: 89 + transform_time_ms: 13 + transform_yield_count: 76 + 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: 484 + task_total_time_ms: 34 + transform_count: 117 + transform_time_ms: 21 + transform_yield_count: 88 + 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: 484 + task_total_time_ms: 34 + transform_count: 117 + transform_time_ms: 20 + transform_yield_count: 88 + insert_time_ms: 1 + insert_new_count: 46 + insert_reused_count: 6 diff --git a/yaml-tests/src/test/resources/alternate-index-syntax.yamsql b/yaml-tests/src/test/resources/alternate-index-syntax.yamsql new file mode 100644 index 0000000000..3a1a50b877 --- /dev/null +++ b/yaml-tests/src/test/resources/alternate-index-syntax.yamsql @@ -0,0 +1,245 @@ +# +# alternate-index-syntax.yamsql +# +# This source file is part of the FoundationDB open source project +# +# Copyright 2021-2025 Apple Inc. and the FoundationDB project authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Test alternate CREATE INDEX ON syntax vs traditional CREATE INDEX AS SELECT +# Both syntaxes should produce equivalent query plans +--- +options: + supported_version: !current_version +--- +schema_template: | + # Table with indexes defined via materialized view + create table customers_materialized_view( + id integer, + name string, + email string, + age integer, + city string, + country string, + profession string, + primary key(id) + ) + + create index idx_mv_name as select name from customers_materialized_view order by name + create index idx_mv_email as select email from customers_materialized_view order by email desc + create index idx_mv_multi as select age, city from customers_materialized_view order by age asc, city asc + create index idx_mv_asc_desc as select age, city from customers_materialized_view order by age asc, city desc + create index idx_mv_include as select name, email, country from customers_materialized_view order by name + + # NULLS FIRST/LAST ordering + create index idx_mv_nulls_first as select country from customers_materialized_view order by country asc nulls first + create index idx_mv_nulls_last as select country from customers_materialized_view order by country asc nulls last + create index idx_mv_desc_nulls_first as select age from customers_materialized_view order by age desc nulls first + create index idx_mv_desc_nulls_last as select age from customers_materialized_view order by age desc nulls last + + # Identical table with CREATE INDEX ON syntax (index on table style) + create table customers_index_on_table( + id integer, + name string, + email string, + age integer, + city string, + country string, + profession string, + primary key(id) + ) + + # CREATE INDEX ON syntax (index on table style) + create index idx_iot_name on customers_index_on_table(name) + create index idx_iot_email on customers_index_on_table(email desc) + create index idx_iot_multi on customers_index_on_table(age, city) + create index idx_iot_asc_desc on customers_index_on_table(age, city desc) + create index idx_iot_include on customers_index_on_table(name) include(email, country) + + # Test NULLS FIRST/LAST ordering + create index idx_iot_nulls_first on customers_index_on_table(country asc nulls first) + create index idx_iot_nulls_last on customers_index_on_table(country asc nulls last) + create index idx_iot_desc_nulls_first on customers_index_on_table(age desc nulls first) + create index idx_iot_desc_nulls_last on customers_index_on_table(age desc nulls last) + + # Test UNIQUE indexes on profession column + create unique index idx_mv_unique_profession as select profession from customers_materialized_view order by profession + create unique index idx_iot_unique_profession on customers_index_on_table(profession) + +--- +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_NAME [EQUALS promote(@c8 AS STRING)] -> [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: KEY[2], EMAIL: KEY[1], ID: KEY[4], 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_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 + 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 +...