diff --git a/docs/sphinx/source/reference/Indexes.rst b/docs/sphinx/source/reference/Indexes.rst index cd23d57ed0..6033e76537 100644 --- a/docs/sphinx/source/reference/Indexes.rst +++ b/docs/sphinx/source/reference/Indexes.rst @@ -64,6 +64,152 @@ In each case, the fact that index entries are ordered by `fname` is leveraged, e the scan to a smaller range of index entries. The `lname` field can then be returned to the user without having to perform an additional lookup of the underlying row. +Index Syntax Alternatives +########################## + +The Relational Layer supports two equivalent syntaxes for creating indexes: + +1. **INDEX AS SELECT** - A query-based syntax (shown above) inspired by materialized views +2. **INDEX ON** - A traditional columnar syntax that creates indexes on tables or views + +Both syntaxes produce identical index structures and have the same capabilities. The choice between them is primarily +a matter of style and organizational preference. + +INDEX ON Syntax +*************** + +The ``INDEX ON`` syntax provides a more traditional approach to index creation, specifying columns directly rather +than through a SELECT query: + +.. code-block:: sql + + CREATE INDEX ON () [INCLUDE()] [OPTIONS(...)] + +Where: + +* :sql:`indexName` is the name of the index +* :sql:`source` is a table or view name +* :sql:`columns` specifies the index key columns with optional ordering +* :sql:`INCLUDE` clause (optional) adds covered columns stored as values +* :sql:`OPTIONS` clause (optional) specifies index-specific options + +Using the same employee table from above, we can create an equivalent index using the INDEX ON syntax: + +.. code-block:: sql + + CREATE INDEX fnameIdx ON employee(fname) INCLUDE(lname) + +This creates the same index structure as the INDEX AS SELECT example - a VALUE index with ``fname`` in the key +and ``lname`` as a covered value. + +Syntax Comparison +***************** + +These two approaches create identical indexes: + +**INDEX AS SELECT:** + +.. code-block:: sql + + CREATE INDEX fnameIdx AS + SELECT fname, lname + FROM employee + ORDER BY fname + +**INDEX ON:** + +.. code-block:: sql + + CREATE INDEX fnameIdx ON employee(fname) INCLUDE(lname) + +Column Ordering and NULL Handling +################################## + +When creating indexes using either syntax, you can control how values are sorted in the index through ordering +clauses and NULL semantics. + +Sorting Criteria +**************** + +Each key column in an INDEX ON definition supports explicit sort order: + +* :sql:`ASC` (ascending) - Values sorted from smallest to largest (default if not specified) +* :sql:`DESC` (descending) - Values sorted from largest to smallest + +For INDEX AS SELECT, the sort order is specified in the ORDER BY clause. + +NULL Semantics +************** + +You can control where NULL values appear in the sort order: + +* :sql:`NULLS FIRST` - NULL values appear before non-NULL values +* :sql:`NULLS LAST` - NULL values appear after non-NULL values + +**Default NULL behavior:** + +* For ``ASC`` ordering: ``NULLS FIRST`` is the default +* For ``DESC`` ordering: ``NULLS LAST`` is the default + +Ordering Syntax Examples +************************* + +The ordering clause for each column in INDEX ON can take several forms: + +1. Sort order only: ``columnName ASC`` or ``columnName DESC`` +2. Sort order with null semantics: ``columnName ASC NULLS LAST`` or ``columnName DESC NULLS FIRST`` +3. Null semantics only: ``columnName NULLS FIRST`` (uses default ASC ordering) + +Examples: + +.. code-block:: sql + + -- Ascending order with nulls last + CREATE INDEX idx_rating ON products(rating ASC NULLS LAST) + + -- Descending order with nulls first + CREATE INDEX idx_price ON products(price DESC NULLS FIRST) + + -- Specify only null semantics (ascending is implicit) + CREATE INDEX idx_stock ON products(stock NULLS LAST) + + -- Mixed ordering across multiple columns + CREATE INDEX idx_complex ON products( + category ASC NULLS FIRST, + price DESC NULLS LAST, + name ASC + ) + +For INDEX AS SELECT syntax, the same ordering is specified in the ORDER BY clause: + +.. code-block:: sql + + CREATE INDEX idx_rating AS + SELECT rating + FROM products + ORDER BY rating ASC NULLS LAST + +Partitioning for Vector Indexes +################################ + +Vector indexes support an optional ``PARTITION BY`` clause that allows organizing vectors by category or tenant. +This clause is **only applicable to vector indexes** created with the ``VECTOR INDEX`` syntax and is not supported +for regular value indexes. + +Partitioning helps improve query performance for vector similarity searches by limiting the search space to relevant +partitions: + +.. code-block:: sql + + CREATE VECTOR INDEX idx_embedding USING HNSW ON products(embedding) + PARTITION BY(category) + +In this example, vectors are partitioned by product category, so similarity searches can be scoped to specific +categories for better performance. + +**Important:** The ``PARTITION BY`` clause cannot be used with regular (non-vector) indexes created using either +the INDEX AS SELECT or INDEX ON syntax. + Indexes on nested fields ######################## @@ -128,3 +274,8 @@ that resembles the structure of the SQL statement: * Projected fields :sql:`f1`, :sql:`f2`, ... :sql:`fn` in (sub)queries maps to a :sql:`concat(field(f1), field(f2), ... field(fn))`. * Projected nested fields (:sql:`f1`, :sql:`f2`, ... :sql:`fn`) from a repeated field :sql:`rf`, i.e. :sql:`select f1, f2, ... fn, ... from FOO.rf` maps to :sql:`field(rf, FAN_OUT).nest(field(f1), (field(f2), ..., field(fn)))`. + +See Also +######## + +* :doc:`CREATE INDEX ` - Complete CREATE INDEX command reference with detailed syntax and examples diff --git a/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.diagram b/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.diagram index dfc11e4aa0..ee846041bd 100644 --- a/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.diagram +++ b/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.diagram @@ -1,8 +1,84 @@ Diagram( - Terminal('CREATE'), - Optional('UNIQUE', 'skip'), - Terminal('INDEX'), - NonTerminal('indexName'), - Terminal('AS'), - NonTerminal('query') - ) + Stack( + Sequence( + Terminal('CREATE'), + Choice(0, + Sequence( + Optional('UNIQUE', 'skip') + ), + Sequence( + Terminal('VECTOR') + ) + ), + Terminal('INDEX'), + NonTerminal('indexName') + ), + Choice(0, + Sequence( + Terminal('AS'), + NonTerminal('query') + ), + Stack( + Sequence( + Optional(Sequence( + Terminal('USING'), + Terminal('HNSW') + )), + Terminal('ON'), + NonTerminal('source'), + Terminal('('), + OneOrMore( + Sequence( + NonTerminal('keyColumn') + ), + ',' + ), + Terminal(')') + ), + Optional( + Sequence( + Terminal('INCLUDE'), + Terminal('('), + OneOrMore(NonTerminal('valueColumn'), ','), + Terminal(')') + ) + ), + Optional( + Sequence( + Terminal('PARTITION'), + Terminal('BY'), + Terminal('('), + OneOrMore(NonTerminal('partitionColumn'), ','), + Terminal(')') + ) + ), + Optional( + Sequence( + Terminal('OPTIONS'), + Terminal('('), + Choice(0, + Sequence( + OneOrMore( + Sequence( + NonTerminal('optionName') + ), + ',' + ) + ), + Sequence( + OneOrMore( + Sequence( + NonTerminal('optionName'), + Terminal('='), + NonTerminal('optionValue') + ), + ',' + ) + ) + ), + Terminal(')')) + ) + ) + ) + ) + ).format(paddingBottom=330, paddingRight=40) diff --git a/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.rst b/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.rst index 2e81e2b54a..a54d314358 100644 --- a/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.rst +++ b/docs/sphinx/source/reference/sql_commands/DDL/CREATE/INDEX.rst @@ -2,11 +2,15 @@ CREATE INDEX ============ -Clause in a :ref:`schema template definition ` to create an index. The syntax here takes -a query and is inspired by materialized view syntax. However, unlike a general `VIEW`, the index must be able to -maintained incrementally. That means that for any given record insert, update, or delete, it must be possible -to construct the difference that needs to be applied to each index in order to update it without needing to -completely rebuild it. That means that there are a number of limitations to the `query` argument. See +Clause in a :ref:`schema template definition ` to create an index. The Record Layer supports +two different syntaxes for creating indexes: + +1. **INDEX AS SELECT** - A query-based syntax inspired by materialized views +2. **INDEX ON** - A traditional syntax that creates indexes on views or tables + +Both syntaxes create indexes that are maintained incrementally. For any given record insert, update, or delete, +the system constructs the difference that needs to be applied to each index in order to update it without needing to +completely rebuild it. This means there are limitations to what kinds of indexes can be created. See :ref:`index_definition` for more details. Syntax @@ -15,8 +19,688 @@ Syntax .. raw:: html :file: INDEX.diagram.svg +INDEX AS SELECT Syntax +====================== + +The ``INDEX AS SELECT`` syntax uses a query to define the index structure. This syntax is inspired by materialized +views and allows you to define indexes using familiar SQL SELECT queries with GROUP BY for aggregate indexes. + +Basic Form +---------- + +.. code-block:: sql + + CREATE INDEX indexName AS query + +The ``query`` must be a SELECT statement that returns the columns to be indexed. The index structure is derived +from the query's SELECT list and ORDER BY clause. + +Examples +-------- + +**Simple Value Index** + +.. code-block:: sql + + CREATE INDEX idx_price AS + SELECT price + FROM products + ORDER BY price + +**Covering Value Index** + +.. code-block:: sql + + CREATE INDEX idx_category_price AS + SELECT category, price, name + FROM products + ORDER BY category, price + +This creates an index with ``category`` and ``price`` in the key, and ``name`` as a covered column. + +**Aggregate Index (SUM)** + +.. code-block:: sql + + CREATE INDEX idx_sales_by_category AS + SELECT category, SUM(amount) + FROM sales + GROUP BY category + +**Aggregate Index (COUNT)** + +COUNT(*) counts all records in each group: + +.. code-block:: sql + + CREATE INDEX idx_count_by_region AS + SELECT region, COUNT(*) + FROM sales + GROUP BY region + +COUNT(column) counts non-NULL values: + +.. code-block:: sql + + CREATE INDEX idx_count_non_null AS + SELECT category, COUNT(quantity) + FROM sales + GROUP BY category + +**Aggregate Index (MIN/MAX)** + +MIN and MAX support permuted ordering, where the aggregate value can appear at different positions in the key: + +.. code-block:: sql + + CREATE INDEX idx_max_by_category AS + SELECT category, MAX(amount) + FROM sales + GROUP BY category + ORDER BY category, MAX(amount) + +The aggregate can also appear in the middle of the key: + +.. code-block:: sql + + CREATE INDEX idx_cat_max_region AS + SELECT category, MAX(amount), region + FROM sales + GROUP BY category, region + ORDER BY category, MAX(amount), region + +**Aggregate Index (MIN_EVER/MAX_EVER)** + +MIN_EVER and MAX_EVER track the minimum/maximum value ever seen, even after deletions. These are useful for +maintaining historical extrema: + +.. code-block:: sql + + CREATE INDEX idx_min_ever_by_category AS + SELECT category, MIN_EVER(price) + FROM products + GROUP BY category + + CREATE INDEX idx_max_ever_by_region AS + SELECT region, MAX_EVER(sales_amount) + FROM transactions + GROUP BY region + +For legacy compatibility with older versions, you can use the LEGACY_EXTREMUM_EVER attribute: + +.. code-block:: sql + + CREATE INDEX idx_min_ever_legacy AS + SELECT category, MIN_EVER(price) + FROM products + GROUP BY category + WITH ATTRIBUTES LEGACY_EXTREMUM_EVER + +**Bitmap Aggregate Index** + +Bitmap indexes use specialized functions for efficient set operations and are particularly useful for filtering +on high-cardinality columns: + +.. code-block:: sql + + CREATE INDEX idx_bitmap_by_category AS + SELECT bitmap_construct_agg(bitmap_bit_position(id)) AS bitmap, + category, + bitmap_bucket_offset(id) AS offset + FROM products + GROUP BY category, bitmap_bucket_offset(id) + +For ungrouped bitmap indexes: + +.. code-block:: sql + + CREATE INDEX idx_bitmap_all AS + SELECT bitmap_construct_agg(bitmap_bit_position(id)) AS bitmap, + bitmap_bucket_offset(id) AS offset + FROM products + GROUP BY bitmap_bucket_offset(id) + +**Aggregate with Expressions in GROUP BY** + +You can use expressions in GROUP BY clauses: + +.. code-block:: sql + + CREATE INDEX idx_sum_by_expression AS + SELECT amount + 100, MAX(quantity) + FROM sales + GROUP BY amount + 100 + + CREATE INDEX idx_multi_expression AS + SELECT category_id + region_id, status + 10, MIN(price) + FROM products + GROUP BY category_id + region_id, status + 10 + +**Filtered Index** + +.. code-block:: sql + + CREATE INDEX idx_expensive_products AS + SELECT name, price + FROM products + WHERE price > 100 + ORDER BY price + +**Multiple Grouping Columns** + +.. code-block:: sql + + CREATE INDEX idx_sales_by_category_region AS + SELECT category, region, SUM(amount) + FROM sales + GROUP BY category, region + +**Descending Order** + +.. code-block:: sql + + CREATE INDEX idx_price_desc AS + SELECT price + FROM products + ORDER BY price DESC + +**Mixed Ordering** + +.. code-block:: sql + + CREATE INDEX idx_category_desc_price_asc AS + SELECT category, price + FROM products + ORDER BY category DESC, price ASC + +**NULL Ordering** + +.. code-block:: sql + + CREATE INDEX idx_rating_nulls_last AS + SELECT rating + FROM products + ORDER BY rating ASC NULLS LAST + + CREATE INDEX idx_supplier_desc_nulls_first AS + SELECT supplier + FROM products + ORDER BY supplier DESC NULLS FIRST + +Aggregate Index Capabilities +----------------------------- + +Aggregate indexes support the following aggregate functions: + +**Supported Aggregate Functions** + +- ``SUM(column)`` - Sum of values +- ``COUNT(*)`` - Count of all rows +- ``COUNT(column)`` - Count of non-NULL values +- ``MIN(column)`` - Minimum value (supports permuted ordering) +- ``MAX(column)`` - Maximum value (supports permuted ordering) +- ``MIN_EVER(column)`` - Historical minimum (persists across deletions) +- ``MAX_EVER(column)`` - Historical maximum (persists across deletions) +- ``BITMAP_CONSTRUCT_AGG(bitmap_bit_position(column))`` - Bitmap construction for set operations + +**Permuted Ordering** + +MIN and MAX indexes support permuted ordering, meaning the aggregate value can appear at any position in the +ORDER BY clause: + +.. code-block:: sql + + -- Aggregate at the end (standard) + CREATE INDEX idx1 AS + SELECT category, MAX(amount) + FROM sales + GROUP BY category + ORDER BY category, MAX(amount) + + -- Aggregate in the middle + CREATE INDEX idx2 AS + SELECT category, MAX(amount), region + FROM sales + GROUP BY category, region + ORDER BY category, MAX(amount), region + + -- Aggregate at the beginning + CREATE INDEX idx3 AS + SELECT MIN(amount), category, region + FROM sales + GROUP BY category, region + ORDER BY MIN(amount), category, region + +**Aggregate Index Limitations** + +- Only one aggregate function per index +- Aggregate columns must be integer types (bigint) for most functions (SUM, COUNT, MIN, MAX) +- MIN_EVER and MAX_EVER work with strings and other comparable types +- Expressions in GROUP BY are supported +- WHERE clauses can be used with aggregate indexes for filtered aggregates + +INDEX ON Syntax +=============== + +The ``INDEX ON`` syntax creates an index on an existing view or table using a traditional columnar specification. +This syntax is particularly useful when combined with views that define filtering or aggregation logic. + +Basic Form +---------- + +.. code-block:: sql + + CREATE INDEX indexName ON source(columns) [INCLUDE(valueColumns)] [OPTIONS(...)] + +Where: +- ``source`` is a table or view name +- ``columns`` specifies the index key columns (with optional ordering) +- ``INCLUDE`` clause adds covered columns stored as values +- ``OPTIONS`` clause specifies index-specific options + +Examples +-------- + +**Simple Value Index** + +.. code-block:: sql + + CREATE INDEX idx_price ON products(price) + +**Multi-Column Index** + +.. code-block:: sql + + CREATE INDEX idx_category_price ON products(category, price) + +**Covering Index with INCLUDE** + +.. code-block:: sql + + CREATE INDEX idx_category_price_covering ON products(category, price) + INCLUDE(name, stock) + +This creates an index with ``category`` and ``price`` in the key, and ``name`` and ``stock`` as covered values. + +**Aggregate Index (SUM)** + +First define a view with the aggregation: + +.. code-block:: sql + + CREATE VIEW v_sales_by_category AS + SELECT category, SUM(amount) AS total_amount + FROM sales + GROUP BY category + + CREATE INDEX idx_sales_by_category ON v_sales_by_category(category) + INCLUDE(total_amount) + +**Aggregate Index (COUNT)** + +.. code-block:: sql + + CREATE VIEW v_count_by_region AS + SELECT region, COUNT(*) AS record_count + FROM sales + GROUP BY region + + CREATE INDEX idx_count_by_region ON v_count_by_region(region) + INCLUDE(record_count) + +**Filtered Index** + +First define a view with the filter: + +.. code-block:: sql + + CREATE VIEW v_expensive_products AS + SELECT name, price + FROM products + WHERE price > 100 + + CREATE INDEX idx_expensive_products ON v_expensive_products(price) + +**Custom Ordering** + +.. code-block:: sql + + CREATE INDEX idx_category_desc ON products(category DESC, price ASC) + +**NULL Ordering** + +.. code-block:: sql + + CREATE INDEX idx_rating_nulls_last ON products(rating ASC NULLS LAST) + +Column Ordering and NULL Handling +---------------------------------- + +When creating an index using the ``INDEX ON`` syntax, each key column can specify sorting criteria and null semantics +to control how values are ordered in the index. + +**Sorting Criteria** + +Each key column supports the following sort orders: + +- ``ASC`` (ascending) - Values are sorted from smallest to largest (default if not specified) +- ``DESC`` (descending) - Values are sorted from largest to smallest + +**NULL Semantics** + +You can control where NULL values appear in the sort order: + +- ``NULLS FIRST`` - NULL values appear before non-NULL values +- ``NULLS LAST`` - NULL values appear after non-NULL values + +Default NULL behavior: +- For ``ASC`` ordering: ``NULLS FIRST`` is the default +- For ``DESC`` ordering: ``NULLS LAST`` is the default + +**Syntax Options** + +The ordering clause for each column can take the following forms: + +1. Sort order only: ``columnName ASC`` or ``columnName DESC`` +2. Sort order with null semantics: ``columnName ASC NULLS LAST`` or ``columnName DESC NULLS FIRST`` +3. Null semantics only: ``columnName NULLS FIRST`` or ``columnName NULLS LAST`` (uses default ASC ordering) + +**Examples** + +.. code-block:: sql + + -- Ascending order with nulls last + CREATE INDEX idx_price ON products(price ASC NULLS LAST) + + -- Descending order with nulls first + CREATE INDEX idx_rating ON products(rating DESC NULLS FIRST) + + -- Specify only null semantics (ascending is implicit) + CREATE INDEX idx_stock ON products(stock NULLS LAST) + + -- Mixed ordering across multiple columns + CREATE INDEX idx_complex ON products( + category ASC NULLS FIRST, + price DESC NULLS LAST, + name ASC + ) + +VECTOR INDEX Syntax +=================== + +The ``VECTOR INDEX`` syntax creates an index specifically designed for vector similarity search using the HNSW +(Hierarchical Navigable Small World) algorithm. Vector indexes enable efficient approximate nearest neighbor (ANN) +search on high-dimensional vector data. + +Basic Form +---------- + +.. code-block:: sql + + CREATE VECTOR INDEX indexName USING HNSW ON source(vectorColumn) + [PARTITION BY(partitionColumns)] + [OPTIONS(...)] + +Where: +- ``source`` is a table or view name +- ``vectorColumn`` is a column of type ``vector(dimensions, float)`` +- ``PARTITION BY`` clause specifies partitioning columns (optional but recommended) +- ``OPTIONS`` clause specifies HNSW-specific configuration options + +.. note:: + The ``PARTITION BY`` clause is only applicable to vector indexes. It is not supported for regular value indexes + created with the ``INDEX ON`` syntax. Partitioning helps organize vectors by category or tenant, improving + query performance for vector similarity searches pertaining specific category or tenant. + +Examples +-------- + +**Simple Vector Index** + +.. code-block:: sql + + CREATE TABLE products( + id bigint, + name string, + embedding vector(128, float), + primary key(id) + ) + + CREATE VECTOR INDEX idx_embedding USING HNSW ON products(embedding) + +**Vector Index with Partitioning** + +Partitioning is recommended for better performance and to organize vectors by category or tenant: + +.. code-block:: sql + + CREATE TABLE products( + id bigint, + category string, + embedding vector(256, float), + primary key(id) + ) + + CREATE VECTOR INDEX idx_embedding USING HNSW ON products(embedding) + PARTITION BY(category) + +**Vector Index with Custom Options** + +.. code-block:: sql + + CREATE VECTOR INDEX idx_embedding USING HNSW ON products(embedding) + PARTITION BY(category) + OPTIONS ( + CONNECTIVITY = 16, + M_MAX = 32, + EF_CONSTRUCTION = 200, + METRIC = COSINE_METRIC + ) + +**Vector Index on Filtered View** + +.. code-block:: sql + + CREATE VIEW v_active_products AS + SELECT id, embedding, category + FROM products + WHERE status = 'active' + + CREATE VECTOR INDEX idx_active_embeddings USING HNSW ON v_active_products(embedding) + PARTITION BY(category) + +**Vector Index with RabitQ Quantization** + +RabitQ is a quantization technique that reduces memory usage for high-dimensional vectors: + +.. code-block:: sql + + CREATE VECTOR INDEX idx_embedding USING HNSW ON products(embedding) + PARTITION BY(category) + OPTIONS ( + USE_RABITQ = true, + RABITQ_NUM_EX_BITS = 4, + MAINTAIN_STATS_PROBABILITY = 0.01 + ) + +**Vector Index with Statistics Sampling** + +.. code-block:: sql + + CREATE VECTOR INDEX idx_embedding USING HNSW ON products(embedding) + PARTITION BY(category) + OPTIONS ( + USE_RABITQ = true, + SAMPLE_VECTOR_STATS_PROBABILITY = 0.05 + ) + +Vector Index Options +-------------------- + +HNSW Algorithm Parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``CONNECTIVITY`` (or ``M``) + The number of bi-directional links created for each node during construction. Higher values improve recall + but increase memory usage and construction time. Default: 16. + +``M_MAX`` + Maximum number of connections per layer. Default: derived from CONNECTIVITY. + +``EF_CONSTRUCTION`` + The size of the dynamic candidate list during index construction. Higher values improve index quality + but increase construction time. Default: 200. + +Distance Metrics +~~~~~~~~~~~~~~~~ + +``METRIC`` + The distance metric used for similarity search. Available options: + + - ``EUCLIDEAN_METRIC`` - L2 distance (default) + - ``MANHATTAN_METRIC`` - L1 distance + - ``DOT_PRODUCT_METRIC`` - Dot product (for normalized vectors) + - ``EUCLIDEAN_SQUARE_METRIC`` - Squared L2 distance + - ``COSINE_METRIC`` - Cosine similarity (recommended for embeddings) + +RabitQ Quantization Options +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``USE_RABITQ`` + Enable RabitQ quantization to reduce memory usage. Default: false. + +``RABITQ_NUM_EX_BITS`` + Number of extra bits for RabitQ quantization. Higher values improve accuracy but increase memory usage. + Valid range: 0-8. Default: 4. + +Statistics and Monitoring +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``MAINTAIN_STATS_PROBABILITY`` + Probability of maintaining statistics during updates when using RabitQ. Default: 0.01. + +``SAMPLE_VECTOR_STATS_PROBABILITY`` + Probability of sampling vectors for statistics collection. Default: 0.0 (disabled). + +Vector Index Limitations +------------------------- + +- Only one vector column can be indexed per vector index +- The indexed column must be of type ``vector(dimensions, float)`` +- Vector dimensions must be specified at table creation time +- INCLUDE clause is not supported for vector indexes +- Partitioning improves performance but is optional + +Comparing Both Syntaxes +======================== + +The two syntaxes are functionally equivalent and produce identical index structures. The choice between them +is primarily a matter of style and organizational preference. + +Value Index Comparison +----------------------- + +These two approaches create identical indexes: + +**INDEX AS SELECT:** + +.. code-block:: sql + + CREATE INDEX idx_category_price AS + SELECT category, price, name + FROM products + ORDER BY category, price + +**INDEX ON:** + +.. code-block:: sql + + CREATE INDEX idx_category_price ON products(category, price) + INCLUDE(name) + +Aggregate Index Comparison +--------------------------- + +These two approaches create identical aggregate indexes: + +**INDEX AS SELECT:** + +.. code-block:: sql + + CREATE INDEX idx_sales_by_category AS + SELECT category, SUM(amount) + FROM sales + GROUP BY category + +**INDEX ON:** + +.. code-block:: sql + + CREATE VIEW v_sales_by_category AS + SELECT category, SUM(amount) AS total_amount + FROM sales + GROUP BY category + + CREATE INDEX idx_sales_by_category ON v_sales_by_category(category) + INCLUDE(total_amount) + Parameters ========== +Common Parameters +----------------- + ``indexName`` - The name of the index. Note that the name of the index should be unique in the schema template. + The name of the index. Must be unique within the schema template. + +``UNIQUE`` (optional) + Specifies that the index should enforce uniqueness constraints. + +INDEX AS SELECT Parameters +--------------------------- + +``query`` + A SELECT statement that defines the index structure. The query must be incrementally maintainable. + + - For value indexes: Must include an ORDER BY clause specifying the key columns + - For aggregate indexes: Must include GROUP BY with a single aggregate function + - May include WHERE clause for filtered indexes + +INDEX ON Parameters +------------------- + +``source`` + The name of the table or view to index. + +``columns`` + A comma-separated list of column names that form the index key. Each column can optionally specify: + + - Sort order: ``ASC`` (default) or ``DESC`` + - NULL handling: ``NULLS FIRST`` or ``NULLS LAST`` + + Example: ``category DESC, price ASC NULLS LAST`` + +``INCLUDE(valueColumns)`` (optional) + A comma-separated list of additional columns to store in the index as values (not part of the key). + This creates a covering index that can satisfy queries without accessing the base table. + +``OPTIONS(...)`` (optional) + Index-specific configuration options. Available options depend on the index type. + +Limitations +=========== + +Both syntaxes share the same limitations because they use the same underlying index implementation: + +- Indexes must be incrementally maintainable +- Aggregate indexes support only one aggregate function per index +- Aggregate indexes require integer types for the aggregated column +- The query structure must allow computing index updates from individual record changes +- See :ref:`index_definition` for detailed limitations + +See Also +======== + +- :ref:`create-schema-template` - Schema template definition +- :ref:`index_definition` - Detailed index definition and limitations +- :ref:`CREATE VIEW ` - Creating views for use with INDEX ON diff --git a/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 77a34e26f3..5fd3081018 100644 --- a/fdb-relational-core/src/main/antlr/RelationalLexer.g4 +++ b/fdb-relational-core/src/main/antlr/RelationalLexer.g4 @@ -112,6 +112,7 @@ GET: 'GET'; GRANT: 'GRANT'; GROUP: 'GROUP'; HAVING: 'HAVING'; +HNSW: 'HNSW'; HIGH_PRIORITY: 'HIGH_PRIORITY'; HISTOGRAM: 'HISTOGRAM'; IF: 'IF'; @@ -919,8 +920,8 @@ ASYMMETRIC_DERIVE: 'ASYMMETRIC_DERIVE'; ASYMMETRIC_ENCRYPT: 'ASYMMETRIC_ENCRYPT'; ASYMMETRIC_SIGN: 'ASYMMETRIC_SIGN'; ASYMMETRIC_VERIFY: 'ASYMMETRIC_VERIFY'; -ATAN: 'ATAN'; ATAN2: 'ATAN2'; +ATAN: 'ATAN'; BENCHMARK: 'BENCHMARK'; BIN: 'BIN'; BIT_COUNT: 'BIT_COUNT'; @@ -942,6 +943,7 @@ CONNECTION_ID: 'CONNECTION_ID'; CONV: 'CONV'; CONVERT_TZ: 'CONVERT_TZ'; COS: 'COS'; +COSINE_METRIC: 'COSINE_METRIC'; COT: 'COT'; CRC32: 'CRC32'; CREATE_ASYMMETRIC_PRIV_KEY: 'CREATE_ASYMMETRIC_PRIV_KEY'; @@ -961,7 +963,9 @@ DES_DECRYPT: 'DES_DECRYPT'; DES_ENCRYPT: 'DES_ENCRYPT'; DIMENSION: 'DIMENSION'; DISJOINT: 'DISJOINT'; +DOT_PRODUCT_METRIC: 'DOT_PRODUCT_METRIC'; DRY: 'DRY'; +EF_CONSTRUCTION: 'EF_CONSTRUCTION'; ELT: 'ELT'; ENABLE_LONG_ROWS: 'ENABLE_LONG_ROWS'; ENCODE: 'ENCODE'; @@ -970,6 +974,8 @@ ENDPOINT: 'ENDPOINT'; ENGINE_ATTRIBUTE: 'ENGINE_ATTRIBUTE'; ENVELOPE: 'ENVELOPE'; EQUALS: 'EQUALS'; +EUCLIDEAN_METRIC: 'EUCLIDEAN_METRIC'; +EUCLIDEAN_SQUARE_METRIC: 'EUCLIDEAN_SQUARE_METRIC'; EXP: 'EXP'; EXPORT_SET: 'EXPORT_SET'; EXTERIORRING: 'EXTERIORRING'; @@ -1029,15 +1035,18 @@ LINESTRINGFROMWKB: 'LINESTRINGFROMWKB'; LN: 'LN'; LOAD_FILE: 'LOAD_FILE'; LOCATE: 'LOCATE'; -LOG: 'LOG'; LOG10: 'LOG10'; LOG2: 'LOG2'; +LOG: 'LOG'; LOWER: 'LOWER'; LPAD: 'LPAD'; LTRIM: 'LTRIM'; +CONNECTIVITY: 'CONNECTIVITY'; +MAINTAIN_STATS_PROBABILITY: 'MAINTAIN_STATS_PROBABILITY'; MAKEDATE: 'MAKEDATE'; MAKETIME: 'MAKETIME'; MAKE_SET: 'MAKE_SET'; +MANHATTAN_METRIC: 'MANHATTAN_METRIC'; MASTER_POS_WAIT: 'MASTER_POS_WAIT'; MBRCONTAINS: 'MBRCONTAINS'; MBRDISJOINT: 'MBRDISJOINT'; @@ -1047,6 +1056,7 @@ MBROVERLAPS: 'MBROVERLAPS'; MBRTOUCHES: 'MBRTOUCHES'; MBRWITHIN: 'MBRWITHIN'; MD5: 'MD5'; +METRIC: 'METRIC'; MLINEFROMTEXT: 'MLINEFROMTEXT'; MLINEFROMWKB: 'MLINEFROMWKB'; MONTHNAME: 'MONTHNAME'; @@ -1060,6 +1070,8 @@ MULTIPOINTFROMTEXT: 'MULTIPOINTFROMTEXT'; MULTIPOINTFROMWKB: 'MULTIPOINTFROMWKB'; MULTIPOLYGONFROMTEXT: 'MULTIPOLYGONFROMTEXT'; MULTIPOLYGONFROMWKB: 'MULTIPOLYGONFROMWKB'; +M_MAX: 'M_MAX'; +M_MAX_0: 'M_MAX_0'; NAME_CONST: 'NAME_CONST'; NULLIF: 'NULLIF'; NUMGEOMETRIES: 'NUMGEOMETRIES'; @@ -1082,6 +1094,7 @@ POLYGONFROMWKB: 'POLYGONFROMWKB'; POW: 'POW'; POWER: 'POWER'; QUOTE: 'QUOTE'; +RABITQ_NUM_EX_BITS: 'RABITQ_NUM_EX_BITS'; RADIANS: 'RADIANS'; RAND: 'RAND'; RANDOM_BYTES: 'RANDOM_BYTES'; @@ -1092,13 +1105,14 @@ ROW_COUNT: 'ROW_COUNT'; RPAD: 'RPAD'; RTRIM: 'RTRIM'; RUN: 'RUN'; -SEC_TO_TIME: 'SEC_TO_TIME'; +SAMPLE_VECTOR_STATS_PROBABILITY: 'SAMPLE_VECTOR_STATS_PROBABILITY'; +SCHEMA_NAME: 'SCHEMA_NAME'; SECONDARY_ENGINE_ATTRIBUTE: 'SECONDARY_ENGINE_ATTRIBUTE'; +SEC_TO_TIME: 'SEC_TO_TIME'; SESSION_USER: 'SESSION_USER'; -SHA: 'SHA'; SHA1: 'SHA1'; SHA2: 'SHA2'; -SCHEMA_NAME: 'SCHEMA_NAME'; +SHA: 'SHA'; SIGN: 'SIGN'; SIN: 'SIN'; SLEEP: 'SLEEP'; @@ -1107,6 +1121,7 @@ SQL_THREAD_WAIT_AFTER_GTIDS: 'SQL_THREAD_WAIT_AFTER_GTIDS'; SQRT: 'SQRT'; SRID: 'SRID'; STARTPOINT: 'STARTPOINT'; +STATS_THRESHOLD: 'STATS_THRESHOLD'; STORE_ROW_VERSIONS: 'STORE_ROW_VERSIONS'; STRCMP: 'STRCMP'; STR_TO_DATE: 'STR_TO_DATE'; @@ -1189,6 +1204,7 @@ UNHEX: 'UNHEX'; UNIX_TIMESTAMP: 'UNIX_TIMESTAMP'; UPDATEXML: 'UPDATEXML'; UPPER: 'UPPER'; +USE_RABITQ: 'USE_RABITQ'; UUID: 'UUID'; UUID_SHORT: 'UUID_SHORT'; VALIDATE_PASSWORD_STRENGTH: 'VALIDATE_PASSWORD_STRENGTH'; @@ -1198,9 +1214,9 @@ WEEKDAY: 'WEEKDAY'; WEEKOFYEAR: 'WEEKOFYEAR'; WEIGHT_STRING: 'WEIGHT_STRING'; WITHIN: 'WITHIN'; +X_FUNCTION: 'X'; YEARWEEK: 'YEARWEEK'; Y_FUNCTION: 'Y'; -X_FUNCTION: 'X'; // Calling conventions diff --git a/fdb-relational-core/src/main/antlr/RelationalParser.g4 b/fdb-relational-core/src/main/antlr/RelationalParser.g4 index 8460fdd8e4..38698aa19e 100644 --- a/fdb-relational-core/src/main/antlr/RelationalParser.g4 +++ b/fdb-relational-core/src/main/antlr/RelationalParser.g4 @@ -168,7 +168,58 @@ enumDefinition ; indexDefinition - : (UNIQUE)? INDEX indexName=uid AS queryTerm indexAttributes? + : (UNIQUE)? INDEX indexName=uid AS queryTerm indexAttributes? #indexAsSelectDefinition + | (UNIQUE)? INDEX indexName=uid ON source=fullId indexColumnList includeClause? indexOptions? #indexOnSourceDefinition + | VECTOR INDEX indexName=uid USING HNSW ON source=fullId indexColumnList includeClause? partitionClause? vectorIndexOptions? #vectorIndexDefinition + ; + +indexColumnList + : '(' indexColumnSpec (',' indexColumnSpec)* ')' + ; + +indexColumnSpec + : columnName=uid orderClause? + ; + +includeClause + : INCLUDE '(' uidList ')' + ; + +indexType + : UNIQUE | VECTOR + ; + +indexOptions + : OPTIONS '(' indexOption (COMMA indexOption)* ')' + ; + +indexOption + : LEGACY_EXTREMUM_EVER + ; + +vectorIndexOptions + : OPTIONS '(' vectorIndexOption (COMMA vectorIndexOption)* ')' + ; + +vectorIndexOption + : EF_CONSTRUCTION '=' efConstruction=DECIMAL_LITERAL + | CONNECTIVITY '=' connectivity=DECIMAL_LITERAL + | M_MAX '=' mMax=DECIMAL_LITERAL + | M_MAX_0 '=' mMaxZero=DECIMAL_LITERAL + | MAINTAIN_STATS_PROBABILITY '=' maintainStatsProbability=REAL_LITERAL + | METRIC '=' metric=hnswMetric + | RABITQ_NUM_EX_BITS '=' rabitQNumExBits=DECIMAL_LITERAL + | SAMPLE_VECTOR_STATS_PROBABILITY '=' statsProbability=REAL_LITERAL + | STATS_THRESHOLD '=' statsThreshold=DECIMAL_LITERAL + | USE_RABITQ '=' useRabitQ=booleanLiteral + ; + +hnswMetric + : MANHATTAN_METRIC + | EUCLIDEAN_METRIC + | EUCLIDEAN_SQUARE_METRIC + | COSINE_METRIC + | DOT_PRODUCT_METRIC ; indexAttributes @@ -408,7 +459,12 @@ orderByClause ; orderByExpression - : expression order=(ASC | DESC)? (NULLS nulls=(FIRST | LAST))? + : expression orderClause? + ; + +orderClause + : order=(ASC | DESC) (NULLS nulls=(FIRST | LAST))? + | NULLS nulls=(FIRST | LAST) ; tableSources // done @@ -1099,10 +1155,11 @@ frameRange | expression (PRECEDING | FOLLOWING) ; +*/ + partitionClause - : PARTITION BY expression (',' expression)* + : PARTITION BY '(' indexColumnSpec (',' indexColumnSpec)* ')' ; -*/ scalarFunctionName : functionNameBase diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/AbstractEmbeddedStatement.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/AbstractEmbeddedStatement.java index 140a86496f..bb203cbcb2 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/AbstractEmbeddedStatement.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/AbstractEmbeddedStatement.java @@ -215,10 +215,15 @@ private int countUpdates(@Nonnull ResultSet resultSet) throws SQLException { } return count; } catch (SQLException | RuntimeException ex) { - if (conn.canCommit()) { - conn.rollbackInternal(); + SQLException finalException = ExceptionUtil.toRelationalException(ex).toSqlException(); + try { + if (conn.canCommit()) { + conn.rollbackInternal(); + } + } catch (SQLException | RuntimeException rollbackError) { + finalException.addSuppressed(rollbackError); } - throw ExceptionUtil.toRelationalException(ex).toSqlException(); + throw finalException; } } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerIndex.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerIndex.java index 68aebe2f3d..e4b2aa8492 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerIndex.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerIndex.java @@ -224,6 +224,15 @@ public Builder setOptions(@Nonnull final Map options) { return this; } + @Nonnull + public Builder addAllOptions(@Nonnull final Map options) { + if (optionsBuilder == null) { + optionsBuilder = ImmutableMap.builder(); + } + optionsBuilder.putAll(options); + return this; + } + @Nonnull public Builder setOption(@Nonnull final String optionKey, @Nonnull final String optionValue) { if (optionsBuilder == null) { diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ParseHelpers.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ParseHelpers.java index 53b8454405..88fa8466a4 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ParseHelpers.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ParseHelpers.java @@ -40,6 +40,7 @@ import org.antlr.v4.runtime.tree.ParseTree; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Base64; import java.util.Locale; import java.util.function.Supplier; @@ -163,13 +164,18 @@ public static byte[] parseBytes(String text) { } } - public static boolean isDescending(@Nonnull RelationalParser.OrderByExpressionContext orderByExpressionContext) { - return (orderByExpressionContext.ASC() == null) && (orderByExpressionContext.DESC() != null); + public static boolean isNullsLast(@Nullable RelationalParser.OrderClauseContext orderClause, boolean isDescending) { + if (orderClause == null || orderClause.nulls == null) { + return isDescending; // Default behavior: ASC NULLS FIRST, DESC NULLS LAST + } + return orderClause.LAST() != null; } - public static boolean isNullsLast(@Nonnull RelationalParser.OrderByExpressionContext orderByExpressionContext, boolean isDescending) { - return orderByExpressionContext.nulls == null ? isDescending : - (orderByExpressionContext.FIRST() == null) && (orderByExpressionContext.LAST() != null); + public static boolean isDescending(@Nullable RelationalParser.OrderClauseContext orderClause) { + if (orderClause == null) { + return false; // Default is ASC + } + return orderClause.DESC() != null; } public static class ParseTreeLikeAdapter implements TreeLike { diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/IndexGenerator.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/MaterializedViewIndexGenerator.java similarity index 96% rename from fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/IndexGenerator.java rename to fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/MaterializedViewIndexGenerator.java index 8695c4a9e3..52df78a46e 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/IndexGenerator.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/MaterializedViewIndexGenerator.java @@ -3,7 +3,7 @@ * * This source file is part of the FoundationDB open source project * - * Copyright 2021-2025 Apple Inc. and the FoundationDB project authors + * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ * limitations under the License. */ -package com.apple.foundationdb.relational.recordlayer.query; +package com.apple.foundationdb.relational.recordlayer.query.ddl; import com.apple.foundationdb.annotation.API; import com.apple.foundationdb.record.EvaluationContext; @@ -70,6 +70,7 @@ import com.apple.foundationdb.relational.api.exceptions.ErrorCode; import com.apple.foundationdb.relational.api.exceptions.RelationalException; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerIndex; +import com.apple.foundationdb.relational.recordlayer.query.FieldValueTrieNode; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerSchemaTemplate; import com.apple.foundationdb.relational.util.Assert; import com.apple.foundationdb.relational.util.NullableArrayUtils; @@ -110,7 +111,7 @@ */ @SuppressWarnings({"PMD.TooManyStaticImports", "OptionalUsedAsFieldOrParameterType"}) @API(API.Status.EXPERIMENTAL) -public final class IndexGenerator { +public final class MaterializedViewIndexGenerator { private static final String BITMAP_BIT_POSITION = "bitmap_bit_position"; private static final String BITMAP_BUCKET_OFFSET = "bitmap_bucket_offset"; @@ -129,7 +130,7 @@ public final class IndexGenerator { private final boolean useLegacyBasedExtremumEver; - private IndexGenerator(@Nonnull RelationalExpression relationalExpression, boolean useLegacyBasedExtremumEver) { + private MaterializedViewIndexGenerator(@Nonnull RelationalExpression relationalExpression, boolean useLegacyBasedExtremumEver) { collectQuantifiers(relationalExpression); final var partialOrder = referencesAndDependencies().evaluate(Reference.initialOf(relationalExpression)); relationalExpressions = @@ -143,7 +144,8 @@ private IndexGenerator(@Nonnull RelationalExpression relationalExpression, boole } @Nonnull - public RecordLayerIndex generate(@Nonnull RecordLayerSchemaTemplate.Builder schemaTemplateBuilder, @Nonnull String indexName, boolean isUnique, boolean containsNullableArray) { + public RecordLayerIndex.Builder generate(@Nonnull RecordLayerSchemaTemplate.Builder schemaTemplateBuilder, @Nonnull String indexName, + boolean isUnique, boolean containsNullableArray, boolean generateKeyValueExpressionWithEmptyKey) { final String recordTypeName = getRecordTypeName(); // Have to use the storage name here because the index generator uses it final Type.Record tableType = schemaTemplateBuilder.findTableByStorageName(recordTypeName).getType(); @@ -188,7 +190,10 @@ public RecordLayerIndex generate(@Nonnull RecordLayerSchemaTemplate.Builder sche } final var reordered = reorderValues(fieldValues, orderByValues); final var expression = generate(reordered, orderingFunctions); - final var splitPoint = orderByValues.isEmpty() ? -1 : orderByValues.size(); + var splitPoint = orderByValues.size(); + if (orderByValues.isEmpty() && !generateKeyValueExpressionWithEmptyKey) { + splitPoint = -1; + } if (splitPoint != -1 && splitPoint < fieldValues.size()) { indexBuilder.setKeyExpression(KeyExpression.fromProto(NullableArrayUtils.wrapArray(keyWithValue(expression, splitPoint).toKeyExpression(), tableType, containsNullableArray))); } else { @@ -231,11 +236,11 @@ public RecordLayerIndex generate(@Nonnull RecordLayerSchemaTemplate.Builder sche if (IndexTypes.PERMUTED_MIN.equals(indexType) || IndexTypes.PERMUTED_MAX.equals(indexType)) { int permutedSize = aggregateOrderIndex < 0 ? 0 : (fieldValues.size() - aggregateOrderIndex); indexBuilder.setOption(IndexOptions.PERMUTED_SIZE_OPTION, permutedSize); - } else if (aggregateOrderIndex >= 0) { + } else if (aggregateOrderIndex > 0) { Assert.failUnchecked(ErrorCode.UNSUPPORTED_OPERATION, "Unsupported index definition. Cannot order " + indexType + " index by aggregate value"); } } - return indexBuilder.build(); + return indexBuilder; } @Nonnull @@ -608,7 +613,7 @@ private void checkValidity(@Nonnull List express } @Nullable - private static QueryPredicate getTopLevelPredicate(@Nonnull List expressions) { + public static QueryPredicate getTopLevelPredicate(@Nonnull List expressions) { if (expressions.isEmpty()) { return null; } @@ -633,7 +638,11 @@ private static QueryPredicate getTopLevelPredicate(@Nonnull List + * This class is responsible for processing and generating {@link RecordLayerIndex} instances from the + * {@code INDEX ON} DDL syntax, which allows creating indexes directly on table sources with explicit + * column specifications. This is in contrast to the {@code INDEX AS} syntax which defines indexes through + * arbitrary SELECT queries. + *

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

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

+ * The index definition consists of: + *

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

+ * Example usage: + *

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

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

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

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

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

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

    + *
  • For DESC columns: NULLS LAST
  • + *
  • For ASC columns: NULLS FIRST
  • + *
+ * + * @param columnSpec the parser context containing the column specification + * @param identifierVisitor the visitor used to extract the column identifier + * @return an {@link IndexedColumn} representing the parsed column specification + */ + @Nonnull + public static OnSourceIndexGenerator.IndexedColumn parseColSpec(@Nonnull final RelationalParser.IndexColumnSpecContext columnSpec, + @Nonnull final IdentifierVisitor identifierVisitor) { + final var columnId = identifierVisitor.visitUid(columnSpec.columnName); + final var orderContext = columnSpec.orderClause(); + + boolean isDesc = false; + boolean nullsLast = false; + + if (orderContext == null) { + return OnSourceIndexGenerator.IndexedColumn.of(columnId, isDesc, nullsLast); + } + + isDesc = orderContext.DESC() != null; + if (orderContext.nulls == null) { + nullsLast = isDesc; + } else { + nullsLast = orderContext.LAST() != null; + } + return OnSourceIndexGenerator.IndexedColumn.of(columnId, isDesc, nullsLast); + } + + @Nonnull + public static OnSourceIndexGenerator.IndexedColumn parseUid(@Nonnull final RelationalParser.UidContext uid, + @Nonnull final IdentifierVisitor identifierVisitor) { + final var columnId = identifierVisitor.visitUid(uid); + return OnSourceIndexGenerator.IndexedColumn.of(columnId, false, false); + } + } + + @Nonnull + public static Builder newBuilder() { + return new Builder(); + } + + public static final class Builder { + + private Identifier indexName; + + private LogicalPlanFragment indexSource; + + private SemanticAnalyzer semanticAnalyzer; + + @Nonnull + private final List keyColumns; + + @Nonnull + private final List valueColumns; + + @Nonnull + private final Map indexOptions; + + private boolean isUnique; + + private boolean useLegacyExtremum; + + private boolean useNullableArrays; + + private boolean generateKeyValueExpressionWithEmptyKey; + + private RecordLayerSchemaTemplate.Builder metadataBuilder; + + private Builder() { + this.keyColumns = new ArrayList<>(); + this.valueColumns = new ArrayList<>(); + this.indexOptions = new HashMap<>(); + } + + @Nonnull + public Builder setIndexName(@Nonnull final Identifier indexName) { + this.indexName = indexName; + return this; + } + + @Nonnull + public Builder setIndexSource(@Nonnull final LogicalPlanFragment indexSource) { + this.indexSource = indexSource; + return this; + } + + @Nonnull + public Builder setSemanticAnalyzer(@Nonnull final SemanticAnalyzer semanticAnalyzer) { + this.semanticAnalyzer = semanticAnalyzer; + return this; + } + + @Nonnull + public Builder addKeyColumn(@Nonnull final IndexedColumn keyColumn) { + keyColumns.add(keyColumn); + return this; + } + + @Nonnull + public Builder addValueColumn(@Nonnull final IndexedColumn keyColumn) { + valueColumns.add(keyColumn); + return this; + } + + @Nonnull + public List getValueColumns() { + return valueColumns; + } + + @Nonnull + public Builder addIndexOption(@Nonnull final String key, @Nonnull final String value) { + indexOptions.put(key, value); + return this; + } + + @Nonnull + public Builder addAllIndexOptions(@Nonnull final Map indexOptions) { + this.indexOptions.putAll(indexOptions); + return this; + } + + @Nonnull + public Builder setUnique(boolean isUnique) { + this.isUnique = isUnique; + return this; + } + + @Nonnull + public Builder setUseLegacyExtremum(boolean useLegacyExtremum) { + this.useLegacyExtremum = useLegacyExtremum; + return this; + } + + @Nonnull + public Builder setUseNullableArrays(boolean useNullableArrays) { + this.useNullableArrays = useNullableArrays; + return this; + } + + @Nonnull + public Builder setGenerateKeyValueExpressionWithEmptyKey(boolean generateKeyValueExpressionWithEmptyKey) { + this.generateKeyValueExpressionWithEmptyKey = generateKeyValueExpressionWithEmptyKey; + return this; + } + + + @Nonnull + public Builder setMetadataBuilder(final RecordLayerSchemaTemplate.Builder metadataBuilder) { + this.metadataBuilder = metadataBuilder; + return this; + } + + @Nonnull + public OnSourceIndexGenerator build() { + Assert.notNullUnchecked(indexName); + Assert.notNullUnchecked(indexSource); + Assert.notNullUnchecked(semanticAnalyzer); + Assert.notNullUnchecked(metadataBuilder); + return new OnSourceIndexGenerator(indexName, indexSource, keyColumns, valueColumns, + isUnique, useLegacyExtremum, useNullableArrays, generateKeyValueExpressionWithEmptyKey, + indexOptions, metadataBuilder); + } + } +} diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/package-info.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/package-info.java new file mode 100644 index 0000000000..4542e54c51 --- /dev/null +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ddl/package-info.java @@ -0,0 +1,25 @@ +/* + * package-info.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2021-2025 Apple Inc. and the FoundationDB project authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This package contains code responsible for creating index definitions from SQL. + */ + +package com.apple.foundationdb.relational.recordlayer.query.ddl; diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/BaseVisitor.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/BaseVisitor.java index ecf6590442..caee5b1a1e 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/BaseVisitor.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/BaseVisitor.java @@ -132,6 +132,11 @@ public MutablePlanGenerationContext getPlanGenerationContext() { return mutablePlanGenerationContext; } + @Nonnull + protected IdentifierVisitor getIdentifierVisitor() { + return identifierVisitor; + } + @Nonnull public Plan generateLogicalPlan(@Nonnull ParseTree parseTree) { final var result = visit(parseTree); @@ -398,8 +403,20 @@ 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 RecordLayerIndex visitVectorIndexDefinition(final RelationalParser.VectorIndexDefinitionContext ctx) { + return ddlVisitor.visitVectorIndexDefinition(ctx); } @Override 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 6602be62a5..6fef957b4c 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/DdlVisitor.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/DdlVisitor.java @@ -21,6 +21,8 @@ package com.apple.foundationdb.relational.recordlayer.query.visitors; import com.apple.foundationdb.annotation.API; +import com.apple.foundationdb.record.metadata.IndexOptions; +import com.apple.foundationdb.record.metadata.IndexTypes; import com.apple.foundationdb.record.query.plan.cascades.CorrelationIdentifier; import com.apple.foundationdb.record.query.plan.cascades.RawSqlFunction; import com.apple.foundationdb.record.query.plan.cascades.UserDefinedFunction; @@ -45,7 +47,9 @@ import com.apple.foundationdb.relational.recordlayer.query.Expression; import com.apple.foundationdb.relational.recordlayer.query.Expressions; import com.apple.foundationdb.relational.recordlayer.query.Identifier; -import com.apple.foundationdb.relational.recordlayer.query.IndexGenerator; +import com.apple.foundationdb.relational.recordlayer.query.LogicalOperators; +import com.apple.foundationdb.relational.recordlayer.query.ddl.OnSourceIndexGenerator; +import com.apple.foundationdb.relational.recordlayer.query.ddl.MaterializedViewIndexGenerator; import com.apple.foundationdb.relational.recordlayer.query.LogicalOperator; import com.apple.foundationdb.relational.recordlayer.query.PreparedParams; import com.apple.foundationdb.relational.recordlayer.query.ProceduralPlan; @@ -54,15 +58,20 @@ import com.apple.foundationdb.relational.recordlayer.query.functions.CompiledSqlFunction; import com.apple.foundationdb.relational.util.Assert; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import org.antlr.v4.runtime.ParserRuleContext; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -192,20 +201,157 @@ public RecordLayerTable visitStructDefinition(@Nonnull RelationalParser.StructDe @Nonnull @Override - public RecordLayerIndex visitIndexDefinition(@Nonnull RelationalParser.IndexDefinitionContext ctx) { - final var indexId = visitUid(ctx.indexName); + public RecordLayerIndex visitIndexAsSelectDefinition(@Nonnull RelationalParser.IndexAsSelectDefinitionContext indexDefinitionContext) { + final var indexId = visitUid(indexDefinitionContext.indexName); final var ddlCatalog = metadataBuilder.build(); // parse the index SQL query using the newly constructed metadata. getDelegate().replaceSchemaTemplate(ddlCatalog); final var viewPlan = getDelegate().getPlanGenerationContext().withDisabledLiteralProcessing(() -> - Assert.castUnchecked(ctx.queryTerm().accept(this), LogicalOperator.class).getQuantifier().getRangesOver().get()); + Assert.castUnchecked(indexDefinitionContext.queryTerm().accept(this), LogicalOperator.class).getQuantifier().getRangesOver().get()); - final var useLegacyBasedExtremumEver = ctx.indexAttributes() != null && ctx.indexAttributes().indexAttribute().stream().anyMatch(attribute -> attribute.LEGACY_EXTREMUM_EVER() != null); - final var isUnique = ctx.UNIQUE() != null; - final var generator = IndexGenerator.from(viewPlan, useLegacyBasedExtremumEver); + final var useLegacyBasedExtremumEver = indexDefinitionContext.indexAttributes() != null && indexDefinitionContext.indexAttributes().indexAttribute().stream().anyMatch(attribute -> attribute.LEGACY_EXTREMUM_EVER() != null); + final var isUnique = indexDefinitionContext.UNIQUE() != null; + final var generator = MaterializedViewIndexGenerator.from(viewPlan, useLegacyBasedExtremumEver); Assert.thatUnchecked(viewPlan instanceof LogicalSortExpression, ErrorCode.INVALID_COLUMN_REFERENCE, "Cannot create index and order by an expression that is not present in the projection list"); - return generator.generate(metadataBuilder, indexId.getName(), isUnique, containsNullableArray); + return generator.generate(metadataBuilder, indexId.getName(), isUnique, containsNullableArray, false).build(); + } + + @Nonnull + @Override + public RecordLayerIndex visitIndexOnSourceDefinition(@Nonnull final RelationalParser.IndexOnSourceDefinitionContext indexDefinitionContext) { + final var ddlCatalog = metadataBuilder.build(); + getDelegate().replaceSchemaTemplate(ddlCatalog); + getDelegate().pushPlanFragment(); + final var sourceIdentifier = visitFullId(indexDefinitionContext.source); + var logicalOperator = generateSourceAccessForIndex(sourceIdentifier); + getDelegate().getCurrentPlanFragment().setOperator(logicalOperator); + + final Identifier indexId = visitUid(indexDefinitionContext.indexName); + final var isUnique = indexDefinitionContext.UNIQUE() != null; + @Nullable final var indexOptions = indexDefinitionContext.indexOptions(); + final var useLegacyExtremum = indexOptions != null && indexOptions.indexOption().stream() + .anyMatch(option -> option.LEGACY_EXTREMUM_EVER() != null); + final var indexGeneratorBuilder = OnSourceIndexGenerator.newBuilder() + .setIndexName(indexId) + .setIndexSource(getDelegate().getCurrentPlanFragment()) + .setSemanticAnalyzer(getDelegate().getSemanticAnalyzer()) + .setUseLegacyExtremum(useLegacyExtremum) + .setUseNullableArrays(containsNullableArray) + .setMetadataBuilder(metadataBuilder) + .setUnique(isUnique); + + indexDefinitionContext.indexColumnList().indexColumnSpec().forEach(colSpec -> + indexGeneratorBuilder.addKeyColumn(OnSourceIndexGenerator.IndexedColumn + .parseColSpec(colSpec, getDelegate().getIdentifierVisitor()))); + + if (indexDefinitionContext.includeClause() != null) { + indexDefinitionContext.includeClause().uidList().uid().forEach(uid -> { + indexGeneratorBuilder.addValueColumn(OnSourceIndexGenerator.IndexedColumn + .parseUid(uid, getDelegate().getIdentifierVisitor())); + }); + } + + getDelegate().popPlanFragment(); + return indexGeneratorBuilder.build().generate().build(); + } + + @Nonnull + @Override + public RecordLayerIndex visitVectorIndexDefinition(final RelationalParser.VectorIndexDefinitionContext indexDefinitionContext) { + final var ddlCatalog = metadataBuilder.build(); + getDelegate().replaceSchemaTemplate(ddlCatalog); + getDelegate().pushPlanFragment(); + final var sourceIdentifier = visitFullId(indexDefinitionContext.source); + var logicalOperator = generateSourceAccessForIndex(sourceIdentifier); + getDelegate().getCurrentPlanFragment().setOperator(logicalOperator); + + final Identifier indexId = visitUid(indexDefinitionContext.indexName); + final var indexOptions = parseVectorOptions(indexDefinitionContext.vectorIndexOptions()); + final var indexGeneratorBuilder = OnSourceIndexGenerator.newBuilder() + .setIndexName(indexId) + .setIndexSource(getDelegate().getCurrentPlanFragment()) + .setSemanticAnalyzer(getDelegate().getSemanticAnalyzer()) + .addAllIndexOptions(indexOptions) + .setMetadataBuilder(metadataBuilder) + .setGenerateKeyValueExpressionWithEmptyKey(true) + .setUseNullableArrays(containsNullableArray); + + indexDefinitionContext.indexColumnList().indexColumnSpec().forEach(colSpec -> + indexGeneratorBuilder.addValueColumn(OnSourceIndexGenerator.IndexedColumn + .parseColSpec(colSpec, getDelegate().getIdentifierVisitor()))); + + // parse the number of dimensions. + final var indexedColumns = indexGeneratorBuilder.getValueColumns(); + Assert.thatUnchecked(indexedColumns.size() == 1, ErrorCode.UNSUPPORTED_OPERATION, + () -> "invalid number of indexed columns, only one column is supported, found " + indexedColumns.size() + " columns"); + final var indexedCol = Iterables.getOnlyElement(indexedColumns).getIdentifier(); + final var type = getDelegate().getSemanticAnalyzer().resolveIdentifier(indexedCol, getDelegate().getCurrentPlanFragment()) + .getDataType(); + Assert.thatUnchecked(type.getCode() == DataType.Code.VECTOR, ErrorCode.SYNTAX_ERROR, + () -> "indexed column must be of vector type, found '" + type.getCode() + "' instead"); + final var numberOfDimensions = ((DataType.VectorType)type).getDimensions(); + indexGeneratorBuilder.addIndexOption(IndexOptions.HNSW_NUM_DIMENSIONS, String.valueOf(numberOfDimensions)); + + Assert.isNullUnchecked(indexDefinitionContext.includeClause(), ErrorCode.UNSUPPORTED_OPERATION, + "INCLUDE clause is not supported for vector indexes"); + + if (indexDefinitionContext.partitionClause() != null) { + indexDefinitionContext.partitionClause().indexColumnSpec().forEach(colSpec -> + indexGeneratorBuilder.addKeyColumn(OnSourceIndexGenerator.IndexedColumn + .parseColSpec(colSpec, getDelegate().getIdentifierVisitor()))); + } + + getDelegate().popPlanFragment(); + return indexGeneratorBuilder.build().generate().setIndexType(IndexTypes.VECTOR).build(); + } + + @Nonnull + private LogicalOperator generateSourceAccessForIndex(@Nonnull final Identifier sourceIdentifier) { + final var semanticAnalyzer = getDelegate().getSemanticAnalyzer(); + var logicalOperator = getDelegate().getPlanGenerationContext().withDisabledLiteralProcessing(() -> + LogicalOperator.generateAccess(sourceIdentifier, Optional.empty(), Set.of(), + semanticAnalyzer, getDelegate().getCurrentPlanFragment(), + getDelegate().getLogicalOperatorCatalog())); + + if (semanticAnalyzer.tableExists(sourceIdentifier)) { + final var output = logicalOperator.getOutput().expanded().rewireQov(logicalOperator.getQuantifier().getFlowedObjectValue()); + logicalOperator = LogicalOperator.generateSimpleSelect(output, LogicalOperators.ofSingle(logicalOperator), + Optional.empty(), Optional.empty(), ImmutableSet.of(), true); + } + + return logicalOperator; + } + + @Nonnull + private Map parseVectorOptions(@Nullable final RelationalParser.VectorIndexOptionsContext indexOptionsContext) { + final var indexOptionsBuilder = ImmutableMap.builder(); + if (indexOptionsContext == null) { + return indexOptionsBuilder.build(); + } + + for (final var option : indexOptionsContext.vectorIndexOption()) { + if (option.EF_CONSTRUCTION() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_EF_CONSTRUCTION, option.efConstruction.getText()); + } else if (option.CONNECTIVITY() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_M, option.connectivity.getText()); + } else if (option.M_MAX() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_M_MAX, option.mMax.getText()); + } else if (option.MAINTAIN_STATS_PROBABILITY() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_MAINTAIN_STATS_PROBABILITY, option.maintainStatsProbability.getText()); + } else if (option.METRIC() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_METRIC, option.metric.getText()); + } else if (option.RABITQ_NUM_EX_BITS() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_RABITQ_NUM_EX_BITS, option.rabitQNumExBits.getText()); + } else if (option.SAMPLE_VECTOR_STATS_PROBABILITY() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_SAMPLE_VECTOR_STATS_PROBABILITY, option.statsProbability.getText()); + } else if (option.STATS_THRESHOLD() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_STATS_THRESHOLD, option.statsThreshold.getText()); + } else if (option.USE_RABITQ() != null) { + indexOptionsBuilder.put(IndexOptions.HNSW_USE_RABITQ, option.useRabitQ.getText()); + } + } + return indexOptionsBuilder.build(); } @Nonnull @@ -264,7 +410,6 @@ public ProceduralPlan visitCreateSchemaTemplateStatement(@Nonnull RelationalPars } structClauses.build().stream().map(this::visitStructDefinition).map(RecordLayerTable::getDatatype).forEach(metadataBuilder::addAuxiliaryType); tableClauses.build().stream().map(this::visitTableDefinition).forEach(metadataBuilder::addTable); - final List indexes = indexClauses.build().stream().map(this::visitIndexDefinition).collect(ImmutableList.toImmutableList()); // TODO: this is currently relying on the lexical order of the function to resolve function dependencies which // is limited. sqlInvokedFunctionClauses.build().forEach(functionClause -> { @@ -275,6 +420,7 @@ public ProceduralPlan visitCreateSchemaTemplateStatement(@Nonnull RelationalPars final var view = getViewMetadata(viewClause, metadataBuilder.build()); metadataBuilder.addView(view); }); + final var indexes = indexClauses.build().stream().map(clause -> Assert.castUnchecked(visit(clause), RecordLayerIndex.class)).collect(ImmutableList.toImmutableList()); for (final RecordLayerIndex index : indexes) { final var table = metadataBuilder.extractTable(index.getTableName()); final var tableWithIndex = RecordLayerTable.Builder.from(table).addIndex(index).build(); @@ -388,10 +534,14 @@ private RecordLayerView getViewMetadata(@Nonnull final RelationalParser.ViewDefi // prepared parameters in views are not supported. QueryParser.validateNoPreparedParams(viewCtx); + getDelegate().pushPlanFragment(); + // 3. visit the SQL string to generate (compile) the corresponding SQL plan. final var viewQuery = getDelegate().getPlanGenerationContext().withDisabledLiteralProcessing(() -> Assert.castUnchecked(viewCtx.viewQuery.accept(this), LogicalOperator.class)); + getDelegate().popPlanFragment(); + // 4. Return it. return RecordLayerView.newBuilder() .setName(viewName) @@ -453,7 +603,7 @@ private UserDefinedFunction visitSqlInvokedFunction(@Nonnull final RelationalPar List dataTypeList = parameters.stream().map(Expression::getDataType).collect(Collectors.toList()); List paramValueList = dataTypeList.stream().map(dt -> QuantifiedObjectValue.of(CorrelationIdentifier.uniqueId(), DataTypeUtils.toRecordLayerType(dt))).collect(Collectors.toList()); - Assert.thatUnchecked(parameters.asList().size() == 1, "we only support 1 input parameter for user defined scalar function now"); + Assert.thatUnchecked(parameters.asList().size() == 1, "only single input parameter for user defined scalar function is supported"); // only support fullId functionBody now final var functionBody = visitUserDefinedScalarFunctionStatementBody(Assert.castUnchecked(bodyCtx, RelationalParser.UserDefinedScalarFunctionStatementBodyContext.class)); diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/DelegatingVisitor.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/DelegatingVisitor.java index d82f3613e1..7661f768b8 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/DelegatingVisitor.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/DelegatingVisitor.java @@ -242,10 +242,34 @@ public DataType.Named visitEnumDefinition(@Nonnull RelationalParser.EnumDefiniti return getDelegate().visitEnumDefinition(ctx); } - @Nonnull @Override - public RecordLayerIndex visitIndexDefinition(@Nonnull RelationalParser.IndexDefinitionContext ctx) { - return getDelegate().visitIndexDefinition(ctx); + public Object visitIndexType(final RelationalParser.IndexTypeContext ctx) { + return getDelegate().visitIndexType(ctx); + } + + @Override + public Object visitIndexOptions(final RelationalParser.IndexOptionsContext ctx) { + return getDelegate().visitIndexOptions(ctx); + } + + @Override + public Object visitIndexOption(final RelationalParser.IndexOptionContext ctx) { + return getDelegate().visitIndexOption(ctx); + } + + @Override + public Object visitVectorIndexOptions(final RelationalParser.VectorIndexOptionsContext ctx) { + return getDelegate().visitVectorIndexOptions(ctx); + } + + @Override + public Object visitVectorIndexOption(final RelationalParser.VectorIndexOptionContext ctx) { + return getDelegate().visitVectorIndexOption(ctx); + } + + @Override + public Object visitHnswMetric(final RelationalParser.HnswMetricContext ctx) { + return getDelegate().visitHnswMetric(ctx); } @Nonnull @@ -536,6 +560,42 @@ 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 RecordLayerIndex visitVectorIndexDefinition(final RelationalParser.VectorIndexDefinitionContext ctx) { + return getDelegate().visitVectorIndexDefinition(ctx); + } + + @Nonnull + @Override + public Object visitIndexColumnList(@Nonnull RelationalParser.IndexColumnListContext ctx) { + return getDelegate().visitIndexColumnList(ctx); + } + + @Nonnull + @Override + public Object visitIndexColumnSpec(@Nonnull RelationalParser.IndexColumnSpecContext ctx) { + return getDelegate().visitIndexColumnSpec(ctx); + } + + @Nonnull + @Override + public Object visitIncludeClause(@Nonnull RelationalParser.IncludeClauseContext ctx) { + return getDelegate().visitIncludeClause(ctx); + } + @Override @Nullable public Void visitTableSources(@Nonnull RelationalParser.TableSourcesContext ctx) { @@ -1364,6 +1424,11 @@ public Object visitWindowName(@Nonnull RelationalParser.WindowNameContext ctx) { return getDelegate().visitWindowName(ctx); } + @Override + public Object visitPartitionClause(final RelationalParser.PartitionClauseContext ctx) { + return getDelegate().visitPartitionClause(ctx); + } + @Nonnull @Override public Object visitScalarFunctionName(@Nonnull RelationalParser.ScalarFunctionNameContext ctx) { @@ -1572,6 +1637,11 @@ public Object visitChildren(RuleNode node) { return getDelegate().visitChildren(node); } + @Override + public Object visitOrderClause(@Nonnull RelationalParser.OrderClauseContext ctx) { + return getDelegate().visitOrderClause(ctx); + } + @Override public Object visitTerminal(TerminalNode node) { return getDelegate().visitTerminal(node); diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/ExpressionVisitor.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/ExpressionVisitor.java index c374dd1105..f0d0356983 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/ExpressionVisitor.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/ExpressionVisitor.java @@ -193,8 +193,8 @@ public List visitOrderByClause(@Nonnull RelationalParser.Orde @Override public OrderByExpression visitOrderByExpression(@Nonnull RelationalParser.OrderByExpressionContext orderByExpressionContext) { final var expression = Assert.castUnchecked(orderByExpressionContext.expression().accept(this), Expression.class); - final var descending = ParseHelpers.isDescending(orderByExpressionContext); - final var nullsLast = ParseHelpers.isNullsLast(orderByExpressionContext, descending); + final var descending = ParseHelpers.isDescending(orderByExpressionContext.orderClause()); + final var nullsLast = ParseHelpers.isNullsLast(orderByExpressionContext.orderClause(), descending); return OrderByExpression.of(expression, descending, nullsLast); } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java index 7d358b8c7e..9dc6e6ad82 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java @@ -587,8 +587,8 @@ public List visitOrderByClauseForSelect(@Nonnull RelationalPa final var matchingExpressionMaybe = isAliasMaybe.flatMap(alias -> semanticAnalyzer.lookupAlias(visitFullId(alias), validSelectAliases)); matchingExpressionMaybe.ifPresentOrElse( matchingExpression -> { - final var descending = ParseHelpers.isDescending(orderByExpression); - final var nullsLast = ParseHelpers.isNullsLast(orderByExpression, descending); + final var descending = ParseHelpers.isDescending(orderByExpression.orderClause()); + final var nullsLast = ParseHelpers.isNullsLast(orderByExpression.orderClause(), descending); orderBysBuilder.add(OrderByExpression.of(matchingExpression, descending, nullsLast)); }, () -> orderBysBuilder.add(visitOrderByExpression(orderByExpression)) diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/TypedVisitor.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/TypedVisitor.java index 1730c86fef..1ac98985ab 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/TypedVisitor.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/TypedVisitor.java @@ -165,7 +165,27 @@ 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 + RecordLayerIndex visitVectorIndexDefinition(RelationalParser.VectorIndexDefinitionContext ctx); + + @Nonnull + @Override + Object visitIndexColumnList(@Nonnull RelationalParser.IndexColumnListContext ctx); + + @Nonnull + @Override + Object visitIndexColumnSpec(@Nonnull RelationalParser.IndexColumnSpecContext ctx); + + @Nonnull + @Override + Object visitIncludeClause(@Nonnull RelationalParser.IncludeClauseContext ctx); @Override Object visitIndexAttributes(RelationalParser.IndexAttributesContext ctx); diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/util/ExceptionUtil.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/util/ExceptionUtil.java index 65035feaf2..fe41d069bc 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/util/ExceptionUtil.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/util/ExceptionUtil.java @@ -22,6 +22,7 @@ import com.apple.foundationdb.annotation.API; import com.apple.foundationdb.record.RecordCoreException; +import com.apple.foundationdb.record.RecordIndexUniquenessViolation; import com.apple.foundationdb.record.metadata.MetaDataException; import com.apple.foundationdb.record.provider.foundationdb.FDBExceptions; import com.apple.foundationdb.record.provider.foundationdb.RecordAlreadyExistsException; @@ -66,7 +67,7 @@ private static RelationalException recordCoreToRelationalException(RecordCoreExc code = ErrorCode.TRANSACTION_INACTIVE; } else if (re instanceof RecordDeserializationException || re.getCause() instanceof RecordDeserializationException) { code = ErrorCode.DESERIALIZATION_FAILURE; - } else if (re instanceof RecordAlreadyExistsException || re.getCause() instanceof RecordAlreadyExistsException) { + } else if (re instanceof RecordAlreadyExistsException || re.getCause() instanceof RecordAlreadyExistsException || re instanceof RecordIndexUniquenessViolation) { code = ErrorCode.UNIQUE_CONSTRAINT_VIOLATION; } else if (re instanceof MetaDataException) { //TODO(bfines) map this to specific error codes based on the violation diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlStatementParsingTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlStatementParsingTest.java index 6164015b59..a8bbdc5b83 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlStatementParsingTest.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlStatementParsingTest.java @@ -20,8 +20,10 @@ package com.apple.foundationdb.relational.api.ddl; +import com.apple.foundationdb.record.RecordMetaDataProto; import com.apple.foundationdb.record.RecordMetaData; import com.apple.foundationdb.record.expressions.RecordKeyExpressionProto; +import com.apple.foundationdb.record.metadata.IndexTypes; import com.apple.foundationdb.record.metadata.Key; import com.apple.foundationdb.record.metadata.MetaDataValidator; import com.apple.foundationdb.record.metadata.RecordType; @@ -32,6 +34,7 @@ import com.apple.foundationdb.record.query.plan.cascades.UserDefinedFunction; import com.apple.foundationdb.record.query.plan.cascades.UserDefinedMacroFunction; import com.apple.foundationdb.relational.api.Options; +import com.apple.foundationdb.relational.api.ddl.DdlTestUtil.IndexedColumn; import com.apple.foundationdb.relational.api.exceptions.ErrorCode; import com.apple.foundationdb.relational.api.exceptions.RelationalException; import com.apple.foundationdb.relational.api.metadata.DataType; @@ -69,6 +72,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; @@ -79,6 +83,7 @@ import java.sql.SQLException; import java.sql.Types; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; @@ -94,6 +99,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(); @@ -117,7 +123,7 @@ public static void setup() { Utils.enableCascadesDebugger(); } - private static final String[] validPrimitiveDataTypes = new String[]{ + private static final String[] validPrimitiveDataTypes = new String[] { "integer", "bigint", "double", "boolean", "string", "bytes", "vector(3, float)", "vector(4, double)", "vector(5, half)" }; @@ -127,7 +133,9 @@ public static Stream columnTypePermutations() { final List items = List.of(validPrimitiveDataTypes); final PermutationIterator permutations = PermutationIterator.generatePermutations(items, numColumns); - return permutations.stream().map(Arguments::of); + return permutations.stream() + .flatMap(permutation -> Arrays.stream(DdlTestUtil.IndexSyntax.values()) + .map(syntax -> Arguments.of(syntax, permutation))); } void shouldFailWith(@Nonnull final String query, @Nullable final ErrorCode errorCode) throws Exception { @@ -196,29 +204,32 @@ void shouldWorkWithInjectedQueryFactory(@Nonnull final String query, @Nonnull Dd @Nonnull private static DescriptorProtos.FileDescriptorProto getProtoDescriptor(@Nonnull final SchemaTemplate schemaTemplate) { Assertions.assertInstanceOf(RecordLayerSchemaTemplate.class, schemaTemplate); - final var asRecordLayerSchemaTemplate = (RecordLayerSchemaTemplate) schemaTemplate; + final var asRecordLayerSchemaTemplate = (RecordLayerSchemaTemplate)schemaTemplate; return asRecordLayerSchemaTemplate.toRecordMetadata().toProto().getRecords(); } - @Test - void indexFailsWithNonExistingTable() throws Exception { + @EnumSource(DdlTestUtil.IndexSyntax.class) + @ParameterizedTest + void indexFailsWithNonExistingTable(DdlTestUtil.IndexSyntax indexSyntax) throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + - "CREATE INDEX t_idx as select a from foo"; + DdlTestUtil.generateIndexDdlStatement(indexSyntax, "t_idx", List.of(new IndexedColumn("a")), List.of(), "foo"); shouldFailWith(stmt, ErrorCode.INVALID_SCHEMA_TEMPLATE); } - @Test - void indexFailsWithNonExistingIndexColumn() throws Exception { + @EnumSource(DdlTestUtil.IndexSyntax.class) + @ParameterizedTest + void indexFailsWithNonExistingIndexColumn(DdlTestUtil.IndexSyntax indexSyntax) throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + - "CREATE TABLE foo(a bigint, PRIMARY KEY(a))" + - " CREATE INDEX t_idx as select non_existing from foo"; + "CREATE TABLE foo(a bigint, PRIMARY KEY(a)) " + + DdlTestUtil.generateIndexDdlStatement(indexSyntax, "t_idx", List.of(new IndexedColumn("non_existing")), List.of(), "foo"); shouldFailWith(stmt, ErrorCode.UNDEFINED_COLUMN); } - @Test - void indexFailsWithReservedKeywordAsName() throws Exception { + @EnumSource(DdlTestUtil.IndexSyntax.class) + @ParameterizedTest + void indexFailsWithReservedKeywordAsName(DdlTestUtil.IndexSyntax indexSyntax) throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + - "CREATE INDEX table as select a from foo"; + DdlTestUtil.generateIndexDdlStatement(indexSyntax, "table", List.of(new IndexedColumn("a")), List.of(), "foo"); shouldFailWith(stmt, ErrorCode.SYNTAX_ERROR); } @@ -249,7 +260,7 @@ void basicEnumParsedCorrectly() throws Exception { @Override public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, @Nonnull Options templateProperties) { Assertions.assertInstanceOf(RecordLayerSchemaTemplate.class, template); - Assertions.assertEquals(1, ((RecordLayerSchemaTemplate) template).getTables().size(), "should have only 1 table"); + Assertions.assertEquals(1, ((RecordLayerSchemaTemplate)template).getTables().size(), "should have only 1 table"); DescriptorProtos.FileDescriptorProto fileDescriptorProto = getProtoDescriptor(template); Assertions.assertEquals(1, fileDescriptorProto.getEnumTypeCount(), "should have one enum defined"); fileDescriptorProto.getEnumTypeList().forEach(enumDescriptorProto -> { @@ -323,14 +334,14 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull final SchemaT void failsToParseEmptyTemplateStatements() throws Exception { //empty template statements are invalid, and can be rejected in the parser final String stmt = "CREATE SCHEMA TEMPLATE test_template "; - boolean[] visited = new boolean[]{false}; + boolean[] visited = new boolean[] {false}; shouldFailWithInjectedFactory(stmt, ErrorCode.SYNTAX_ERROR, new AbstractMetadataOperationsFactory() { @Nonnull @Override public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, @Nonnull Options templateProperties) { Assertions.assertInstanceOf(RecordLayerSchemaTemplate.class, template); - Assertions.assertEquals(0, ((RecordLayerSchemaTemplate) template).getTables().size(), "Tables defined!"); + Assertions.assertEquals(0, ((RecordLayerSchemaTemplate)template).getTables().size(), "Tables defined!"); visited[0] = true; return txn -> { }; @@ -528,7 +539,8 @@ void createInvalidVectorType(String vectorType) throws Exception { @ParameterizedTest @MethodSource("columnTypePermutations") - void createSchemaTemplateWithOutOfOrderDefinitionsWork(List columns) throws Exception { + void createSchemaTemplateWithOutOfOrderDefinitionsWork(DdlTestUtil.IndexSyntax indexSyntax, List columns) throws Exception { + Assumptions.assumeTrue(indexSyntax == DdlTestUtil.IndexSyntax.INDEX_AS_SYNTAX); final String templateStatement = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TABLE TBL " + makeColumnDefinition(columns, true) + "CREATE TYPE AS STRUCT FOO " + makeColumnDefinition(columns, false); @@ -539,7 +551,7 @@ void createSchemaTemplateWithOutOfOrderDefinitionsWork(List columns) thr public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, @Nonnull Options templateProperties) { Assertions.assertInstanceOf(RecordLayerSchemaTemplate.class, template); - Assertions.assertEquals(1, ((RecordLayerSchemaTemplate) template).getTables().size(), "Incorrect number of tables"); + Assertions.assertEquals(1, ((RecordLayerSchemaTemplate)template).getTables().size(), "Incorrect number of tables"); return txn -> { }; } @@ -549,7 +561,8 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat /*Schema Template tests*/ @ParameterizedTest @MethodSource("columnTypePermutations") - void createSchemaTemplates(List columns) throws Exception { + void createSchemaTemplates(DdlTestUtil.IndexSyntax indexSyntax, List columns) throws Exception { + Assumptions.assumeTrue(indexSyntax == DdlTestUtil.IndexSyntax.INDEX_AS_SYNTAX); final String columnStatement = "CREATE SCHEMA TEMPLATE test_template " + " CREATE TYPE AS STRUCT foo " + makeColumnDefinition(columns, false) + " CREATE TABLE bar (col0 bigint, col1 foo, PRIMARY KEY(col0))"; @@ -575,7 +588,8 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @ParameterizedTest @MethodSource("columnTypePermutations") - void createSchemaTemplateTableWithOnlyRecordType(List columns) throws Exception { + void createSchemaTemplateTableWithOnlyRecordType(DdlTestUtil.IndexSyntax indexSyntax, List columns) throws Exception { + Assumptions.assumeTrue(indexSyntax == DdlTestUtil.IndexSyntax.INDEX_AS_SYNTAX); final String baseTableDef = replaceLast(makeColumnDefinition(columns, false), ')', ", SINGLE ROW ONLY)"); final String columnStatement = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TABLE foo " + baseTableDef; @@ -602,12 +616,12 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @ParameterizedTest @MethodSource("columnTypePermutations") - void createSchemaTemplateWithDuplicateIndexesFails(List columns) throws Exception { + void createSchemaTemplateWithDuplicateIndexesFails(DdlTestUtil.IndexSyntax indexSyntax, List columns) throws Exception { final String baseTableDef = makeColumnDefinition(columns, true); final String columnStatement = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TABLE FOO " + baseTableDef + - " CREATE INDEX foo_idx as select col0 from foo order by col0" + - " CREATE INDEX foo_idx as select col1 from foo order by col1"; //duplicate with the same name on same table should fail + DdlTestUtil.generateIndexDdlStatement(indexSyntax, "foo_idx", List.of(new IndexedColumn("col0")), List.of(), "foo") + + DdlTestUtil.generateIndexDdlStatement(indexSyntax, "foo_idx", List.of(new IndexedColumn("col1")), List.of(), "foo"); //duplicate with the same name on same table should fail shouldFailWithInjectedFactory(columnStatement, ErrorCode.INDEX_ALREADY_EXISTS, new AbstractMetadataOperationsFactory() { @Nonnull @@ -623,12 +637,12 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @ParameterizedTest @MethodSource("columnTypePermutations") - void createSchemaTemplateWithIndex(List columns) throws Exception { - final String indexColumns = String.join(",", chooseIndexColumns(columns, n -> n % 2 == 0)); + void createSchemaTemplateWithIndex(DdlTestUtil.IndexSyntax indexSyntax, List columns) throws Exception { + final List indexColumns = chooseIndexColumns(columns, n -> n % 2 == 0); final String templateStatement = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TYPE AS STRUCT foo " + makeColumnDefinition(columns, false) + "CREATE TABLE tbl " + makeColumnDefinition(columns, true) + - "CREATE INDEX v_idx as select " + indexColumns + " from tbl order by " + indexColumns; + DdlTestUtil.generateIndexDdlStatement(indexSyntax, "v_idx", indexColumns.stream().map(IndexedColumn::new).collect(Collectors.toList()), List.of(), "tbl"); shouldWorkWithInjectedFactory(templateStatement, new AbstractMetadataOperationsFactory() { @Nonnull @@ -636,13 +650,13 @@ void createSchemaTemplateWithIndex(List columns) throws Exception { public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, @Nonnull Options templateProperties) { Assertions.assertInstanceOf(RecordLayerSchemaTemplate.class, template); - Assertions.assertEquals(1, ((RecordLayerSchemaTemplate) template).getTables().size(), "Incorrect number of tables"); - Table info = ((RecordLayerSchemaTemplate) template).getTables().stream().findFirst().orElseThrow(); + Assertions.assertEquals(1, ((RecordLayerSchemaTemplate)template).getTables().size(), "Incorrect number of tables"); + Table info = ((RecordLayerSchemaTemplate)template).getTables().stream().findFirst().orElseThrow(); Assertions.assertEquals(1, info.getIndexes().size(), "Incorrect number of indexes!"); final Index index = Assert.optionalUnchecked(info.getIndexes().stream().findFirst()); Assertions.assertEquals("v_idx", index.getName(), "Incorrect index name!"); - final var actualKe = ((RecordLayerIndex) index).getKeyExpression().toKeyExpression(); + final var actualKe = ((RecordLayerIndex)index).getKeyExpression().toKeyExpression(); List keys = null; if (actualKe.hasThen()) { keys = new ArrayList<>(actualKe.getThen().getChildList()); @@ -669,26 +683,26 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @ParameterizedTest @MethodSource("columnTypePermutations") - void createSchemaTemplateWithIndexAndInclude(List columns) throws Exception { + void createSchemaTemplateWithIndexAndInclude(DdlTestUtil.IndexSyntax indexSyntax, List columns) throws Exception { Assumptions.assumeTrue(columns.size() > 1); //the test only works with multiple columns final List indexedColumns = chooseIndexColumns(columns, n -> n % 2 == 0); //choose every other column final List unindexedColumns = chooseIndexColumns(columns, n -> n % 2 != 0); final String templateStatement = "CREATE SCHEMA TEMPLATE test_template " + " CREATE TYPE AS STRUCT foo " + makeColumnDefinition(columns, false) + " CREATE TABLE tbl " + makeColumnDefinition(columns, true) + - " CREATE INDEX v_idx as select " + Stream.concat(indexedColumns.stream(), unindexedColumns.stream()).collect(Collectors.joining(",")) + " from tbl order by " + String.join(",", indexedColumns); + DdlTestUtil.generateIndexDdlStatement(indexSyntax, "v_idx", indexedColumns.stream().map(IndexedColumn::new).collect(Collectors.toList()), unindexedColumns, "tbl"); shouldWorkWithInjectedFactory(templateStatement, new AbstractMetadataOperationsFactory() { @Nonnull @Override public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, @Nonnull Options templateProperties) { - Assertions.assertEquals(1, ((RecordLayerSchemaTemplate) template).getTables().size(), "Incorrect number of tables"); - Table info = ((RecordLayerSchemaTemplate) template).getTables().stream().findFirst().orElseThrow(); + Assertions.assertEquals(1, ((RecordLayerSchemaTemplate)template).getTables().size(), "Incorrect number of tables"); + Table info = ((RecordLayerSchemaTemplate)template).getTables().stream().findFirst().orElseThrow(); Assertions.assertEquals(1, info.getIndexes().size(), "Incorrect number of indexes!"); final Index index = Assert.optionalUnchecked(info.getIndexes().stream().findFirst()); Assertions.assertEquals("v_idx", index.getName(), "Incorrect index name!"); - RecordKeyExpressionProto.KeyExpression actualKe = ((RecordLayerIndex) index).getKeyExpression().toKeyExpression(); + RecordKeyExpressionProto.KeyExpression actualKe = ((RecordLayerIndex)index).getKeyExpression().toKeyExpression(); Assertions.assertNotNull(actualKe.getKeyWithValue(), "Null KeyExpression for included columns!"); final RecordKeyExpressionProto.KeyWithValue keyWithValue = actualKe.getKeyWithValue(); @@ -747,7 +761,7 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @Test void dropSchemaTemplates() throws Exception { final String columnStatement = "DROP SCHEMA TEMPLATE test_template"; - boolean[] called = new boolean[]{false}; + boolean[] called = new boolean[] {false}; shouldWorkWithInjectedFactory(columnStatement, new AbstractMetadataOperationsFactory() { @Nonnull @Override @@ -777,7 +791,8 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @ParameterizedTest @MethodSource("columnTypePermutations") - void createTable(List columns) throws Exception { + void createTable(DdlTestUtil.IndexSyntax indexSyntax, List columns) throws Exception { + Assumptions.assumeTrue(indexSyntax == DdlTestUtil.IndexSyntax.INDEX_AS_SYNTAX); final String columnStatement = "CREATE SCHEMA TEMPLATE test_template CREATE TABLE foo " + makeColumnDefinition(columns, true); shouldWorkWithInjectedFactory(columnStatement, new AbstractMetadataOperationsFactory() { @@ -802,7 +817,8 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat @ParameterizedTest @MethodSource("columnTypePermutations") - void createTableAndType(List columns) throws Exception { + void createTableAndType(DdlTestUtil.IndexSyntax indexSyntax, List columns) throws Exception { + Assumptions.assumeTrue(indexSyntax == DdlTestUtil.IndexSyntax.INDEX_AS_SYNTAX); final String typeDef = "CREATE TYPE AS STRUCT typ " + makeColumnDefinition(columns, false); // current implementation of metadata prunes unused types in the serialization, this may or may not // be something we want to commit to long term. @@ -893,7 +909,7 @@ public ConstantAction getDropDatabaseConstantAction(@Nonnull URI dbUrl, boolean void listDatabasesWithoutPrefixParsesCorrectly() throws Exception { final String command = "SHOW DATABASES"; - boolean[] called = new boolean[]{false}; + boolean[] called = new boolean[] {false}; shouldWorkWithInjectedQueryFactory(command, new AbstractQueryFactory() { @Override public DdlQuery getListDatabasesQueryAction(@Nonnull URI prefixPath) { @@ -911,7 +927,7 @@ public DdlQuery getListDatabasesQueryAction(@Nonnull URI prefixPath) { void listDatabasesWithPrefixParsesCorrectly() throws Exception { final String command = "SHOW DATABASES WITH PREFIX /prefix"; - boolean[] called = new boolean[]{false}; + boolean[] called = new boolean[] {false}; shouldWorkWithInjectedQueryFactory(command, new AbstractQueryFactory() { @Override @@ -930,7 +946,7 @@ public DdlQuery getListDatabasesQueryAction(@Nonnull URI prefixPath) { void listSchemaTemplatesParsesProperly() throws Exception { final String command = "SHOW SCHEMA TEMPLATES"; - boolean[] called = new boolean[]{false}; + boolean[] called = new boolean[] {false}; shouldWorkWithInjectedQueryFactory(command, new AbstractQueryFactory() { @Override public DdlQuery getListDatabasesQueryAction(@Nonnull URI prefixPath) { @@ -964,7 +980,7 @@ public DdlQuery getListSchemaTemplatesQueryAction() { void describeSchemaTemplate() throws Exception { final String templateName = "TEST_TEMPLATE"; - boolean[] called = new boolean[]{false}; + boolean[] called = new boolean[] {false}; shouldWorkWithInjectedQueryFactory("DESCRIBE SCHEMA TEMPLATE " + templateName, new AbstractQueryFactory() { @Override public DdlQuery getDescribeSchemaTemplateQueryAction(@Nonnull String schemaId) { @@ -1007,7 +1023,7 @@ public DdlQuery getDescribeSchemaTemplateQueryAction(@Nonnull String schemaId) { void describeSchemaSucceedsWithoutDatabase() throws Exception { // because parser falls back to connection's database. final String templateName = "TEST_TEMPLATE"; - boolean[] called = new boolean[]{false}; + boolean[] called = new boolean[] {false}; shouldWorkWithInjectedQueryFactory("DESCRIBE SCHEMA " + templateName, new AbstractQueryFactory() { @Override public DdlQuery getDescribeSchemaQueryAction(@Nonnull URI dbUri, @Nonnull String schemaId) { @@ -1025,7 +1041,7 @@ public DdlQuery getDescribeSchemaQueryAction(@Nonnull URI dbUri, @Nonnull String void describeSchemaPathSucceeds() throws Exception { final String templateName = "TEST_TEMPLATE"; - boolean[] called = new boolean[]{false}; + boolean[] called = new boolean[] {false}; shouldWorkWithInjectedQueryFactory("DESCRIBE SCHEMA " + "/test_db/" + templateName, new AbstractQueryFactory() { @Override public DdlQuery getDescribeSchemaQueryAction(@Nonnull URI dbUri, @Nonnull String schemaId) { @@ -1043,7 +1059,7 @@ public DdlQuery getDescribeSchemaQueryAction(@Nonnull URI dbUri, @Nonnull String void describeSchemaWithSetDatabaseSucceeds() throws Exception { final String templateName = "TEST_TEMPLATE"; - boolean[] called = new boolean[]{false}; + boolean[] called = new boolean[] {false}; shouldWorkWithInjectedQueryFactory("DESCRIBE SCHEMA " + templateName, new AbstractQueryFactory() { @Override public DdlQuery getDescribeSchemaQueryAction(@Nonnull URI dbUri, @Nonnull String schemaId) { @@ -1061,7 +1077,7 @@ public DdlQuery getDescribeSchemaQueryAction(@Nonnull URI dbUri, @Nonnull String void createSchemaWithPath() throws Exception { final String templateName = "test_template"; - boolean[] called = new boolean[]{false}; + boolean[] called = new boolean[] {false}; shouldWorkWithInjectedFactory("CREATE SCHEMA /test_db/" + templateName + " WITH TEMPLATE " + templateName, new AbstractMetadataOperationsFactory() { @Nonnull @Override @@ -1269,9 +1285,9 @@ void createViewWithFunctionAndCteComplexNestingWorks() throws Exception { final String schemaStatement = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TYPE AS STRUCT baz (a bigint, b bigint) " + "CREATE TYPE AS ENUM foo ('OPTION_1', 'OPTION_2') " + + "CREATE TABLE bar (id bigint, baz_field baz, foo_field foo, PRIMARY KEY(id)) " + "CREATE FUNCTION F1 (IN A BIGINT) AS SELECT id, baz_field, foo_field FROM bar WHERE id > A " + - "CREATE VIEW v AS WITH C1 AS (WITH C2 AS (SELECT foo_field, id, baz_field FROM F1(20)) SELECT * FROM C2) SELECT * FROM C1 " + - "CREATE TABLE bar (id bigint, baz_field baz, foo_field foo, PRIMARY KEY(id)) "; + "CREATE VIEW v AS WITH C1 AS (WITH C2 AS (SELECT foo_field, id, baz_field FROM F1(20)) SELECT * FROM C2) SELECT * FROM C1 "; shouldWorkWithInjectedFactory(schemaStatement, new AbstractMetadataOperationsFactory() { @Nonnull @@ -1329,6 +1345,247 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplat }); } + @Test + void createIndexOnBasicSyntax() throws Exception { + final String schemaStatement = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TYPE AS STRUCT baz (a bigint, b bigint) " + + "CREATE TYPE AS ENUM foo ('OPTION_1', 'OPTION_2') " + + "CREATE TABLE bar (id bigint, a bigint, b bigint, c bigint, PRIMARY KEY(id)) " + + "CREATE INDEX i1 on bar(a, b) include (c)"; + + shouldWorkWithInjectedFactory(schemaStatement, new AbstractMetadataOperationsFactory() { + @Nonnull + @Override + public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, + @Nonnull Options templateProperties) { + final var tableMaybe = Assertions.assertDoesNotThrow(() -> template.findTableByName("bar")); + assertThat(tableMaybe).isPresent(); + final var table = Assert.optionalUnchecked(tableMaybe); + assertThat(table.getIndexes().size()).isEqualTo(1); + final var index = Assert.optionalUnchecked(table.getIndexes().stream().findFirst()); + assertThat(index.getName()).isEqualTo("i1"); + assertThat(index.getIndexType()).isEqualTo(IndexTypes.VALUE); + assertThat(index).isInstanceOf(RecordLayerIndex.class); + final var recordLayerIndex = Assert.castUnchecked(index, RecordLayerIndex.class); + assertThat(recordLayerIndex.getKeyExpression()).isEqualTo(Key.Expressions.keyWithValue( + Key.Expressions.concat(Key.Expressions.field("a"), Key.Expressions.field("b"), Key.Expressions.field("c")), 2)); + return txn -> { + }; + } + }); + } + + @Test + void createIndexOnPredicatedView() throws Exception { + final String schemaStatement = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TYPE AS STRUCT baz (a bigint, b bigint) " + + "CREATE TYPE AS ENUM foo ('OPTION_1', 'OPTION_2') " + + "CREATE TABLE bar (id bigint, a bigint, b bigint, c bigint, PRIMARY KEY(id)) " + + "CREATE VIEW v1 AS SELECT b, c FROM bar WHERE a < 100 " + + "CREATE INDEX i1 on v1(b, c)"; + + shouldWorkWithInjectedFactory(schemaStatement, new AbstractMetadataOperationsFactory() { + @Nonnull + @Override + public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, + @Nonnull Options templateProperties) { + final var tableMaybe = Assertions.assertDoesNotThrow(() -> template.findTableByName("bar")); + assertThat(tableMaybe).isPresent(); + final var table = Assert.optionalUnchecked(tableMaybe); + assertThat(table.getIndexes().size()).isEqualTo(1); + final var index = Assert.optionalUnchecked(table.getIndexes().stream().findFirst()); + assertThat(index.getName()).isEqualTo("i1"); + assertThat(index.getIndexType()).isEqualTo(IndexTypes.VALUE); + assertThat(index).isInstanceOf(RecordLayerIndex.class); + final var recordLayerIndex = Assert.castUnchecked(index, RecordLayerIndex.class); + assertThat(recordLayerIndex.getKeyExpression()).isEqualTo( + Key.Expressions.concat(Key.Expressions.field("b"), Key.Expressions.field("c"))); + assertThat(recordLayerIndex.getPredicate()).isNotNull(); + final var predicate = Assert.notNullUnchecked(recordLayerIndex.getPredicate()); + final var expectedPredicateProto = RecordMetaDataProto.Predicate.newBuilder() + .setValuePredicate(RecordMetaDataProto.ValuePredicate.newBuilder().addValue("a") + .setComparison(RecordMetaDataProto.Comparison.newBuilder() + .setSimpleComparison(RecordMetaDataProto.SimpleComparison.newBuilder() + .setType(RecordMetaDataProto.ComparisonType.LESS_THAN) + .setOperand(RecordKeyExpressionProto.Value.newBuilder().setLongValue(100L).build()) + .build()) + .build()) + .build()) + .build(); + assertThat(predicate).isEqualTo(expectedPredicateProto); + return txn -> { + }; + } + }); + } + + @Test + void createVectorIndex() throws Exception { + final String schemaStatement = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TYPE AS STRUCT baz (a bigint, b bigint) " + + "CREATE TYPE AS ENUM foo ('OPTION_1', 'OPTION_2') " + + "CREATE TABLE bar (id bigint, a bigint, b bigint, c bigint, PRIMARY KEY(id)) " + + "CREATE VIEW v1 AS SELECT b, c FROM bar WHERE a < 100 " + + "CREATE INDEX i1 on v1(b, c)"; + + shouldWorkWithInjectedFactory(schemaStatement, new AbstractMetadataOperationsFactory() { + @Nonnull + @Override + public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, + @Nonnull Options templateProperties) { + final var tableMaybe = Assertions.assertDoesNotThrow(() -> template.findTableByName("bar")); + assertThat(tableMaybe).isPresent(); + final var table = Assert.optionalUnchecked(tableMaybe); + assertThat(table.getIndexes().size()).isEqualTo(1); + final var index = Assert.optionalUnchecked(table.getIndexes().stream().findFirst()); + assertThat(index.getName()).isEqualTo("i1"); + assertThat(index.getIndexType()).isEqualTo(IndexTypes.VALUE); + assertThat(index).isInstanceOf(RecordLayerIndex.class); + final var recordLayerIndex = Assert.castUnchecked(index, RecordLayerIndex.class); + assertThat(recordLayerIndex.getKeyExpression()).isEqualTo( + Key.Expressions.concat(Key.Expressions.field("b"), Key.Expressions.field("c"))); + assertThat(recordLayerIndex.getPredicate()).isNotNull(); + final var predicate = Assert.notNullUnchecked(recordLayerIndex.getPredicate()); + final var expectedPredicateProto = RecordMetaDataProto.Predicate.newBuilder() + .setValuePredicate(RecordMetaDataProto.ValuePredicate.newBuilder().addValue("a") + .setComparison(RecordMetaDataProto.Comparison.newBuilder() + .setSimpleComparison(RecordMetaDataProto.SimpleComparison.newBuilder() + .setType(RecordMetaDataProto.ComparisonType.LESS_THAN) + .setOperand(RecordKeyExpressionProto.Value.newBuilder().setLongValue(100L).build()) + .build()) + .build()) + .build()) + .build(); + assertThat(predicate).isEqualTo(expectedPredicateProto); + return txn -> { + }; + } + }); + } + + @Test + void createIndexOnBasicSyntaxComplex() throws Exception { + final String schemaStatement = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TYPE AS STRUCT baz (a bigint, b bigint) " + + "CREATE TYPE AS ENUM foo ('OPTION_1', 'OPTION_2') " + + "CREATE TABLE bar (id bigint, a bigint, b bigint, c bigint, PRIMARY KEY(id)) " + + "CREATE VIEW v1 AS SELECT * FROM (SELECT b as x, c as y FROM bar) as d " + + "CREATE INDEX i1 on v1(x desc nulls first, y asc nulls last)"; + + shouldWorkWithInjectedFactory(schemaStatement, new AbstractMetadataOperationsFactory() { + @Nonnull + @Override + public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, + @Nonnull Options templateProperties) { + final var tableMaybe = Assertions.assertDoesNotThrow(() -> template.findTableByName("bar")); + assertThat(tableMaybe).isPresent(); + final var table = Assert.optionalUnchecked(tableMaybe); + assertThat(table.getIndexes().size()).isEqualTo(1); + final var index = Assert.optionalUnchecked(table.getIndexes().stream().findFirst()); + assertThat(index.getIndexType()).isEqualTo(IndexTypes.VALUE); + assertThat(index.getName()).isEqualTo("i1"); + assertThat(index).isInstanceOf(RecordLayerIndex.class); + final var recordLayerIndex = Assert.castUnchecked(index, RecordLayerIndex.class); + assertThat(recordLayerIndex.getKeyExpression()).isEqualTo( + Key.Expressions.concat(Key.Expressions.function("order_desc_nulls_first", Key.Expressions.field("b")), + Key.Expressions.function("order_asc_nulls_last", Key.Expressions.field("c")))); + return txn -> { + }; + } + }); + } + + @Test + void createIndexOnRepeated() throws Exception { + final String schemaStatement = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TYPE AS STRUCT A(x bigint) " + + "CREATE TABLE T(p bigint, a A array, primary key(p)) " + + "CREATE VIEW mv1 AS SELECT SQ.x, t.p from T AS t, (select M.x from t.a AS M) SQ " + + "CREATE INDEX i1 on mv1(x, p)"; + + shouldWorkWithInjectedFactory(schemaStatement, new AbstractMetadataOperationsFactory() { + @Nonnull + @Override + public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, + @Nonnull Options templateProperties) { + final var tableMaybe = Assertions.assertDoesNotThrow(() -> template.findTableByName("T")); + assertThat(tableMaybe).isPresent(); + final var table = Assert.optionalUnchecked(tableMaybe); + assertThat(table.getIndexes().size()).isEqualTo(1); + final var index = Assert.optionalUnchecked(table.getIndexes().stream().findFirst()); + assertThat(index.getIndexType()).isEqualTo(IndexTypes.VALUE); + assertThat(index.getName()).isEqualTo("i1"); + assertThat(index).isInstanceOf(RecordLayerIndex.class); + final var recordLayerIndex = Assert.castUnchecked(index, RecordLayerIndex.class); + assertThat(recordLayerIndex.getKeyExpression()).isEqualTo( + Key.Expressions.concat(Key.Expressions.field("a", KeyExpression.FanType.None) + .nest(Key.Expressions.field("values", KeyExpression.FanType.FanOut).nest("x")), Key.Expressions.field("p"))); + return txn -> { + }; + } + }); + } + + @Test + void createIndexOnRepeatedUsingMatViewSyntax() throws Exception { + final String schemaStatement = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TYPE AS STRUCT A(x bigint) " + + "CREATE TABLE T(p bigint, a A array, primary key(p)) " + + "CREATE INDEX mv1 AS SELECT SQ.x, t.p from T AS t, (select M.x from t.a AS M) SQ order by SQ.x, t.p "; + + shouldWorkWithInjectedFactory(schemaStatement, new AbstractMetadataOperationsFactory() { + @Nonnull + @Override + public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, + @Nonnull Options templateProperties) { + final var tableMaybe = Assertions.assertDoesNotThrow(() -> template.findTableByName("T")); + assertThat(tableMaybe).isPresent(); + final var table = Assert.optionalUnchecked(tableMaybe); + assertThat(table.getIndexes().size()).isEqualTo(1); + final var index = Assert.optionalUnchecked(table.getIndexes().stream().findFirst()); + assertThat(index.getIndexType()).isEqualTo(IndexTypes.VALUE); + assertThat(index.getName()).isEqualTo("mv1"); + assertThat(index).isInstanceOf(RecordLayerIndex.class); + final var recordLayerIndex = Assert.castUnchecked(index, RecordLayerIndex.class); + assertThat(recordLayerIndex.getKeyExpression()).isEqualTo( + Key.Expressions.concat(Key.Expressions.field("a", KeyExpression.FanType.None) + .nest(Key.Expressions.field("values", KeyExpression.FanType.FanOut).nest("x")), Key.Expressions.field("p"))); + return txn -> { + }; + } + }); + } + + + @Test + void createIndexOnAggregate() throws Exception { + final String schemaStatement = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, a bigint, b bigint, c bigint, primary key(p)) " + + "CREATE VIEW mv1 AS SELECT sum(c) as S, a, b from T group by a, b " + + "CREATE INDEX i1 on mv1(a, b) include (S)"; + + shouldWorkWithInjectedFactory(schemaStatement, new AbstractMetadataOperationsFactory() { + @Nonnull + @Override + public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull SchemaTemplate template, + @Nonnull Options templateProperties) { + final var tableMaybe = Assertions.assertDoesNotThrow(() -> template.findTableByName("T")); + assertThat(tableMaybe).isPresent(); + final var table = Assert.optionalUnchecked(tableMaybe); + assertThat(table.getIndexes().size()).isEqualTo(1); + final var index = Assert.optionalUnchecked(table.getIndexes().stream().findFirst()); + assertThat(index.getIndexType()).isEqualTo(IndexTypes.SUM); + assertThat(index.getName()).isEqualTo("i1"); + assertThat(index).isInstanceOf(RecordLayerIndex.class); + final var recordLayerIndex = Assert.castUnchecked(index, RecordLayerIndex.class); + assertThat(recordLayerIndex.getKeyExpression()).isEqualTo( + Key.Expressions.field("c").groupBy(Key.Expressions.concat(Key.Expressions.field("a"), Key.Expressions.field("b")))); + return txn -> { + }; + } + }); + } + @Nonnull private static String makeColumnDefinition(@Nonnull final List columns, boolean isTable) { StringBuilder columnStatement = new StringBuilder("("); diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlTestUtil.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlTestUtil.java index 5697cbe746..0e450d511a 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlTestUtil.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlTestUtil.java @@ -39,6 +39,7 @@ import org.assertj.core.api.Assertions; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.net.URI; import java.sql.SQLException; import java.util.ArrayList; @@ -453,4 +454,51 @@ ParsedType getTable(@Nonnull final String tableName) { return null; // not reachable. } } + + /** + * Enum to represent the different index creation syntaxes. + */ + enum IndexSyntax { + INDEX_AS_SYNTAX, // CREATE INDEX AS SELECT ... FROM ... ORDER BY ... + INDEX_ON_SYNTAX // CREATE INDEX ON table(columns) ... + } + + static class IndexedColumn { + String column; + String order = ""; + String nullsOrder = ""; + + IndexedColumn(String column, @Nullable String order, @Nullable String nullsOrder) { + this.column = column; + this.order = order == null ? "" : " " + order; + this.nullsOrder = nullsOrder == null ? "" : " " + nullsOrder; + } + + IndexedColumn(String column) { + this(column, null, null); + } + + @Override + public String toString() { + return column + order + nullsOrder; + } + } + + @Nonnull + static String generateIndexDdlStatement(@Nonnull final IndexSyntax indexSyntax, @Nonnull final String indexName, + @Nonnull final List indexedColumns, @Nonnull final List includedColumns, + @Nonnull final String tableName) { + final var indexedColumnsString = indexedColumns.stream().map(IndexedColumn::toString).collect(Collectors.joining(",")); + final var includedColumnsString = String.join(",", includedColumns); + if (indexSyntax == IndexSyntax.INDEX_AS_SYNTAX) { + return " CREATE INDEX " + indexName + + " AS SELECT " + indexedColumnsString + (includedColumns.isEmpty() ? "" : ", " + includedColumnsString) + + " FROM " + tableName + + (indexedColumns.size() > 1 || !includedColumns.isEmpty() ? " ORDER BY " + indexedColumnsString : "") + " "; + } else { + return " CREATE INDEX " + indexName + + " ON " + tableName + "(" + indexedColumnsString + ") " + + (includedColumns.isEmpty() ? "" : "INCLUDE (" + includedColumnsString + ")") + " "; + } + } } diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java index 4e7f6214eb..7d8abef11d 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/IndexTest.java @@ -20,6 +20,7 @@ package com.apple.foundationdb.relational.api.ddl; +import com.apple.foundationdb.record.metadata.IndexOptions; import com.apple.foundationdb.record.metadata.IndexTypes; import com.apple.foundationdb.record.metadata.expressions.GroupingKeyExpression; import com.apple.foundationdb.record.metadata.expressions.KeyExpression; @@ -53,6 +54,12 @@ import java.util.Locale; import java.util.function.Consumer; +import static com.apple.foundationdb.record.RecordMetaDataProto.Comparison; +import static com.apple.foundationdb.record.RecordMetaDataProto.ComparisonType; +import static com.apple.foundationdb.record.RecordMetaDataProto.Predicate; +import static com.apple.foundationdb.record.RecordMetaDataProto.SimpleComparison; +import static com.apple.foundationdb.record.RecordMetaDataProto.ValuePredicate; +import static com.apple.foundationdb.record.expressions.RecordKeyExpressionProto.Value; import static com.apple.foundationdb.record.metadata.Key.Expressions.concat; import static com.apple.foundationdb.record.metadata.Key.Expressions.concatenateFields; import static com.apple.foundationdb.record.metadata.Key.Expressions.field; @@ -62,6 +69,8 @@ import static com.apple.foundationdb.record.metadata.Key.Expressions.version; import static com.apple.foundationdb.relational.util.NullableArrayUtils.REPEATED_FIELD_NAME; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + public class IndexTest { @RegisterExtension @Order(0) @@ -110,7 +119,7 @@ private void indexIs(@Nonnull final String stmt, @Nonnull final KeyExpression ex } private void indexIs(@Nonnull final String stmt, @Nonnull final KeyExpression expectedKey, @Nonnull final String indexType, - @Nonnull final Consumer validator) throws Exception { + @Nonnull final Consumer validator) throws Exception { shouldWorkWithInjectedFactory(stmt, new AbstractMetadataOperationsFactory() { @Nonnull @Override @@ -123,11 +132,12 @@ public ConstantAction getSaveSchemaTemplateConstantAction(@Nonnull final SchemaT Assertions.assertEquals(1, table.getIndexes().size(), "Incorrect number of indexes!"); final Index index = Assert.optionalUnchecked(table.getIndexes().stream().findFirst()); Assertions.assertInstanceOf(RecordLayerIndex.class, index); + final var recordLayerIndex = (RecordLayerIndex)index; Assertions.assertEquals("MV1", index.getName(), "Incorrect index name!"); Assertions.assertEquals(indexType, index.getIndexType()); - final KeyExpression actualKey = KeyExpression.fromProto(((RecordLayerIndex) index).getKeyExpression().toKeyExpression()); + final KeyExpression actualKey = KeyExpression.fromProto((recordLayerIndex).getKeyExpression().toKeyExpression()); Assertions.assertEquals(expectedKey, actualKey); - validator.accept(index); + validator.accept(recordLayerIndex); return txn -> { }; } @@ -197,7 +207,7 @@ void createdIndexWorksDeepNestingAndConcat() throws Exception { } @Test - void createdIndexWorksDeepNestingAndConcatCartesian() throws Exception { + void createdLegacyIndexWorksDeepNestingAndConcatCartesian() throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TYPE AS STRUCT A(x bigint) " + "CREATE TYPE AS STRUCT C(z bigint, k bigint) " + @@ -219,7 +229,29 @@ void createdIndexWorksDeepNestingAndConcatCartesian() throws Exception { } @Test - void createdIndexWorksDeepNestingAndNestedCartesianConcat() throws Exception { + void createdIndexWorksDeepNestingAndConcatCartesian() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TYPE AS STRUCT A(x bigint) " + + "CREATE TYPE AS STRUCT C(z bigint, k bigint) " + + "CREATE TYPE AS STRUCT B(a A array, c C array) " + + "CREATE TABLE T(p bigint, b B array, primary key(p))" + + "CREATE VIEW v1 AS SELECT SQ1.x,SQ2.z, SQ2.k from " + + " T AS t," + + " (select M.x from t.b AS Y, (select x from Y.a) M) SQ1," + + " (select M.z, M.k from t.b AS Y, (select z,k from Y.c) M) SQ2 " + + "CREATE INDEX MV1 ON v1(Z, K, X)"; + indexIs(stmt, + concat(field("B").nest(field(REPEATED_FIELD_NAME, KeyExpression.FanType.FanOut) + .nest(field("C").nest(field(REPEATED_FIELD_NAME, KeyExpression.FanType.FanOut) + .nest(concat(field("Z"), field("K")))))), + field("B").nest(field(REPEATED_FIELD_NAME, KeyExpression.FanType.FanOut) + .nest(field("A").nest(field(REPEATED_FIELD_NAME, KeyExpression.FanType.FanOut) + .nest(field("X")))))), + IndexTypes.VALUE); + } + + @Test + void createdLegacyIndexWorksDeepNestingAndNestedCartesianConcat() throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TYPE AS STRUCT A(x bigint) " + "CREATE TYPE AS STRUCT C(z bigint) " + @@ -235,14 +267,67 @@ void createdIndexWorksDeepNestingAndNestedCartesianConcat() throws Exception { } @Test - void createIndexWithPredicateIsSupported() throws Exception { + void createdIndexWorksDeepNestingAndNestedCartesianConcat() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TYPE AS STRUCT A(x bigint) " + + "CREATE TYPE AS STRUCT C(z bigint) " + + "CREATE TYPE AS STRUCT B(a A array, c C array) " + + "CREATE TABLE T(p bigint, b B array, primary key(p))" + + "CREATE VIEW v1 AS SELECT SQ.x, SQ.z from T AS t, (select M.x, N.z from t.b AS Y, (select x from Y.a) M, (select z from Y.c) N) SQ " + + "CREATE INDEX mv1 on v1(x, z)"; + indexIs(stmt, + field("B", KeyExpression.FanType.None).nest(field(NullableArrayUtils.getRepeatedFieldName(), KeyExpression.FanType.FanOut).nest( + concat(field("A", KeyExpression.FanType.None).nest(field(NullableArrayUtils.getRepeatedFieldName(), KeyExpression.FanType.FanOut).nest(field("X", KeyExpression.FanType.None))), + field("C", KeyExpression.FanType.None).nest(field(NullableArrayUtils.getRepeatedFieldName(), KeyExpression.FanType.FanOut).nest(field("Z", KeyExpression.FanType.None))) + ))), + IndexTypes.VALUE); + } + + @Test + void createLegacyIndexWithPredicateIsSupported() throws Exception { final String stmt = "CREATE SCHEMA TEMPLATE test_template " + "CREATE TYPE AS STRUCT A(x bigint) " + "CREATE TYPE AS STRUCT B(y string) " + "CREATE TABLE T(p bigint, a A array, b B array, primary key(p))" + "CREATE INDEX mv1 AS SELECT p FROM T where p > 10 order by p"; + indexIs(stmt, field("P", KeyExpression.FanType.None), IndexTypes.VALUE, index -> { + assertThat(index.isUnique()).isFalse(); + assertThat(index.getName()).isEqualTo("MV1"); + assertThat(index.getPredicate()).isEqualTo(Predicate.newBuilder() + .setValuePredicate(ValuePredicate.newBuilder().addValue("P") + .setComparison(Comparison.newBuilder() + .setSimpleComparison(SimpleComparison.newBuilder() + .setType(ComparisonType.GREATER_THAN) + .setOperand(Value.newBuilder().setLongValue(10L).build()) + .build()) + .build()) + .build()) + .build()); + }); + } + + @Test + void createIndexWithPredicateIsSupported() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TYPE AS STRUCT A(x bigint) " + + "CREATE TYPE AS STRUCT B(y string) " + + "CREATE TABLE T(p bigint, a A array, b B array, primary key(p))" + + "CREATE VIEW v AS SELECT p FROM T where p > 10 " + + "CREATE INDEX mv1 ON v(p)"; // todo (yhatem) verify the predicate. - indexIs(stmt, field("P", KeyExpression.FanType.None), IndexTypes.VALUE); + indexIs(stmt, field("P", KeyExpression.FanType.None), IndexTypes.VALUE, index -> { + assertThat(index.isUnique()).isFalse(); + assertThat(index.getPredicate()).isEqualTo(Predicate.newBuilder() + .setValuePredicate(ValuePredicate.newBuilder().addValue("P") + .setComparison(Comparison.newBuilder() + .setSimpleComparison(SimpleComparison.newBuilder() + .setType(ComparisonType.GREATER_THAN) + .setOperand(Value.newBuilder().setLongValue(10L).build()) + .build()) + .build()) + .build()) + .build()); + }); } @Test @@ -703,7 +788,7 @@ void createAggregateIndexOnMinMax(String index) throws Exception { indexIs(stmt, field("COL2").groupBy(field("COL1")), "MIN".equals(index) ? IndexTypes.PERMUTED_MIN : IndexTypes.PERMUTED_MAX, - idx -> Assertions.assertEquals("0", ((RecordLayerIndex) idx).getOptions().get("permutedSize")) + idx -> Assertions.assertEquals("0", idx.getOptions().get("permutedSize")) ); } @@ -716,7 +801,7 @@ void createAggregateIndexOnMinMaxWithGroupingOrdering(String index) throws Excep indexIs(stmt, field("COL2").groupBy(field("COL1")), "MIN".equals(index) ? IndexTypes.PERMUTED_MIN : IndexTypes.PERMUTED_MAX, - idx -> Assertions.assertEquals("0", ((RecordLayerIndex) idx).getOptions().get("permutedSize")) + idx -> Assertions.assertEquals("0", idx.getOptions().get("permutedSize")) ); } @@ -729,7 +814,7 @@ void createAggregateIndexOnMinMaxWithGroupingOrderingIncludingMax(String index) indexIs(stmt, field("COL2").groupBy(field("COL1")), "MIN".equals(index) ? IndexTypes.PERMUTED_MIN : IndexTypes.PERMUTED_MAX, - idx -> Assertions.assertEquals("0", ((RecordLayerIndex) idx).getOptions().get("permutedSize")) + idx -> Assertions.assertEquals("0", idx.getOptions().get("permutedSize")) ); } @@ -742,7 +827,21 @@ void createAggregateIndexOnMinMaxWithPermutedOrdering(String index) throws Excep indexIs(stmt, field("COL4").groupBy(concatenateFields("COL1", "COL2", "COL3")), "MIN".equals(index) ? IndexTypes.PERMUTED_MIN : IndexTypes.PERMUTED_MAX, - idx -> Assertions.assertEquals("1", ((RecordLayerIndex) idx).getOptions().get("permutedSize")) + idx -> Assertions.assertEquals("1", idx.getOptions().get("permutedSize")) + ); + } + + @ParameterizedTest + @ValueSource(strings = {"MIN", "MAX"}) + void createAggregateIndexOnSourceOnMinMaxWithPermutedOrdering(String index) throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T1(col1 bigint, col2 bigint, col3 bigint, col4 bigint, primary key(col1)) " + + String.format(Locale.ROOT, "CREATE VIEW v1 AS SELECT col1, col2, col3, %s(col4) as agg FROM T1 group by col1, col2, col3 ", index) + + "CREATE INDEX mv1 ON v1(col1, col2, agg, col3)"; + indexIs(stmt, + field("COL4").groupBy(concatenateFields("COL1", "COL2", "COL3")), + "MIN".equals(index) ? IndexTypes.PERMUTED_MIN : IndexTypes.PERMUTED_MAX, + idx -> Assertions.assertEquals("1", idx.getOptions().get("permutedSize")) ); } @@ -927,4 +1026,195 @@ void createIndexWithOrderByMixedDirection() throws Exception { concat(field("COL1"), function("order_desc_nulls_last", field("COL2")), function("order_asc_nulls_last", field("COL3"))), IndexTypes.VALUE); } + + @Test + void createVectorIndexWorksCorrectly() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(3, float), c bigint, z bigint, primary key(p))" + + "CREATE VIEW V1 AS SELECT p, b, c, z from T where c > 50 " + + "CREATE VECTOR INDEX MV1 USING HNSW ON V1(b) PARTITION BY(z)"; + indexIs(stmt, + keyWithValue(concat(field("Z"), field("B")), 1), + IndexTypes.VECTOR, + idx -> { + final var predicate = idx.getPredicate(); + assertThat(predicate).isEqualTo(Predicate.newBuilder() + .setValuePredicate(ValuePredicate + .newBuilder() + .addValue("C") + .setComparison(Comparison + .newBuilder() + .setSimpleComparison(SimpleComparison.newBuilder() + .setType(ComparisonType.GREATER_THAN) + .setOperand(Value.newBuilder().setLongValue(50).build()) + .build()) + .build()) + .build()) + .build()); + }); + } + + + @Test + void createVectorIndexWithoutPartitionClauseWorksCorrectly() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(3, float), c bigint, z bigint, primary key(p))" + + "CREATE VIEW V1 AS SELECT p, b, c, z from T " + + "CREATE VECTOR INDEX MV1 USING HNSW ON V1(b)"; + indexIs(stmt, + keyWithValue(field("B"), 0), + IndexTypes.VECTOR); + } + + @Test + void createVectorIndexWithOptionsWorksCorrectly() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(3, float), c bigint, primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) PARTITION BY (p) " + + "OPTIONS (CONNECTIVITY = 16, M_MAX = 32, EF_CONSTRUCTION = 200, METRIC = COSINE_METRIC)"; + indexIs(stmt, + keyWithValue(concat(field("P"), field("B")), 1), + IndexTypes.VECTOR, + idx -> { + final var options = idx.getOptions(); + Assertions.assertEquals("3", options.get(IndexOptions.HNSW_NUM_DIMENSIONS)); + Assertions.assertEquals("16", options.get(IndexOptions.HNSW_M)); + Assertions.assertEquals("32", options.get(IndexOptions.HNSW_M_MAX)); + Assertions.assertEquals("200", options.get(IndexOptions.HNSW_EF_CONSTRUCTION)); + Assertions.assertEquals("COSINE_METRIC", options.get(IndexOptions.HNSW_METRIC)); + validateVectorIndex(idx); + }); + } + + @Test + void createVectorIndexWithRabitQOptionsWorksCorrectly() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(128, float), primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) PARTITION BY (p) " + + "OPTIONS (USE_RABITQ = true, RABITQ_NUM_EX_BITS = 4, MAINTAIN_STATS_PROBABILITY = 0.01)"; + indexIs(stmt, + keyWithValue(concat(field("P"), field("B")), 1), + IndexTypes.VECTOR, + idx -> { + final var options = idx.getOptions(); + Assertions.assertEquals("128", options.get(IndexOptions.HNSW_NUM_DIMENSIONS)); + Assertions.assertEquals("true", options.get(IndexOptions.HNSW_USE_RABITQ)); + Assertions.assertEquals("4", options.get(IndexOptions.HNSW_RABITQ_NUM_EX_BITS)); + Assertions.assertEquals("0.01", options.get(IndexOptions.HNSW_MAINTAIN_STATS_PROBABILITY)); + validateVectorIndex(idx); + }); + } + + @ParameterizedTest + @ValueSource(ints = {2, 16, 256, 1024}) + void createVectorIndexWithVariousDimensionsWorksCorrectly(int dimensions) throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(" + dimensions + ", float), primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) PARTITION BY (p)"; + indexIs(stmt, keyWithValue(concat(field("P"), field("B")), 1), IndexTypes.VECTOR, + idx -> { + Assertions.assertEquals(String.valueOf(dimensions), idx.getOptions().get(IndexOptions.HNSW_NUM_DIMENSIONS)); + validateVectorIndex(idx); + }); + } + + @ParameterizedTest + @ValueSource(strings = {"EUCLIDEAN_METRIC", "MANHATTAN_METRIC", "DOT_PRODUCT_METRIC", "EUCLIDEAN_SQUARE_METRIC"}) + void createVectorIndexWithAllMetricTypesWorksCorrectly(String metric) throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(512, float), primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) PARTITION BY (p) OPTIONS (METRIC = " + metric + ")"; + + indexIs(stmt, keyWithValue(concat(field("P"), field("B")), 1), IndexTypes.VECTOR, + idx -> { + Assertions.assertEquals("512", idx.getOptions().get(IndexOptions.HNSW_NUM_DIMENSIONS)); + Assertions.assertEquals(metric, idx.getOptions().get(IndexOptions.HNSW_METRIC)); + // Validate using VectorIndexMaintainerFactory validator + validateVectorIndex(idx); + }); + } + + private void validateVectorIndex(RecordLayerIndex recordLayerIndex) { + // Convert RecordLayerIndex to core Index + final var coreIndex = new com.apple.foundationdb.record.metadata.Index( + recordLayerIndex.getName(), + recordLayerIndex.getKeyExpression(), + recordLayerIndex.getIndexType(), + recordLayerIndex.getOptions(), + recordLayerIndex.getPredicate() != null + ? com.apple.foundationdb.record.metadata.IndexPredicate.fromProto(recordLayerIndex.getPredicate()) + : null + ); + + // Validate using VectorIndexHelper - this validates the configuration options + // VectorIndexHelper.getConfig() will throw IllegalArgumentException if options are invalid + Assertions.assertDoesNotThrow(() -> + com.apple.foundationdb.record.provider.foundationdb.indexes.VectorIndexHelper.getConfig(coreIndex), + "Vector index configuration should be valid"); + } + + @Test + void createVectorIndexWithStatsOptionsWorksCorrectly() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(64, float), primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) PARTITION BY (p) " + + "OPTIONS (SAMPLE_VECTOR_STATS_PROBABILITY = 0.05)"; + indexIs(stmt, + keyWithValue(concat(field("P"), field("B")), 1), + IndexTypes.VECTOR, + idx -> { + final var options = idx.getOptions(); + Assertions.assertEquals("64", options.get(IndexOptions.HNSW_NUM_DIMENSIONS)); + Assertions.assertEquals("0.05", options.get(IndexOptions.HNSW_SAMPLE_VECTOR_STATS_PROBABILITY)); + validateVectorIndex(idx); + }); + } + + @Test + void createVectorIndexOnMultipleColumnsFails() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(3, float), c vector(3, float), primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b, c) PARTITION BY (p)"; + shouldFailWith(stmt, ErrorCode.UNSUPPORTED_OPERATION, "invalid number of indexed columns, only one column is supported"); + } + + @Test + void createVectorIndexOnNonVectorColumnFails() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b bigint, primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) PARTITION BY (p)"; + shouldFailWith(stmt, ErrorCode.SYNTAX_ERROR, "indexed column must be of vector type"); + } + + @Test + void createVectorIndexOnStringColumnFails() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b string, primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) PARTITION BY (p)"; + shouldFailWith(stmt, ErrorCode.SYNTAX_ERROR, "indexed column must be of vector type"); + } + + @Test + void createVectorIndexWithIncludeClauseFails() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(3, float), c bigint, d string, primary key(p)) " + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) INCLUDE (c, d) PARTITION BY (p) "; + shouldFailWith(stmt, ErrorCode.UNSUPPORTED_OPERATION, "INCLUDE clause is not supported for vector indexes"); + } + + @Test + void createVectorIndexWithIncludeClauseAndPartitionFails() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(3, float), c bigint, z bigint, primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) INCLUDE (c) PARTITION BY(z)"; + shouldFailWith(stmt, ErrorCode.UNSUPPORTED_OPERATION, "INCLUDE clause is not supported for vector indexes"); + } + + @Test + void createVectorIndexWithIncludeClauseAndOptionsFails() throws Exception { + final String stmt = "CREATE SCHEMA TEMPLATE test_template " + + "CREATE TABLE T(p bigint, b vector(3, float), c bigint, primary key(p))" + + "CREATE VECTOR INDEX MV1 USING HNSW ON T(b) INCLUDE (c) PARTITION BY (p) OPTIONS (CONNECTIVITY = 16)"; + shouldFailWith(stmt, ErrorCode.UNSUPPORTED_OPERATION, "INCLUDE clause is not supported for vector indexes"); + } } 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 traversalStrings() { - return Stream.of("TRAVERSAL ORDER PRE_ORDER", "TRAVERSAL ORDER LEVEL_ORDER"); - } - @Nonnull private static RecordLayerSchemaTemplate generateMetadata() { return RecordLayerSchemaTemplate @@ -79,208 +78,401 @@ private static RecordLayerSchemaTemplate generateMetadata() { .build(); } - @Test - void visitPredicatedExpressionTest() { - final var query = "X BETWEEN 32 AND 43"; - final MutableBoolean baseVisitorCalled = new MutableBoolean(false); - final var baseVisitor = new BaseVisitor( - new MutablePlanGenerationContext(PreparedParams.empty(), - PlanHashable.PlanHashMode.VC0, query, query, 42), - generateMetadata(), - NoOpQueryFactory.INSTANCE, - NoOpMetadataOperationsFactory.INSTANCE, - URI.create("/FDB/FRL1"), - false) { - @Nonnull - @Override - public Expression visitPredicatedExpression(@Nonnull final RelationalParser.PredicatedExpressionContext ctx) { - baseVisitorCalled.setTrue(); - return Expression.ofUnnamed(LiteralValue.ofScalar(42)); - } - }; - final var delegatingVisitor = new DelegatingVisitor<>(baseVisitor); + /** + * Generic test helper for visitor methods that return Object or void. + * + * @param query the SQL query to parse + * @param parseMethod parser method to extract context + * @param visitMethod delegating visitor method to test + * @param visitorOverride function to create a BaseVisitor with overridden method + */ + private void testVisitor(String query, + Function parseMethod, + BiConsumer, T> visitMethod, + Function visitorOverride) { + final MutableBoolean called = new MutableBoolean(false); + final var visitor = visitorOverride.apply(called); + final var delegatingVisitor = new DelegatingVisitor<>(visitor); + final var context = parseQuery(query, parseMethod); + visitMethod.accept(delegatingVisitor, context); + Assertions.assertThat(called.booleanValue()).as("Expecting the method to be called").isTrue(); + } + + /** + * Simplified helper for most common case: parse, visit, assert called. + */ + private void testSimple(String query, + Function parseMethod, + BiConsumer, T> visitMethod, + Function visitorOverride) { + testVisitor(query, parseMethod, visitMethod, visitorOverride); + } + + private T parseQuery(String query, Function parseMethod) { final var tokenSource = new RelationalLexer(new CaseInsensitiveCharStream(query)); final var parser = new RelationalParser(new CommonTokenStream(tokenSource)); - final var predicatedExpression = (RelationalParser.PredicatedExpressionContext)parser.expression(); - delegatingVisitor.visitPredicatedExpression(predicatedExpression); - Assertions.assertThat(baseVisitorCalled.booleanValue()).isTrue(); + return parseMethod.apply(parser); + } + + private BaseVisitor createBaseVisitor(String query, MutableBoolean called) { + return new BaseVisitor( + new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, query, query, 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, + URI.create("/FDB/FRL1"), false); } @Test - void visitSubscriptExpressionTest() { - final var query = "X[42]"; - final MutableBoolean baseVisitorCalled = new MutableBoolean(false); - final var baseVisitor = new BaseVisitor( - new MutablePlanGenerationContext(PreparedParams.empty(), - PlanHashable.PlanHashMode.VC0, query, query, 42), - generateMetadata(), - NoOpQueryFactory.INSTANCE, - NoOpMetadataOperationsFactory.INSTANCE, - URI.create("/FDB/FRL1"), - false) { + void visitPredicatedExpressionTest() { + testSimple("X BETWEEN 32 AND 43", + RelationalParser::expression, + (visitor, ctx) -> visitor.visitPredicatedExpression((RelationalParser.PredicatedExpressionContext) ctx), + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Nonnull + @Override + public Expression visitPredicatedExpression(@Nonnull RelationalParser.PredicatedExpressionContext ctx) { + called.setTrue(); + return Expression.ofUnnamed(LiteralValue.ofScalar(42)); + } + }); + } - @Override - public Expression visitSubscriptExpression(@Nonnull final RelationalParser.SubscriptExpressionContext ctx) { - baseVisitorCalled.setTrue(); - return Expression.ofUnnamed(LiteralValue.ofScalar(42)); - } - }; - final var delegatingVisitor = new DelegatingVisitor<>(baseVisitor); - final var tokenSource = new RelationalLexer(new CaseInsensitiveCharStream(query)); - final var parser = new RelationalParser(new CommonTokenStream(tokenSource)); - final var predicatedExpression = (RelationalParser.SubscriptExpressionContext)parser.expressionAtom(); - delegatingVisitor.visitSubscriptExpression(predicatedExpression); - Assertions.assertThat(baseVisitorCalled.booleanValue()).isTrue(); + @Test + void visitSubscriptExpressionTest() { + testSimple("X[42]", + RelationalParser::expressionAtom, + (visitor, ctx) -> visitor.visitSubscriptExpression((RelationalParser.SubscriptExpressionContext) ctx), + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Expression visitSubscriptExpression(@Nonnull RelationalParser.SubscriptExpressionContext ctx) { + called.setTrue(); + return Expression.ofUnnamed(LiteralValue.ofScalar(42)); + } + }); } @Test void visitUserDefinedScalarFunctionStatementBodyTest() { final var query = "AS testIdentifier"; - final MutableBoolean baseVisitorCalled = new MutableBoolean(false); - final var baseVisitor = new BaseVisitor( - new MutablePlanGenerationContext(PreparedParams.empty(), - PlanHashable.PlanHashMode.VC0, query, query, 42), - generateMetadata(), - NoOpQueryFactory.INSTANCE, - NoOpMetadataOperationsFactory.INSTANCE, - URI.create("/FDB/FRL1"), - false) { - + final MutableBoolean called = new MutableBoolean(false); + final var visitor = new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, query, query, 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { @Nonnull @Override - public Identifier visitUserDefinedScalarFunctionStatementBody(@Nonnull final RelationalParser.UserDefinedScalarFunctionStatementBodyContext ctx) { - baseVisitorCalled.setTrue(); + public Identifier visitUserDefinedScalarFunctionStatementBody(@Nonnull RelationalParser.UserDefinedScalarFunctionStatementBodyContext ctx) { + called.setTrue(); return Identifier.of("testIdentifier"); } }; - final var delegatingVisitor = new DelegatingVisitor<>(baseVisitor); - final var tokenSource = new RelationalLexer(new CaseInsensitiveCharStream(query)); - final var parser = new RelationalParser(new CommonTokenStream(tokenSource)); - final var routineBodyContext = parser.routineBody(); - - if (routineBodyContext instanceof RelationalParser.UserDefinedScalarFunctionStatementBodyContext) { - final var userDefinedScalarFunctionStatementBodyContext = (RelationalParser.UserDefinedScalarFunctionStatementBodyContext)routineBodyContext; - final var result = delegatingVisitor.visitUserDefinedScalarFunctionStatementBody(userDefinedScalarFunctionStatementBodyContext); - Assertions.assertThat(result).isEqualTo(Identifier.of("testIdentifier")); - } else { - Assertions.fail("Expected UserDefinedScalarFunctionStatementBodyContext but got " + routineBodyContext.getClass().getSimpleName()); - } - - Assertions.assertThat(baseVisitorCalled.booleanValue()).isTrue(); + final var delegatingVisitor = new DelegatingVisitor<>(visitor); + final var routineBodyContext = parseQuery(query, RelationalParser::routineBody); + Assertions.assertThat(routineBodyContext).isInstanceOf(RelationalParser.UserDefinedScalarFunctionStatementBodyContext.class); + final var result = delegatingVisitor.visitUserDefinedScalarFunctionStatementBody( + (RelationalParser.UserDefinedScalarFunctionStatementBodyContext) routineBodyContext); + Assertions.assertThat(result).isEqualTo(Identifier.of("testIdentifier")); + Assertions.assertThat(called.booleanValue()).isTrue(); } @Test void visitUserDefinedScalarFunctionNameTest() { - final var query = "fake query"; - final MutableBoolean baseVisitorCalled = new MutableBoolean(false); - final var baseVisitor = new BaseVisitor( - new MutablePlanGenerationContext(PreparedParams.empty(), - PlanHashable.PlanHashMode.VC0, query, query, 42), - generateMetadata(), - NoOpQueryFactory.INSTANCE, - NoOpMetadataOperationsFactory.INSTANCE, - URI.create("/FDB/FRL1"), - false) { - - @Nonnull - @Override - public String visitUserDefinedScalarFunctionName(@Nonnull final RelationalParser.UserDefinedScalarFunctionNameContext ctx) { - baseVisitorCalled.setTrue(); - return "testFunction"; - } - }; - final var delegatingVisitor = new DelegatingVisitor<>(baseVisitor); - final var tokenSource = new RelationalLexer(new CaseInsensitiveCharStream(query)); - final var parser = new RelationalParser(new CommonTokenStream(tokenSource)); - Assertions.assertThat(delegatingVisitor.visitUserDefinedScalarFunctionName(parser.userDefinedScalarFunctionName())).isEqualTo("testFunction"); - Assertions.assertThat(baseVisitorCalled.booleanValue()).isTrue(); + testSimple("fake query", + RelationalParser::userDefinedScalarFunctionName, + (visitor, ctx) -> Assertions.assertThat(visitor.visitUserDefinedScalarFunctionName(ctx)).isEqualTo("testFunction"), + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Nonnull + @Override + public String visitUserDefinedScalarFunctionName(@Nonnull RelationalParser.UserDefinedScalarFunctionNameContext ctx) { + called.setTrue(); + return "testFunction"; + } + }); } + static Stream traversalStrings() { + return Stream.of("TRAVERSAL ORDER PRE_ORDER", "TRAVERSAL ORDER LEVEL_ORDER"); + } @ParameterizedTest @MethodSource("traversalStrings") void visitTraversalExpressionTest(String query) { - final MutableBoolean baseVisitorCalled = new MutableBoolean(false); - final var baseVisitor = new BaseVisitor( - new MutablePlanGenerationContext(PreparedParams.empty(), - PlanHashable.PlanHashMode.VC0, query, query, 42), - generateMetadata(), - NoOpQueryFactory.INSTANCE, - NoOpMetadataOperationsFactory.INSTANCE, - URI.create("/FDB/FRL1"), - false) { - - public Object visitTraversalOrderClause(final RelationalParser.TraversalOrderClauseContext ctx) { - baseVisitorCalled.setTrue(); - if (query.equals("TRAVERSAL ORDER LEVEL_ORDER")) { - return RecursiveUnionExpression.TraversalStrategy.LEVEL; - } else { - return RecursiveUnionExpression.TraversalStrategy.PREORDER; - } - } - }; - - final var delegatingVisitor = new DelegatingVisitor<>(baseVisitor); - final var tokenSource = new RelationalLexer(new CaseInsensitiveCharStream(query)); - final var parser = new RelationalParser(new CommonTokenStream(tokenSource)); - final var predicatedExpression = parser.traversalOrderClause(); - delegatingVisitor.visitTraversalOrderClause(predicatedExpression); - Assertions.assertThat(baseVisitorCalled.booleanValue()).isTrue(); + testSimple(query, + RelationalParser::traversalOrderClause, + DelegatingVisitor::visitTraversalOrderClause, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, query, query, 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Object visitTraversalOrderClause(RelationalParser.TraversalOrderClauseContext ctx) { + called.setTrue(); + return query.equals("TRAVERSAL ORDER LEVEL_ORDER") + ? RecursiveUnionExpression.TraversalStrategy.LEVEL + : RecursiveUnionExpression.TraversalStrategy.PREORDER; + } + }); } @Test void visitViewDefinitionTest() { - final var query = "VIEW V AS SELECT * FROM T"; - final MutableBoolean baseVisitorCalled = new MutableBoolean(false); - final var baseVisitor = new BaseVisitor( - new MutablePlanGenerationContext(PreparedParams.empty(), - PlanHashable.PlanHashMode.VC0, query, query, 42), - generateMetadata(), - NoOpQueryFactory.INSTANCE, - NoOpMetadataOperationsFactory.INSTANCE, - URI.create("/FDB/FRL1"), - false) { - - public Object visitViewDefinition(final RelationalParser.ViewDefinitionContext ctx) { - baseVisitorCalled.setTrue(); - return null; - } - }; - - final var delegatingVisitor = new DelegatingVisitor<>(baseVisitor); - final var tokenSource = new RelationalLexer(new CaseInsensitiveCharStream(query)); - final var parser = new RelationalParser(new CommonTokenStream(tokenSource)); - final var viewDefinitionContext = parser.viewDefinition(); - delegatingVisitor.visitViewDefinition(viewDefinitionContext); - Assertions.assertThat(baseVisitorCalled.booleanValue()).isTrue(); + testSimple("VIEW V AS SELECT * FROM T", + RelationalParser::viewDefinition, + DelegatingVisitor::visitViewDefinition, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Object visitViewDefinition(RelationalParser.ViewDefinitionContext ctx) { + called.setTrue(); + return null; + } + }); } @Test void visitUserDefinedScalarFunctionCallTest() { - final var query = "myFunction(123)"; - final MutableBoolean baseVisitorCalled = new MutableBoolean(false); - final var baseVisitor = new BaseVisitor( - new MutablePlanGenerationContext(PreparedParams.empty(), - PlanHashable.PlanHashMode.VC0, query, query, 42), - generateMetadata(), - NoOpQueryFactory.INSTANCE, - NoOpMetadataOperationsFactory.INSTANCE, - URI.create("/FDB/FRL1"), - false) { + testSimple("myFunction(123)", + RelationalParser::functionCall, + (visitor, ctx) -> visitor.visitUserDefinedScalarFunctionCall((RelationalParser.UserDefinedScalarFunctionCallContext) ctx), + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Nonnull + @Override + public Expression visitUserDefinedScalarFunctionCall(@Nonnull RelationalParser.UserDefinedScalarFunctionCallContext ctx) { + called.setTrue(); + return Expression.ofUnnamed(LiteralValue.ofScalar(42)); + } + }); + } - @Nonnull - @Override - public Expression visitUserDefinedScalarFunctionCall(@Nonnull final RelationalParser.UserDefinedScalarFunctionCallContext ctx) { - baseVisitorCalled.setTrue(); - return Expression.ofUnnamed(LiteralValue.ofScalar(42)); - } - }; - final var delegatingVisitor = new DelegatingVisitor<>(baseVisitor); - final var tokenSource = new RelationalLexer(new CaseInsensitiveCharStream(query)); - final var parser = new RelationalParser(new CommonTokenStream(tokenSource)); - final var functionCallExpression = parser.functionCall(); - final var userDefinedScalarFunctionCall = (RelationalParser.UserDefinedScalarFunctionCallContext)functionCallExpression; - delegatingVisitor.visitUserDefinedScalarFunctionCall(userDefinedScalarFunctionCall); - Assertions.assertThat(baseVisitorCalled.booleanValue()).isTrue(); + @Test + void visitIndexOptionsTest() { + testSimple("OPTIONS (LEGACY_EXTREMUM_EVER)", + RelationalParser::indexOptions, + DelegatingVisitor::visitIndexOptions, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Object visitIndexOptions(@Nonnull RelationalParser.IndexOptionsContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitIndexOptionTest() { + testSimple("LEGACY_EXTREMUM_EVER", + RelationalParser::indexOption, + DelegatingVisitor::visitIndexOption, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Object visitIndexOption(@Nonnull RelationalParser.IndexOptionContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitVectorIndexDefinitionTest() { + final var query = "VECTOR INDEX myIndex USING HNSW ON table1 (R)"; + testVisitor(query, + RelationalParser::indexDefinition, + (visitor, ctx) -> { + Assertions.assertThat(ctx).isInstanceOf(RelationalParser.VectorIndexDefinitionContext.class); + visitor.visitVectorIndexDefinition((RelationalParser.VectorIndexDefinitionContext) ctx); + }, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, query, query, 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + @SuppressWarnings({"NullableProblems", "DataFlowIssue"}) + public RecordLayerIndex visitVectorIndexDefinition(@Nonnull RelationalParser.VectorIndexDefinitionContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitPartitionClauseTest() { + final var query = "PARTITION BY (col1)"; + testVisitor(query, + RelationalParser::partitionClause, + (visitor, ctx) -> { + Assertions.assertThat(ctx).isInstanceOf(RelationalParser.PartitionClauseContext.class); + visitor.visitPartitionClause(ctx); + }, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, query, query, 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + + @Override + public Object visitPartitionClause(final RelationalParser.PartitionClauseContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitIndexColumnListTest() { + testSimple("(col1, col2)", + RelationalParser::indexColumnList, + DelegatingVisitor::visitIndexColumnList, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + @SuppressWarnings({"NullableProblems", "DataFlowIssue"}) + public Object visitIndexColumnList(@Nonnull RelationalParser.IndexColumnListContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitIncludeClauseTest() { + testSimple("INCLUDE (col1, col2)", + RelationalParser::includeClause, + DelegatingVisitor::visitIncludeClause, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + @SuppressWarnings({"NullableProblems", "DataFlowIssue"}) + public Object visitIncludeClause(@Nonnull RelationalParser.IncludeClauseContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitIndexOnSourceDefinitionTest() { + final var query = "INDEX myIndex ON table1 (R)"; + testVisitor(query, + RelationalParser::indexDefinition, + (visitor, ctx) -> { + Assertions.assertThat(ctx).isInstanceOf(RelationalParser.IndexOnSourceDefinitionContext.class); + visitor.visitIndexOnSourceDefinition((RelationalParser.IndexOnSourceDefinitionContext) ctx); + }, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, query, query, 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + @SuppressWarnings({"NullableProblems", "DataFlowIssue"}) + public RecordLayerIndex visitIndexOnSourceDefinition(@Nonnull RelationalParser.IndexOnSourceDefinitionContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitIndexTypeTest() { + testSimple("UNIQUE", + RelationalParser::indexType, + DelegatingVisitor::visitIndexType, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Object visitIndexType(@Nonnull RelationalParser.IndexTypeContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitIndexColumnSpecTest() { + testSimple("col1 ASC", + RelationalParser::indexColumnSpec, + DelegatingVisitor::visitIndexColumnSpec, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + @SuppressWarnings({"NullableProblems", "DataFlowIssue"}) + public Object visitIndexColumnSpec(@Nonnull RelationalParser.IndexColumnSpecContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitIndexAsSelectDefinitionTest() { + final var query = "INDEX myIndex AS SELECT * FROM table1"; + testVisitor(query, + RelationalParser::indexDefinition, + (visitor, ctx) -> { + Assertions.assertThat(ctx).isInstanceOf(RelationalParser.IndexAsSelectDefinitionContext.class); + visitor.visitIndexAsSelectDefinition((RelationalParser.IndexAsSelectDefinitionContext) ctx); + }, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, query, query, 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + @SuppressWarnings({"NullableProblems", "DataFlowIssue"}) + public RecordLayerIndex visitIndexAsSelectDefinition(@Nonnull RelationalParser.IndexAsSelectDefinitionContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitOrderClauseTest() { + testSimple("ASC NULLS FIRST", + RelationalParser::orderClause, + DelegatingVisitor::visitOrderClause, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Object visitOrderClause(@Nonnull RelationalParser.OrderClauseContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitVectorIndexOptionsTest() { + testSimple("OPTIONS (EF_CONSTRUCTION = 100)", + RelationalParser::vectorIndexOptions, + DelegatingVisitor::visitVectorIndexOptions, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Object visitVectorIndexOptions(@Nonnull RelationalParser.VectorIndexOptionsContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitHnswMetricTest() { + testSimple("EUCLIDEAN_METRIC", + RelationalParser::hnswMetric, + DelegatingVisitor::visitHnswMetric, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Object visitHnswMetric(@Nonnull RelationalParser.HnswMetricContext ctx) { + called.setTrue(); + return null; + } + }); + } + + @Test + void visitVectorIndexOptionTest() { + testSimple("EF_CONSTRUCTION = 100", + RelationalParser::vectorIndexOption, + DelegatingVisitor::visitVectorIndexOption, + called -> new BaseVisitor(new MutablePlanGenerationContext(PreparedParams.empty(), PlanHashable.PlanHashMode.VC0, "", "", 42), + generateMetadata(), NoOpQueryFactory.INSTANCE, NoOpMetadataOperationsFactory.INSTANCE, URI.create("/FDB/FRL1"), false) { + @Override + public Object visitVectorIndexOption(@Nonnull RelationalParser.VectorIndexOptionContext ctx) { + called.setTrue(); + return null; + } + }); } } diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/ExplainTests.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/ExplainTests.java index 4e4df8120f..03d3d79d4f 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/ExplainTests.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/ExplainTests.java @@ -49,8 +49,8 @@ public class ExplainTests { " CREATE TYPE AS STRUCT ReviewerStats (start_date bigint, school_name string, hometown string)" + " CREATE TABLE RestaurantComplexRecord (rest_no bigint, name string, location Location, reviews RestaurantComplexReview ARRAY, tags RestaurantTag array, customer string array, encoded_bytes bytes, 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 RestaurantComplexRecord" + - " CREATE INDEX reviewer_name_idx as select name from RestaurantReviewer" + + " CREATE INDEX record_name_idx ON RestaurantComplexRecord(name)" + + " CREATE INDEX reviewer_name_idx ON RestaurantReviewer(name)" + " CREATE INDEX mv1 AS SELECT R.rating from RestaurantComplexRecord AS Rec, (select rating from Rec.reviews) R" + " CREATE INDEX mv2 AS SELECT endo.\"endorsementText\" FROM RestaurantComplexRecord rec, (SELECT X.\"endorsementText\" FROM rec.reviews rev, (SELECT \"endorsementText\" from rev.endorsements) X) endo"; diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/GroupByQueryTests.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/GroupByQueryTests.java index 2cd1e84930..2020ef11e1 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/GroupByQueryTests.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/GroupByQueryTests.java @@ -53,7 +53,7 @@ public GroupByQueryTests() { void groupByWithScanLimit() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b, c from t1 order by a, b, c"; + "CREATE INDEX idx1 ON t1(a, b, c)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var conn = ddl.setSchemaAndGetConnection()) { Continuation continuation = null; @@ -119,7 +119,7 @@ void groupByWithScanLimit() throws Exception { void groupByWithRowLimit() throws Exception { final String schemaTemplate = "create table t1(pk bigint, a bigint, b bigint, c bigint, primary key(pk))\n" + - "create index i1 as select a from t1"; + "create index i1 ON t1(a)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var conn = ddl.setSchemaAndGetConnection()) { conn.setOption(Options.Name.MAX_ROWS, 1); @@ -199,7 +199,7 @@ void isNullPredicateUsesGroupIndex() throws Exception { void groupByClauseWithPredicateWorks() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b, c from t1 order by a, b, c"; + "CREATE INDEX idx1 ON t1(a, b, c)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 20); @@ -249,7 +249,7 @@ void groupByClauseWithPredicateMultipleAggregationsWorks() throws Exception { void groupByClauseWorks() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b from T1 order by a, b"; + "CREATE INDEX idx1 ON T1(a, b)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 20); @@ -278,7 +278,7 @@ void groupByClauseWorks() throws Exception { void groupByClauseWorksWithSubquery() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b from T1 order by a, b"; + "CREATE INDEX idx1 ON T1(a, b)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 20); @@ -307,7 +307,7 @@ void groupByClauseWorksWithSubquery() throws Exception { void groupByClauseWorksWithSubqueryAliases() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b from T1 order by a, b"; + "CREATE INDEX idx1 ON T1(a, b)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 20); @@ -336,7 +336,7 @@ void groupByClauseWorksWithSubqueryAliases() throws Exception { void groupByClauseWorksWithSubqueryAliasesComplex() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b from t1 order by a, b"; + "CREATE INDEX idx1 ON t1(a, b)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 20); @@ -365,7 +365,7 @@ void groupByClauseWorksWithSubqueryAliasesComplex() throws Exception { void groupByClauseWorksDifferentAggregations() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b from t1 order by a, b"; + "CREATE INDEX idx1 ON t1(a, b)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 20); @@ -394,7 +394,7 @@ void groupByClauseWorksDifferentAggregations() throws Exception { void groupByClauseWorksComplexGrouping() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b from t1 order by a, b"; + "CREATE INDEX idx1 ON t1(a, b)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 20); @@ -423,7 +423,7 @@ void groupByClauseWorksComplexGrouping() throws Exception { void groupByClauseSingleGroup() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b from t1 order by a, b"; + "CREATE INDEX idx1 ON t1(a, b)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 2000); @@ -444,7 +444,7 @@ void groupByClauseSingleGroup() throws Exception { void groupByClauseWithoutGroupingColumnsInProjectionList() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b from t1 order by a, b"; + "CREATE INDEX idx1 ON t1(a, b)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 2000); @@ -465,7 +465,7 @@ void groupByClauseWithoutGroupingColumnsInProjectionList() throws Exception { void groupByClauseWithoutAggregationsInProjectionList() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b from t1 order by a, b"; + "CREATE INDEX idx1 ON t1(a, b)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 2000); @@ -486,7 +486,7 @@ void groupByClauseWithoutAggregationsInProjectionList() throws Exception { void groupByInSubSelectWorks() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b from t1 order by a, b"; + "CREATE INDEX idx1 ON t1(a, b)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 2000); @@ -507,7 +507,7 @@ void groupByInSubSelectWorks() throws Exception { void groupByConstantColumn() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b from t1 order by a, b"; + "CREATE INDEX idx1 ON t1(a, b)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 2000); @@ -528,7 +528,7 @@ void groupByConstantColumn() throws Exception { void aggregationWithoutGroupBy() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b from t1 order by a, b"; + "CREATE INDEX idx1 ON t1(a, b)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 2000); @@ -549,7 +549,7 @@ void aggregationWithoutGroupBy() throws Exception { void nestedGroupByStatements() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b from t1 order by a, b"; + "CREATE INDEX idx1 ON t1(a, b)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 2000); @@ -570,7 +570,7 @@ void nestedGroupByStatements() throws Exception { void groupByClauseWorksComplexAggregations() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b, c from t1 order by a, b, c"; + "CREATE INDEX idx1 ON t1(a, b, c)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 20); @@ -599,7 +599,7 @@ void groupByClauseWorksComplexAggregations() throws Exception { void groupByClauseWithNestedAggregationsIsSupported() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b, c from t1 order by a, b, c"; + "CREATE INDEX idx1 ON t1(a, b, c)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 20); @@ -628,7 +628,7 @@ void groupByClauseWithNestedAggregationsIsSupported() throws Exception { void expansionOfStarWorks() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a from t1"; + "CREATE INDEX idx1 ON t1(a)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 20); @@ -655,7 +655,7 @@ void expansionOfStarWorks() throws Exception { void groupByClauseWithNamedGroupingColumns() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b, c from t1 order by a, b, c"; + "CREATE INDEX idx1 ON t1(a, b, c)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 20); @@ -685,7 +685,7 @@ void groupByOverJoinWorks() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + "CREATE TABLE T2(pk bigint, x bigint, y bigint, z bigint, primary key(pk))" + - "CREATE INDEX idx1 AS SELECT B FROM T1"; + "CREATE INDEX idx1 ON T1(B)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 20); @@ -712,7 +712,7 @@ void groupByOverJoinWorks() throws Exception { void groupByWithNamedGroups() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))" + - "CREATE INDEX idx1 as select a, b, c from t1 order by a, b, c"; + "CREATE INDEX idx1 ON t1(a, b, c)"; try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { insertT1Record(statement, 2, 1, 1, 20); diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/PreparedStatementTests.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/PreparedStatementTests.java index 6487a2afc9..d963fe7525 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/PreparedStatementTests.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/PreparedStatementTests.java @@ -72,8 +72,8 @@ public class PreparedStatementTests { " CREATE TYPE AS STRUCT ReviewerStats (start_date bigint, school_name string, hometown string)" + " CREATE TABLE RestaurantComplexRecord (rest_no bigint, name string, location Location, reviews RestaurantComplexReview ARRAY, tags RestaurantTag array, customer string array, encoded_bytes bytes, key bytes, PRIMARY KEY(rest_no))" + " CREATE TABLE RestaurantReviewer (id bigint, name string, email string, stats ReviewerStats, secrets bytes array, PRIMARY KEY(id))" + - " CREATE INDEX record_name_idx as select name from RestaurantComplexRecord" + - " CREATE INDEX reviewer_name_idx as select name from RestaurantReviewer" + + " CREATE INDEX record_name_idx ON RestaurantComplexRecord(name)" + + " CREATE INDEX reviewer_name_idx ON RestaurantReviewer(name)" + " CREATE INDEX mv1 AS SELECT R.rating from RestaurantComplexRecord AS Rec, (select rating from Rec.reviews) R" + " CREATE INDEX mv2 AS SELECT endo.\"endorsementText\" FROM RestaurantComplexRecord rec, (SELECT X.\"endorsementText\" FROM rec.reviews rev, (SELECT \"endorsementText\" from rev.endorsements) X) endo"; diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/QueryWithContinuationTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/QueryWithContinuationTest.java index 53f75a069d..c441f3294d 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/QueryWithContinuationTest.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/QueryWithContinuationTest.java @@ -55,8 +55,8 @@ public class QueryWithContinuationTest { " CREATE TYPE AS STRUCT ReviewerStats (start_date bigint, school_name string, hometown string)" + " CREATE TABLE RestaurantComplexRecord (rest_no bigint, name string, location Location, reviews RestaurantComplexReview ARRAY, tags RestaurantTag array, customer string array, encoded_bytes bytes, 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 RestaurantComplexRecord" + - " CREATE INDEX reviewer_name_idx as select name from RestaurantReviewer" + + " CREATE INDEX record_name_idx ON RestaurantComplexRecord(name)" + + " CREATE INDEX reviewer_name_idx ON RestaurantReviewer(name)" + " CREATE INDEX mv1 AS SELECT R.rating from RestaurantComplexRecord AS Rec, (select rating from Rec.reviews) R" + " CREATE INDEX mv2 AS SELECT endo.\"endorsementText\" FROM RestaurantComplexRecord rec, (SELECT X.\"endorsementText\" FROM rec.reviews rev, (SELECT \"endorsementText\" from rev.endorsements) X) endo"; diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/StandardQueryTests.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/StandardQueryTests.java index 615bb8fb55..484a5983a8 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/StandardQueryTests.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/StandardQueryTests.java @@ -87,8 +87,8 @@ public class StandardQueryTests { " CREATE TYPE AS STRUCT ReviewerStats (start_date bigint, school_name string, hometown string)" + " CREATE TABLE RestaurantComplexRecord (rest_no bigint, name string, location Location, reviews RestaurantComplexReview ARRAY, tags RestaurantTag array, customer string array, encoded_bytes bytes, 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 RestaurantComplexRecord" + - " CREATE INDEX reviewer_name_idx as select name from RestaurantReviewer" + + " CREATE INDEX record_name_idx ON RestaurantComplexRecord(name)" + + " CREATE INDEX reviewer_name_idx ON RestaurantReviewer(name)" + " CREATE INDEX mv1 AS SELECT R.rating from RestaurantComplexRecord AS Rec, (select rating from Rec.reviews) R" + " CREATE INDEX mv2 AS SELECT endo.\"endorsementText\" FROM RestaurantComplexRecord rec, (SELECT X.\"endorsementText\" FROM rec.reviews rev, (SELECT \"endorsementText\" from rev.endorsements) X) endo"; @@ -100,8 +100,8 @@ public class StandardQueryTests { " CREATE TYPE AS STRUCT ReviewerStats (start_date bigint, school_name string, hometown string)" + " CREATE TABLE RestaurantComplexRecord (rest_no bigint, name string, location Location, reviews RestaurantComplexReview ARRAY NOT NULL, tags RestaurantTag array NOT NULL, customer string array NOT NULL, encoded_bytes bytes, 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 RestaurantComplexRecord" + - " CREATE INDEX reviewer_name_idx as select name from RestaurantReviewer" + + " CREATE INDEX record_name_idx ON RestaurantComplexRecord(name)" + + " CREATE INDEX reviewer_name_idx ON RestaurantReviewer(name)" + " CREATE INDEX mv1 AS SELECT R.rating from RestaurantComplexRecord AS Rec, (select rating from Rec.reviews) R" + " CREATE INDEX mv2 AS SELECT endo.\"endorsementText\" FROM RestaurantComplexRecord rec, (SELECT X.\"endorsementText\" FROM rec.reviews rev, (SELECT \"endorsementText\" from rev.endorsements) X) endo"; diff --git a/fdb-relational-core/src/testFixtures/java/com/apple/foundationdb/relational/utils/TestSchemas.java b/fdb-relational-core/src/testFixtures/java/com/apple/foundationdb/relational/utils/TestSchemas.java index 7e82910636..e1d9b29686 100644 --- a/fdb-relational-core/src/testFixtures/java/com/apple/foundationdb/relational/utils/TestSchemas.java +++ b/fdb-relational-core/src/testFixtures/java/com/apple/foundationdb/relational/utils/TestSchemas.java @@ -65,7 +65,7 @@ public static String restaurantWithCoveringIndex() { "CREATE TABLE Card (id bigint, suit suit, rank bigint, PRIMARY KEY(id))" + "CREATE TABLE Card_Nested (id bigint, info SuitAndRank, PRIMARY KEY(id))" + "CREATE TABLE Card_Array (id bigint, collection SuitAndRank array, PRIMARY KEY(id))" + - "CREATE INDEX suit_idx AS SELECT suit FROM Card ORDER BY suit"; + "CREATE INDEX suit_idx ON Card(suit)"; @Nonnull public static String playingCard() { diff --git a/yaml-tests/src/test/java/DocumentationQueriesTests.java b/yaml-tests/src/test/java/DocumentationQueriesTests.java index 9ebdef72fc..b9376db885 100644 --- a/yaml-tests/src/test/java/DocumentationQueriesTests.java +++ b/yaml-tests/src/test/java/DocumentationQueriesTests.java @@ -49,6 +49,11 @@ void inOperatorQueriesTests(YamlTest.Runner runner) throws Exception { runner.runYamsql(PREFIX + "/in-operator-queries.yamsql"); } + @TestTemplate + void indexDocumentationQueriesTests(YamlTest.Runner runner) throws Exception { + runner.runYamsql(PREFIX + "/index-documentation-queries.yamsql"); + } + @TestTemplate void isDistinctFromOperatorQueriesTests(YamlTest.Runner runner) throws Exception { runner.runYamsql(PREFIX + "/is-distinct-from-operator-queries.yamsql"); diff --git a/yaml-tests/src/test/java/YamlIntegrationTests.java b/yaml-tests/src/test/java/YamlIntegrationTests.java index fc11024c93..05edc35969 100644 --- a/yaml-tests/src/test/java/YamlIntegrationTests.java +++ b/yaml-tests/src/test/java/YamlIntegrationTests.java @@ -207,6 +207,21 @@ public void maxRows(YamlTest.Runner runner) throws Exception { runner.runYamsql("maxRows.yamsql"); } + @TestTemplate + public void indexDdl(YamlTest.Runner runner) throws Exception { + runner.runYamsql("index-ddl.yamsql"); + } + + @TestTemplate + public void indexDdlValuesOnly(YamlTest.Runner runner) throws Exception { + runner.runYamsql("index-ddl-values-only.yamsql"); + } + + @TestTemplate + public void indexDdlAggregatesOnly(YamlTest.Runner runner) throws Exception { + runner.runYamsql("index-ddl-aggregates-only.yamsql"); + } + @TestTemplate public void nested(YamlTest.Runner runner) throws Exception { runner.runYamsql("nested-tests.yamsql"); diff --git a/yaml-tests/src/test/resources/documentation-queries/index-documentation-queries.yamsql b/yaml-tests/src/test/resources/documentation-queries/index-documentation-queries.yamsql new file mode 100644 index 0000000000..af5acd6433 --- /dev/null +++ b/yaml-tests/src/test/resources/documentation-queries/index-documentation-queries.yamsql @@ -0,0 +1,204 @@ +--- +options: + supported_version: !current_version +--- +schema_template: + create table products(id bigint, name string, category string, price bigint, rating bigint, supplier string, stock bigint, primary key(id)) + create table sales(id bigint, category string, region string, amount bigint, quantity bigint, primary key(id)) + create index idx_price as select price from products order by price + create index idx_category_price as select category, price, name from products order by category, price + create index idx_sales_by_category as select category, sum(amount) from sales group by category + create index idx_count_by_region as select region, count(*) from sales group by region + create index idx_max_by_category as select category, max(amount) from sales group by category order by category, max(amount) + create index idx_price_desc as select price from products order by price desc + create index idx_category_desc_price_asc as select category, price from products order by category desc, price asc + create index idx_rating_nulls_last as select rating from products order by rating asc nulls last + create index idx_on_price on products(price) + create index idx_on_category_price on products(category, price) + create index idx_on_category_price_covering on products(category, price) include(name, stock) + create index idx_on_category_desc on products(category desc, price asc) + create index idx_on_rating_nulls_last on products(rating asc nulls last) + create index idx_on_supplier_desc_nulls_first on products(supplier desc nulls first) + create index idx_on_stock_nulls_last on products(stock nulls last) + create index idx_on_complex on products(category asc nulls first, price desc nulls last, name asc) +--- +setup: + steps: + # Setup products data + - query: insert into products + values (1, 'Widget A', 'Electronics', 100, 5, 'SupplierA', 50), + (2, 'Widget B', 'Electronics', 150, 4, 'SupplierB', 30), + (3, 'Gadget X', 'Electronics', 200, NULL, 'SupplierA', 20), + (4, 'Tool A', 'Hardware', 80, 3, NULL, 100), + (5, 'Tool B', 'Hardware', 120, NULL, 'SupplierC', NULL) + + # Setup sales data + - query: insert into sales + values (1, 'Electronics', 'North', 500, 10), + (2, 'Electronics', 'South', 300, 5), + (3, 'Hardware', 'North', 200, 3), + (4, 'Electronics', 'North', 700, 15), + (5, 'Hardware', 'South', 400, 8) +--- +test_block: + name: index-documentation-tests + preset: single_repetition_ordered + tests: + # INDEX AS SELECT - Simple Value Index + - + - query: SELECT price + FROM products + ORDER BY price + - result: [{price: 80}, + {price: 100}, + {price: 120}, + {price: 150}, + {price: 200}] + + # INDEX AS SELECT - Covering Value Index + - + - query: SELECT category, price, name + FROM products + ORDER BY category, price + - result: [{category: "Electronics", price: 100, name: "Widget A"}, + {category: "Electronics", price: 150, name: "Widget B"}, + {category: "Electronics", price: 200, name: "Gadget X"}, + {category: "Hardware", price: 80, name: "Tool A"}, + {category: "Hardware", price: 120, name: "Tool B"}] + + # INDEX AS SELECT - Aggregate Index (SUM) + - + - query: SELECT category, SUM(amount) as total + FROM sales + GROUP BY category + ORDER BY category + - result: [{category: "Electronics", total: 1500}, + {category: "Hardware", total: 600}] + + # INDEX AS SELECT - Aggregate Index (COUNT) + - + - query: SELECT region, COUNT(*) as cnt + FROM sales + GROUP BY region + ORDER BY region + - result: [{region: "North", cnt: 3}, + {region: "South", cnt: 2}] + + # INDEX AS SELECT - Aggregate Index (MAX) + - + - query: SELECT category, MAX(amount) as max_amt + FROM sales + GROUP BY category + ORDER BY category, MAX(amount) + - result: [{category: "Electronics", max_amt: 700}, + {category: "Hardware", max_amt: 400}] + + # INDEX AS SELECT - Descending Order + - + - query: SELECT price + FROM products + ORDER BY price DESC + - result: [{price: 200}, + {price: 150}, + {price: 120}, + {price: 100}, + {price: 80}] + + # INDEX AS SELECT - Mixed Ordering + - + - query: SELECT category, price + FROM products + ORDER BY category DESC, price ASC + - result: [{category: "Hardware", price: 80}, + {category: "Hardware", price: 120}, + {category: "Electronics", price: 100}, + {category: "Electronics", price: 150}, + {category: "Electronics", price: 200}] + + # INDEX ON - Simple Value Index + - + - query: SELECT price + FROM products + ORDER BY price + - result: [{price: 80}, + {price: 100}, + {price: 120}, + {price: 150}, + {price: 200}] + + # INDEX ON - Multi-Column Index + - + - query: SELECT category, price + FROM products + ORDER BY category, price + - result: [{category: "Electronics", price: 100}, + {category: "Electronics", price: 150}, + {category: "Electronics", price: 200}, + {category: "Hardware", price: 80}, + {category: "Hardware", price: 120}] + + # INDEX ON - Covering Index with INCLUDE + - + - query: SELECT category, price, name, stock + FROM products + ORDER BY category, price + - result: [{category: "Electronics", price: 100, name: "Widget A", stock: 50}, + {category: "Electronics", price: 150, name: "Widget B", stock: 30}, + {category: "Electronics", price: 200, name: "Gadget X", stock: 20}, + {category: "Hardware", price: 80, name: "Tool A", stock: 100}, + {category: "Hardware", price: 120, name: "Tool B", stock: !null _}] + + # INDEX ON - Custom Ordering (DESC/ASC) + - + - query: SELECT category, price + FROM products + ORDER BY category DESC, price ASC + - result: [{category: "Hardware", price: 80}, + {category: "Hardware", price: 120}, + {category: "Electronics", price: 100}, + {category: "Electronics", price: 150}, + {category: "Electronics", price: 200}] + + # INDEX ON - NULL Ordering (ASC NULLS LAST) + - + - query: SELECT rating + FROM products + ORDER BY rating ASC NULLS LAST + - result: [{rating: 3}, + {rating: 4}, + {rating: 5}, + {rating: !null _}, + {rating: !null _}] + + # INDEX ON - NULL Ordering (DESC NULLS FIRST) + - + - query: SELECT supplier + FROM products + ORDER BY supplier DESC NULLS FIRST + - result: [{supplier: !null _}, + {supplier: "SupplierC"}, + {supplier: "SupplierB"}, + {supplier: "SupplierA"}, + {supplier: "SupplierA"}] + + # INDEX ON - NULL Semantics Only (implicit ASC) + - + - query: SELECT stock + FROM products + ORDER BY stock NULLS LAST + - result: [{stock: 20}, + {stock: 30}, + {stock: 50}, + {stock: 100}, + {stock: !null _}] + + # INDEX ON - Mixed Ordering Across Multiple Columns + - + - query: SELECT category, price, name + FROM products + ORDER BY category ASC NULLS FIRST, price DESC NULLS LAST, name ASC + - result: [{category: "Electronics", price: 200, name: "Gadget X"}, + {category: "Electronics", price: 150, name: "Widget B"}, + {category: "Electronics", price: 100, name: "Widget A"}, + {category: "Hardware", price: 120, name: "Tool B"}, + {category: "Hardware", price: 80, name: "Tool A"}] diff --git a/yaml-tests/src/test/resources/index-ddl-aggregates-only.metrics.binpb b/yaml-tests/src/test/resources/index-ddl-aggregates-only.metrics.binpb new file mode 100644 index 0000000000..d6ec0c7b4f --- /dev/null +++ b/yaml-tests/src/test/resources/index-ddl-aggregates-only.metrics.binpb @@ -0,0 +1,353 @@ + + +sum_aggregate_single_groupingeEXPLAIN select category, sum(amount) as total from sales_mat_view group by category order by category + (08b@ +sAISCAN(IDX_MV_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS TOTAL) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_MV_SUM_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +sum_aggregate_single_groupingeEXPLAIN select category, sum(amount) as total from sales_index_on group by category order by category + ѱ(08b@ +tAISCAN(IDX_IOT_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS TOTAL) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_SUM_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +sum_aggregate_single_grouping_EXPLAIN select region, sum(amount) as total from sales_mat_view group by region order by region + (w08$@oAISCAN(IDX_MV_SUM_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS TOTAL) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS REGION, q6._1 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS REGION, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_MV_SUM_BY_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +sum_aggregate_single_grouping_EXPLAIN select region, sum(amount) as total from sales_index_on group by region order by region +F Ѵ9(w0G8$@pAISCAN(IDX_IOT_SUM_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS TOTAL) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS REGION, q6._1 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS REGION, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_SUM_BY_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +sum_aggregate_multi_grouping}EXPLAIN select category, region, sum(amount) as total from sales_mat_view group by category, region order by category, region +؁ Ջa(~08E@AISCAN(IDX_MV_CAT_REGION_SUM <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS REGION, q6._2 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, STRING AS REGION, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1, LONG AS _2)" ]; + 3 [ label=<
Index
IDX_MV_CAT_REGION_SUM
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +sum_aggregate_multi_grouping}EXPLAIN select category, region, sum(amount) as total from sales_index_on group by category, region order by category, region + d(~08E@AISCAN(IDX_IOT_CAT_REGION_SUM <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS REGION, q6._2 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, STRING AS REGION, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1, LONG AS _2)" ]; + 3 [ label=<
Index
IDX_IOT_CAT_REGION_SUM
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +sum_aggregate_multi_groupingEXPLAIN select category, region, sum(amount) as total from sales_mat_view where category = 'Electronics' group by category, region order by region + j(08G@AISCAN(IDX_MV_CAT_REGION_SUM [EQUALS promote(@c17 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS REGION, q6._2 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, STRING AS REGION, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c17 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1, LONG AS _2)" ]; + 3 [ label=<
Index
IDX_MV_CAT_REGION_SUM
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +sum_aggregate_multi_groupingEXPLAIN select category, region, sum(amount) as total from sales_index_on where category = 'Electronics' group by category, region order by region +s O(08G@AISCAN(IDX_IOT_CAT_REGION_SUM [EQUALS promote(@c17 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS REGION, q6._2 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, STRING AS REGION, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c17 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1, LONG AS _2)" ]; + 3 [ label=<
Index
IDX_IOT_CAT_REGION_SUM
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +sum_aggregate_three_groupingEXPLAIN select category, region, product_id, sum(amount) as total from sales_mat_view group by category, region, product_id order by category, region, product_id +H ݅<(d0.8@AISCAN(IDX_MV_SUM_BY_ALL <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: KEY:[2], _3: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS PRODUCT_ID, _._3 AS TOTAL) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS REGION, q6._2 AS PRODUCT_ID, q6._3 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, STRING AS REGION, LONG AS PRODUCT_ID, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1, LONG AS _2, LONG AS _3)" ]; + 3 [ label=<
Index
IDX_MV_SUM_BY_ALL
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +sum_aggregate_three_groupingEXPLAIN select category, region, product_id, sum(amount) as total from sales_index_on group by category, region, product_id order by category, region, product_id +H =(d0)8@AISCAN(IDX_IOT_SUM_BY_ALL <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: KEY:[2], _3: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS PRODUCT_ID, _._3 AS TOTAL) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS REGION, q6._2 AS PRODUCT_ID, q6._3 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, STRING AS REGION, LONG AS PRODUCT_ID, LONG AS TOTAL)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1, LONG AS _2, LONG AS _3)" ]; + 3 [ label=<
Index
IDX_IOT_SUM_BY_ALL
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +x +count_star_aggregate`EXPLAIN select category, count(*) as cnt from sales_mat_view group by category order by category +> )(n0ٵ88@sAISCAN(IDX_MV_COUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS CNT) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS CNT)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS CNT)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_MV_COUNT_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +x +count_star_aggregate`EXPLAIN select category, count(*) as cnt from sales_index_on group by category order by category += &(n0Ѓ88@tAISCAN(IDX_IOT_COUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS CNT) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS CNT)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS CNT)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_COUNT_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +{ +count_column_aggregateaEXPLAIN select region, count(quantity) as cnt from sales_mat_view group by region order by region +0 "(h0߾^8$@sAISCAN(IDX_MV_COUNT_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS CNT) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS REGION, q6._1 AS CNT)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS REGION, LONG AS CNT)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_MV_COUNT_QTY_BY_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +{ +count_column_aggregateaEXPLAIN select region, count(quantity) as cnt from sales_index_on group by region order by region +0 "(h0S8$@tAISCAN(IDX_IOT_COUNT_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS CNT) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS REGION, q6._1 AS CNT)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS REGION, LONG AS CNT)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_COUNT_QTY_BY_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +aggregate_with_havingEXPLAIN select category, sum(amount) as total from sales_mat_view group by category having sum(amount) > 2000 order by category +籛 ߅q(08o@ +AISCAN(IDX_MV_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | FILTER _._1 GREATER_THAN promote(@c21 AS LONG) | MAP (_._0 AS CATEGORY, _._1 AS TOTAL)digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS TOTAL)" ]; + 2 [ label=<
Predicate Filter
WHERE q6._1 GREATER_THAN promote(@c21 AS LONG)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 4 [ label=<
Index
IDX_MV_SUM_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 3 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +aggregate_with_havingEXPLAIN select category, sum(amount) as total from sales_index_on group by category having sum(amount) > 2000 order by category + чr(08o@ +AISCAN(IDX_IOT_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | FILTER _._1 GREATER_THAN promote(@c21 AS LONG) | MAP (_._0 AS CATEGORY, _._1 AS TOTAL)digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS TOTAL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS TOTAL)" ]; + 2 [ label=<
Predicate Filter
WHERE q6._1 GREATER_THAN promote(@c21 AS LONG)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 4 [ label=<
Index
IDX_IOT_SUM_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 3 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +x + min_aggregategEXPLAIN select category, min(amount) as min_amt from sales_mat_view group by category order by category +ِC .(z08H@zAISCAN(IDX_MV_MIN_AMOUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_AMT) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS MIN_AMT)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS MIN_AMT)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_MV_MIN_AMOUNT_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +x + min_aggregategEXPLAIN select category, min(amount) as min_amt from sales_index_on group by category order by category +Ì; )(u0f8:@{AISCAN(IDX_IOT_MIN_AMOUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_AMT) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS MIN_AMT)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS MIN_AMT)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_MIN_AMOUNT_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +r + max_aggregateaEXPLAIN select region, max(amount) as max_amt from sales_mat_view group by region order by region +Ȼ) (o0.8&@vAISCAN(IDX_MV_MAX_AMOUNT_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1]]) | MAP (_._0 AS REGION, _._1 AS MAX_AMT) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS REGION, q6._1 AS MAX_AMT)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS REGION, LONG AS MAX_AMT)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_MV_MAX_AMOUNT_BY_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +r + max_aggregateaEXPLAIN select region, max(amount) as max_amt from sales_index_on group by region order by region +* (o078&@wAISCAN(IDX_IOT_MAX_AMOUNT_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1]]) | MAP (_._0 AS REGION, _._1 AS MAX_AMT) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS REGION, q6._1 AS MAX_AMT)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS REGION, LONG AS MAX_AMT)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_MAX_AMOUNT_BY_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +min_ever_aggregatenEXPLAIN select category, min_ever(quantity) as min_qty from sales_mat_view group by category order by category +( (l0E82@~AISCAN(IDX_MV_MIN_EVER_QTY_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_QTY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS MIN_QTY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS MIN_QTY)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_MV_MIN_EVER_QTY_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +min_ever_aggregatenEXPLAIN select category, min_ever(quantity) as min_qty from sales_index_on group by category order by category +( Ǩ(l0֠M82@AISCAN(IDX_IOT_MIN_EVER_QTY_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_QTY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._1 AS MIN_QTY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS MIN_QTY)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_MIN_EVER_QTY_BY_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +~ +max_ever_aggregatehEXPLAIN select region, max_ever(quantity) as max_qty from sales_mat_view group by region order by region +ϻ! (g0+8!@zAISCAN(IDX_MV_MAX_EVER_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS MAX_QTY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS REGION, q6._1 AS MAX_QTY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS REGION, LONG AS MAX_QTY)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_MV_MAX_EVER_QTY_BY_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +~ +max_ever_aggregatehEXPLAIN select region, max_ever(quantity) as max_qty from sales_index_on group by region order by region +! ȱ(g0+8!@{AISCAN(IDX_IOT_MAX_EVER_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS MAX_QTY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS REGION, q6._1 AS MAX_QTY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS REGION, LONG AS MAX_QTY)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, LONG AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_MAX_EVER_QTY_BY_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +aggregate_in_middle_of_keyEXPLAIN select category, max(amount) as max_amt, region from sales_mat_view group by category, region order by category, region +: Ǡ*(l08.@AISCAN(IDX_MV_CAT_MAX_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: KEY:[2]]) | MAP (_._0 AS CATEGORY, _._2 AS MAX_AMT, _._1 AS REGION) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS CATEGORY, q6._2 AS MAX_AMT, q6._1 AS REGION)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS MAX_AMT, STRING AS REGION)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1, LONG AS _2)" ]; + 3 [ label=<
Index
IDX_MV_CAT_MAX_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +aggregate_in_middle_of_keyEXPLAIN select category, max(amount) as max_amt, region from sales_index_on group by category, region order by category, region +3 &(e08!@ISCAN(IDX_IOT_MIN_CAT_REGION <,>) | MAP (_ AS _0) | AGG (max_l(_._0.AMOUNT) AS _0) GROUP BY (_._0.CATEGORY AS _0, _._0.REGION AS _1) | MAP (_._0._0 AS CATEGORY, _._1._0 AS MAX_AMT, _._0._1 AS REGION)digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0._0 AS CATEGORY, q6._1._0 AS MAX_AMT, q6._0._1 AS REGION)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, LONG AS MAX_AMT, STRING AS REGION)" ]; + 2 [ label=<
Streaming Aggregate
COLLECT (max_l(q206._0.AMOUNT) AS _0)
GROUP BY (q206._0.CATEGORY AS _0, q206._0.REGION AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1 AS _0, LONG AS _0 AS _1)" ]; + 3 [ label=<
Value Computation
MAP (q2 AS _0)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY AS _0)" ]; + 4 [ label=<
Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 5 [ label=<
Index
IDX_IOT_MIN_CAT_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ label=< q206> label="q206" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 3 [ label=< q2> label="q2" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 4 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +aggregate_in_middle_of_keyEXPLAIN select min(amount) as min_amt, category, region from sales_mat_view group by category, region order by category, region +9 *(l0Ä8.@AISCAN(IDX_MV_MIN_CAT_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: KEY:[2]]) | MAP (_._2 AS MIN_AMT, _._0 AS CATEGORY, _._1 AS REGION) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._2 AS MIN_AMT, q6._0 AS CATEGORY, q6._1 AS REGION)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS MIN_AMT, STRING AS CATEGORY, STRING AS REGION)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1, LONG AS _2)" ]; + 3 [ label=<
Index
IDX_MV_MIN_CAT_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +aggregate_in_middle_of_keyEXPLAIN select min(amount) as min_amt, category, region from sales_index_on group by category, region order by category, region + (e0ܵ)8!@ISCAN(IDX_IOT_MIN_CAT_REGION <,>) | MAP (_ AS _0) | AGG (min_l(_._0.AMOUNT) AS _0) GROUP BY (_._0.CATEGORY AS _0, _._0.REGION AS _1) | MAP (_._1._0 AS MIN_AMT, _._0._0 AS CATEGORY, _._0._1 AS REGION)digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._1._0 AS MIN_AMT, q6._0._0 AS CATEGORY, q6._0._1 AS REGION)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS MIN_AMT, STRING AS CATEGORY, STRING AS REGION)" ]; + 2 [ label=<
Streaming Aggregate
COLLECT (min_l(q206._0.AMOUNT) AS _0)
GROUP BY (q206._0.CATEGORY AS _0, q206._0.REGION AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, STRING AS _1 AS _0, LONG AS _0 AS _1)" ]; + 3 [ label=<
Value Computation
MAP (q2 AS _0)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY AS _0)" ]; + 4 [ label=<
Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 5 [ label=<
Index
IDX_IOT_MIN_CAT_REGION
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PRODUCT_ID, STRING AS CATEGORY, STRING AS REGION, LONG AS AMOUNT, LONG AS QUANTITY)" ]; + 3 -> 2 [ label=< q206> label="q206" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 3 [ label=< q2> label="q2" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 4 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} \ No newline at end of file diff --git a/yaml-tests/src/test/resources/index-ddl-aggregates-only.metrics.yaml b/yaml-tests/src/test/resources/index-ddl-aggregates-only.metrics.yaml new file mode 100644 index 0000000000..7f4c42fb71 --- /dev/null +++ b/yaml-tests/src/test/resources/index-ddl-aggregates-only.metrics.yaml @@ -0,0 +1,357 @@ +sum_aggregate_single_grouping: +- query: EXPLAIN select category, sum(amount) as total from sales_mat_view group + by category order by category + explain: 'AISCAN(IDX_MV_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) + | MAP (_._0 AS CATEGORY, _._1 AS TOTAL)' + task_count: 1034 + task_total_time_ms: 429 + transform_count: 295 + transform_time_ms: 336 + transform_yield_count: 140 + insert_time_ms: 7 + insert_new_count: 98 + insert_reused_count: 10 +- query: EXPLAIN select category, sum(amount) as total from sales_index_on group + by category order by category + explain: 'AISCAN(IDX_IOT_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) + | MAP (_._0 AS CATEGORY, _._1 AS TOTAL)' + task_count: 1034 + task_total_time_ms: 431 + transform_count: 295 + transform_time_ms: 334 + transform_yield_count: 140 + insert_time_ms: 7 + insert_new_count: 98 + insert_reused_count: 10 +- query: EXPLAIN select region, sum(amount) as total from sales_mat_view group by + region order by region + explain: 'AISCAN(IDX_MV_SUM_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) + | MAP (_._0 AS REGION, _._1 AS TOTAL)' + task_count: 558 + task_total_time_ms: 352 + transform_count: 182 + transform_time_ms: 297 + transform_yield_count: 119 + insert_time_ms: 4 + insert_new_count: 36 + insert_reused_count: 3 +- query: EXPLAIN select region, sum(amount) as total from sales_index_on group by + region order by region + explain: 'AISCAN(IDX_IOT_SUM_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) + | MAP (_._0 AS REGION, _._1 AS TOTAL)' + task_count: 558 + task_total_time_ms: 148 + transform_count: 182 + transform_time_ms: 120 + transform_yield_count: 119 + insert_time_ms: 1 + insert_new_count: 36 + insert_reused_count: 3 +sum_aggregate_multi_grouping: +- query: EXPLAIN select category, region, sum(amount) as total from sales_mat_view + group by category, region order by category, region + explain: 'AISCAN(IDX_MV_CAT_REGION_SUM <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], + _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL)' + task_count: 815 + task_total_time_ms: 271 + transform_count: 248 + transform_time_ms: 205 + transform_yield_count: 126 + insert_time_ms: 3 + insert_new_count: 69 + insert_reused_count: 7 +- query: EXPLAIN select category, region, sum(amount) as total from sales_index_on + group by category, region order by category, region + explain: 'AISCAN(IDX_IOT_CAT_REGION_SUM <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], + _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL)' + task_count: 815 + task_total_time_ms: 277 + transform_count: 248 + transform_time_ms: 210 + transform_yield_count: 126 + insert_time_ms: 3 + insert_new_count: 69 + insert_reused_count: 7 +- query: EXPLAIN select category, region, sum(amount) as total from sales_mat_view + where category = 'Electronics' group by category, region order by region + explain: 'AISCAN(IDX_MV_CAT_REGION_SUM [EQUALS promote(@c17 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 + AS REGION, _._2 AS TOTAL)' + task_count: 865 + task_total_time_ms: 318 + transform_count: 276 + transform_time_ms: 223 + transform_yield_count: 129 + insert_time_ms: 3 + insert_new_count: 71 + insert_reused_count: 3 +- query: EXPLAIN select category, region, sum(amount) as total from sales_index_on + where category = 'Electronics' group by category, region order by region + explain: 'AISCAN(IDX_IOT_CAT_REGION_SUM [EQUALS promote(@c17 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 + AS REGION, _._2 AS TOTAL)' + task_count: 865 + task_total_time_ms: 242 + transform_count: 276 + transform_time_ms: 166 + transform_yield_count: 129 + insert_time_ms: 3 + insert_new_count: 71 + insert_reused_count: 3 +sum_aggregate_three_grouping: +- query: EXPLAIN select category, region, product_id, sum(amount) as total from + sales_mat_view group by category, region, product_id order by category, region, + product_id + explain: 'AISCAN(IDX_MV_SUM_BY_ALL <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], + _2: KEY:[2], _3: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 + AS PRODUCT_ID, _._3 AS TOTAL)' + task_count: 468 + task_total_time_ms: 151 + transform_count: 163 + transform_time_ms: 125 + transform_yield_count: 100 + insert_time_ms: 0 + insert_new_count: 24 + insert_reused_count: 1 +- query: EXPLAIN select category, region, product_id, sum(amount) as total from + sales_index_on group by category, region, product_id order by category, region, + product_id + explain: 'AISCAN(IDX_IOT_SUM_BY_ALL <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], + _2: KEY:[2], _3: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 + AS PRODUCT_ID, _._3 AS TOTAL)' + task_count: 468 + task_total_time_ms: 152 + transform_count: 163 + transform_time_ms: 128 + transform_yield_count: 100 + insert_time_ms: 0 + insert_new_count: 24 + insert_reused_count: 1 +count_star_aggregate: +- query: EXPLAIN select category, count(*) as cnt from sales_mat_view group by category + order by category + explain: 'AISCAN(IDX_MV_COUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) + | MAP (_._0 AS CATEGORY, _._1 AS CNT)' + task_count: 623 + task_total_time_ms: 130 + transform_count: 190 + transform_time_ms: 86 + transform_yield_count: 110 + insert_time_ms: 2 + insert_new_count: 56 + insert_reused_count: 7 +- query: EXPLAIN select category, count(*) as cnt from sales_index_on group by category + order by category + explain: 'AISCAN(IDX_IOT_COUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) + | MAP (_._0 AS CATEGORY, _._1 AS CNT)' + task_count: 623 + task_total_time_ms: 129 + transform_count: 190 + transform_time_ms: 81 + transform_yield_count: 110 + insert_time_ms: 2 + insert_new_count: 56 + insert_reused_count: 7 +count_column_aggregate: +- query: EXPLAIN select region, count(quantity) as cnt from sales_mat_view group + by region order by region + explain: 'AISCAN(IDX_MV_COUNT_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: + VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS CNT)' + task_count: 528 + task_total_time_ms: 101 + transform_count: 173 + transform_time_ms: 71 + transform_yield_count: 104 + insert_time_ms: 1 + insert_new_count: 36 + insert_reused_count: 3 +- query: EXPLAIN select region, count(quantity) as cnt from sales_index_on group + by region order by region + explain: 'AISCAN(IDX_IOT_COUNT_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: + VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS CNT)' + task_count: 528 + task_total_time_ms: 102 + transform_count: 173 + transform_time_ms: 71 + transform_yield_count: 104 + insert_time_ms: 1 + insert_new_count: 36 + insert_reused_count: 3 +aggregate_with_having: +- query: EXPLAIN select category, sum(amount) as total from sales_mat_view group + by category having sum(amount) > 2000 order by category + explain: 'AISCAN(IDX_MV_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) + | FILTER _._1 GREATER_THAN promote(@c21 AS LONG) | MAP (_._0 AS CATEGORY, + _._1 AS TOTAL)' + task_count: 1125 + task_total_time_ms: 325 + transform_count: 313 + transform_time_ms: 236 + transform_yield_count: 144 + insert_time_ms: 7 + insert_new_count: 111 + insert_reused_count: 10 +- query: EXPLAIN select category, sum(amount) as total from sales_index_on group + by category having sum(amount) > 2000 order by category + explain: 'AISCAN(IDX_IOT_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) + | FILTER _._1 GREATER_THAN promote(@c21 AS LONG) | MAP (_._0 AS CATEGORY, + _._1 AS TOTAL)' + task_count: 1125 + task_total_time_ms: 326 + transform_count: 313 + transform_time_ms: 239 + transform_yield_count: 144 + insert_time_ms: 9 + insert_new_count: 111 + insert_reused_count: 10 +min_aggregate: +- query: EXPLAIN select category, min(amount) as min_amt from sales_mat_view group + by category order by category + explain: 'AISCAN(IDX_MV_MIN_AMOUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: + KEY:[1]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_AMT)' + task_count: 780 + task_total_time_ms: 140 + transform_count: 229 + transform_time_ms: 98 + transform_yield_count: 122 + insert_time_ms: 2 + insert_new_count: 72 + insert_reused_count: 8 +- query: EXPLAIN select category, min(amount) as min_amt from sales_index_on group + by category order by category + explain: 'AISCAN(IDX_IOT_MIN_AMOUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], + _1: KEY:[1]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_AMT)' + task_count: 653 + task_total_time_ms: 125 + transform_count: 197 + transform_time_ms: 87 + transform_yield_count: 117 + insert_time_ms: 1 + insert_new_count: 58 + insert_reused_count: 7 +max_aggregate: +- query: EXPLAIN select region, max(amount) as max_amt from sales_mat_view group + by region order by region + explain: 'AISCAN(IDX_MV_MAX_AMOUNT_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: + KEY:[1]]) | MAP (_._0 AS REGION, _._1 AS MAX_AMT)' + task_count: 558 + task_total_time_ms: 86 + transform_count: 180 + transform_time_ms: 64 + transform_yield_count: 111 + insert_time_ms: 0 + insert_new_count: 38 + insert_reused_count: 3 +- query: EXPLAIN select region, max(amount) as max_amt from sales_index_on group + by region order by region + explain: 'AISCAN(IDX_IOT_MAX_AMOUNT_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: + KEY:[1]]) | MAP (_._0 AS REGION, _._1 AS MAX_AMT)' + task_count: 558 + task_total_time_ms: 89 + transform_count: 180 + transform_time_ms: 67 + transform_yield_count: 111 + insert_time_ms: 0 + insert_new_count: 38 + insert_reused_count: 3 +min_ever_aggregate: +- query: EXPLAIN select category, min_ever(quantity) as min_qty from sales_mat_view + group by category order by category + explain: 'AISCAN(IDX_MV_MIN_EVER_QTY_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], + _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_QTY)' + task_count: 593 + task_total_time_ms: 85 + transform_count: 187 + transform_time_ms: 60 + transform_yield_count: 108 + insert_time_ms: 1 + insert_new_count: 50 + insert_reused_count: 7 +- query: EXPLAIN select category, min_ever(quantity) as min_qty from sales_index_on + group by category order by category + explain: 'AISCAN(IDX_IOT_MIN_EVER_QTY_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], + _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_QTY)' + task_count: 593 + task_total_time_ms: 85 + transform_count: 187 + transform_time_ms: 53 + transform_yield_count: 108 + insert_time_ms: 1 + insert_new_count: 50 + insert_reused_count: 7 +max_ever_aggregate: +- query: EXPLAIN select region, max_ever(quantity) as max_qty from sales_mat_view + group by region order by region + explain: 'AISCAN(IDX_MV_MAX_EVER_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: + VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS MAX_QTY)' + task_count: 513 + task_total_time_ms: 69 + transform_count: 171 + transform_time_ms: 47 + transform_yield_count: 103 + insert_time_ms: 0 + insert_new_count: 33 + insert_reused_count: 3 +- query: EXPLAIN select region, max_ever(quantity) as max_qty from sales_index_on + group by region order by region + explain: 'AISCAN(IDX_IOT_MAX_EVER_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], + _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS MAX_QTY)' + task_count: 513 + task_total_time_ms: 69 + transform_count: 171 + transform_time_ms: 48 + transform_yield_count: 103 + insert_time_ms: 0 + insert_new_count: 33 + insert_reused_count: 3 +aggregate_in_middle_of_key: +- query: EXPLAIN select category, max(amount) as max_amt, region from sales_mat_view + group by category, region order by category, region + explain: 'AISCAN(IDX_MV_CAT_MAX_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], + _2: KEY:[2]]) | MAP (_._0 AS CATEGORY, _._2 AS MAX_AMT, _._1 AS REGION)' + task_count: 583 + task_total_time_ms: 122 + transform_count: 184 + transform_time_ms: 88 + transform_yield_count: 108 + insert_time_ms: 2 + insert_new_count: 46 + insert_reused_count: 5 +- query: EXPLAIN select category, max(amount) as max_amt, region from sales_index_on + group by category, region order by category, region + explain: ISCAN(IDX_IOT_MIN_CAT_REGION <,>) | MAP (_ AS _0) | AGG (max_l(_._0.AMOUNT) + AS _0) GROUP BY (_._0.CATEGORY AS _0, _._0.REGION AS _1) | MAP (_._0._0 AS + CATEGORY, _._1._0 AS MAX_AMT, _._0._1 AS REGION) + task_count: 458 + task_total_time_ms: 108 + transform_count: 150 + transform_time_ms: 80 + transform_yield_count: 101 + insert_time_ms: 2 + insert_new_count: 33 + insert_reused_count: 4 +- query: EXPLAIN select min(amount) as min_amt, category, region from sales_mat_view + group by category, region order by category, region + explain: 'AISCAN(IDX_MV_MIN_CAT_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], + _2: KEY:[2]]) | MAP (_._2 AS MIN_AMT, _._0 AS CATEGORY, _._1 AS REGION)' + task_count: 583 + task_total_time_ms: 120 + transform_count: 184 + transform_time_ms: 88 + transform_yield_count: 108 + insert_time_ms: 2 + insert_new_count: 46 + insert_reused_count: 5 +- query: EXPLAIN select min(amount) as min_amt, category, region from sales_index_on + group by category, region order by category, region + explain: ISCAN(IDX_IOT_MIN_CAT_REGION <,>) | MAP (_ AS _0) | AGG (min_l(_._0.AMOUNT) + AS _0) GROUP BY (_._0.CATEGORY AS _0, _._0.REGION AS _1) | MAP (_._1._0 AS + MIN_AMT, _._0._0 AS CATEGORY, _._0._1 AS REGION) + task_count: 458 + task_total_time_ms: 61 + transform_count: 150 + transform_time_ms: 46 + transform_yield_count: 101 + insert_time_ms: 0 + insert_new_count: 33 + insert_reused_count: 4 diff --git a/yaml-tests/src/test/resources/index-ddl-aggregates-only.yamsql b/yaml-tests/src/test/resources/index-ddl-aggregates-only.yamsql new file mode 100644 index 0000000000..3bc59d5f91 --- /dev/null +++ b/yaml-tests/src/test/resources/index-ddl-aggregates-only.yamsql @@ -0,0 +1,320 @@ +# +# index-ddl-aggregates-only.yamsql +# +# This source file is part of the FoundationDB open source project +# +# Copyright 2021-2025 Apple Inc. and the FoundationDB project authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Test AGGREGATE indexes with different aggregation functions +# Compares CREATE INDEX AS SELECT with GROUP BY vs CREATE INDEX ON with aggregate views +# Both syntaxes should produce equivalent query plans + +--- +options: + supported_version: !current_version + noChecks: [metrics] +--- +schema_template: + create table sales_mat_view( + id bigint, + product_id bigint, + category string, + region string, + amount bigint, + quantity bigint, + primary key(id) + ) + + create index idx_mv_sum_by_category as select sum(amount) from sales_mat_view group by category + create index idx_mv_sum_by_region as select sum(amount) from sales_mat_view group by region + create index idx_mv_sum_by_cat_region as select sum(amount) from sales_mat_view group by category, region + create index idx_mv_sum_by_all as select sum(amount) from sales_mat_view group by category, region, product_id + + create index idx_mv_count_by_category as select count(*) from sales_mat_view group by category + create index idx_mv_count_qty_by_region as select count(quantity) from sales_mat_view group by region + + create index idx_mv_cat_region_sum as select category, region, sum(amount) from sales_mat_view group by category, region + + create index idx_mv_min_amount_by_category as select min(amount) from sales_mat_view group by category + create index idx_mv_max_amount_by_region as select max(amount) from sales_mat_view group by region + + create index idx_mv_min_ever_qty_by_category as select min_ever(quantity) from sales_mat_view group by category + create index idx_mv_max_ever_qty_by_region as select max_ever(quantity) from sales_mat_view group by region + + create index idx_mv_cat_max_region as select category, max(amount), region from sales_mat_view group by category, region + create index idx_mv_min_cat_region as select min(amount), category, region from sales_mat_view group by category, region + + create table sales_index_on( + id bigint, + product_id bigint, + category string, + region string, + amount bigint, + quantity bigint, + primary key(id) + ) + + create view v_iot_sum_by_category as select sum(amount) as total_amount, category from sales_index_on group by category + create index idx_iot_sum_by_category on v_iot_sum_by_category(category) include(total_amount) + + create view v_iot_sum_by_region as select sum(amount) as total_amount, region from sales_index_on group by region + create index idx_iot_sum_by_region on v_iot_sum_by_region(region) include(total_amount) + + create view v_iot_sum_by_cat_region as select sum(amount) as total_amount, category, region from sales_index_on group by category, region + create index idx_iot_sum_by_cat_region on v_iot_sum_by_cat_region(category, region) include(total_amount) + + create view v_iot_sum_by_all as select sum(amount) as total_amount, category, region, product_id from sales_index_on group by category, region, product_id + create index idx_iot_sum_by_all on v_iot_sum_by_all(category, region, product_id) include(total_amount) + + create view v_iot_count_by_category as select count(*) as record_count, category from sales_index_on group by category + create index idx_iot_count_by_category on v_iot_count_by_category(category) include(record_count) + + create view v_iot_count_qty_by_region as select count(quantity) as qty_count, region from sales_index_on group by region + create index idx_iot_count_qty_by_region on v_iot_count_qty_by_region(region) include(qty_count) + + create view v_iot_cat_region_sum as select category, region, sum(amount) as total_amount from sales_index_on group by category, region + create index idx_iot_cat_region_sum on v_iot_cat_region_sum(category, region) include(total_amount) + + create view v_iot_min_amount_by_category as select min(amount) as min_amount, category from sales_index_on group by category + create index idx_iot_min_amount_by_category on v_iot_min_amount_by_category(category) include(min_amount) + + create view v_iot_max_amount_by_region as select max(amount) as max_amount, region from sales_index_on group by region + create index idx_iot_max_amount_by_region on v_iot_max_amount_by_region(region) include(max_amount) + + create view v_iot_min_ever_qty_by_category as select min_ever(quantity) as min_qty, category from sales_index_on group by category + create index idx_iot_min_ever_qty_by_category on v_iot_min_ever_qty_by_category(category) include(min_qty) + + create view v_iot_max_ever_qty_by_region as select max_ever(quantity) as max_qty, region from sales_index_on group by region + create index idx_iot_max_ever_qty_by_region on v_iot_max_ever_qty_by_region(region) include(max_qty) + + create view v_iot_cat_max_region as select category, max(amount) as max_amount, region from sales_index_on group by category, region + create index idx_iot_cat_max_region on v_iot_cat_max_region(category, max_amount, region) + + create view v_iot_min_cat_region as select min(amount) as min_amount, category, region from sales_index_on group by category, region + create index idx_iot_min_cat_region on v_iot_min_cat_region(min_amount, category, region) + +--- +setup: + steps: + - query: INSERT INTO sales_mat_view VALUES + (1, 101, 'Electronics', 'North', 1500, 10), + (2, 102, 'Electronics', 'South', 2000, 15), + (3, 103, 'Electronics', 'East', 1200, 8), + (4, 104, 'Electronics', 'North', 1800, 12), + (5, 201, 'Furniture', 'North', 800, 5), + (6, 202, 'Furniture', 'South', 950, 7), + (7, 203, 'Furniture', 'West', 1100, 6), + (8, 301, 'Clothing', 'East', 500, 20), + (9, 302, 'Clothing', 'West', 600, 25), + (10, 303, 'Clothing', 'North', 450, 18) + + - query: INSERT INTO sales_index_on VALUES + (1, 101, 'Electronics', 'North', 1500, 10), + (2, 102, 'Electronics', 'South', 2000, 15), + (3, 103, 'Electronics', 'East', 1200, 8), + (4, 104, 'Electronics', 'North', 1800, 12), + (5, 201, 'Furniture', 'North', 800, 5), + (6, 202, 'Furniture', 'South', 950, 7), + (7, 203, 'Furniture', 'West', 1100, 6), + (8, 301, 'Clothing', 'East', 500, 20), + (9, 302, 'Clothing', 'West', 600, 25), + (10, 303, 'Clothing', 'North', 450, 18) + +--- +test_block: + name: sum_aggregate_single_grouping + tests: + # Test SUM aggregate with single GROUP BY column - category + - + - query: select category, sum(amount) as total from sales_mat_view group by category order by category + - explain: "AISCAN(IDX_MV_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS TOTAL)" + - result: [{ "Clothing", 1550 }, { "Electronics", 6500 }, { "Furniture", 2850 }] + - + - query: select category, sum(amount) as total from sales_index_on group by category order by category + - explain: "AISCAN(IDX_IOT_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS TOTAL)" + - result: [{ "Clothing", 1550 }, { "Electronics", 6500 }, { "Furniture", 2850 }] + + # Test SUM aggregate with single GROUP BY column - region + - + - query: select region, sum(amount) as total from sales_mat_view group by region order by region + - explain: "AISCAN(IDX_MV_SUM_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS TOTAL)" + - result: [{ "East", 1700 }, { "North", 4550 }, { "South", 2950 }, { "West", 1700 }] + - + - query: select region, sum(amount) as total from sales_index_on group by region order by region + - explain: "AISCAN(IDX_IOT_SUM_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS TOTAL)" + - result: [{ "East", 1700 }, { "North", 4550 }, { "South", 2950 }, { "West", 1700 }] + +--- +test_block: + name: sum_aggregate_multi_grouping + tests: + # Test SUM aggregate with multiple GROUP BY columns + - + - query: select category, region, sum(amount) as total from sales_mat_view group by category, region order by category, region + - explain: "AISCAN(IDX_MV_CAT_REGION_SUM <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL)" + - result: [{ "Clothing", "East", 500 }, { "Clothing", "North", 450 }, { "Clothing", "West", 600 }, { "Electronics", "East", 1200 }, { "Electronics", "North", 3300 }, { "Electronics", "South", 2000 }, { "Furniture", "North", 800 }, { "Furniture", "South", 950 }, { "Furniture", "West", 1100 }] + - + - query: select category, region, sum(amount) as total from sales_index_on group by category, region order by category, region + - explain: "AISCAN(IDX_IOT_CAT_REGION_SUM <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL)" + - result: [{ "Clothing", "East", 500 }, { "Clothing", "North", 450 }, { "Clothing", "West", 600 }, { "Electronics", "East", 1200 }, { "Electronics", "North", 3300 }, { "Electronics", "South", 2000 }, { "Furniture", "North", 800 }, { "Furniture", "South", 950 }, { "Furniture", "West", 1100 }] + + # Test filtering on specific category + - + - query: select category, region, sum(amount) as total from sales_mat_view where category = 'Electronics' group by category, region order by region + - explain: "AISCAN(IDX_MV_CAT_REGION_SUM [EQUALS promote(@c17 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL)" + - result: [{ "Electronics", "East", 1200 }, { "Electronics", "North", 3300 }, { "Electronics", "South", 2000 }] + - + - query: select category, region, sum(amount) as total from sales_index_on where category = 'Electronics' group by category, region order by region + - explain: "AISCAN(IDX_IOT_CAT_REGION_SUM [EQUALS promote(@c17 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS TOTAL)" + - result: [{ "Electronics", "East", 1200 }, { "Electronics", "North", 3300 }, { "Electronics", "South", 2000 }] + +--- +test_block: + name: sum_aggregate_three_grouping + tests: + # Test aggregate with three GROUP BY columns + - + - query: select category, region, product_id, sum(amount) as total from sales_mat_view group by category, region, product_id order by category, region, product_id + - explain: "AISCAN(IDX_MV_SUM_BY_ALL <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: KEY:[2], _3: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS PRODUCT_ID, _._3 AS TOTAL)" + - result: [{ "Clothing", "East", 301, 500 }, { "Clothing", "North", 303, 450 }, { "Clothing", "West", 302, 600 }, { "Electronics", "East", 103, 1200 }, { "Electronics", "North", 101, 1500 }, { "Electronics", "North", 104, 1800 }, { "Electronics", "South", 102, 2000 }, { "Furniture", "North", 201, 800 }, { "Furniture", "South", 202, 950 }, { "Furniture", "West", 203, 1100 }] + - + - query: select category, region, product_id, sum(amount) as total from sales_index_on group by category, region, product_id order by category, region, product_id + - explain: "AISCAN(IDX_IOT_SUM_BY_ALL <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: KEY:[2], _3: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS REGION, _._2 AS PRODUCT_ID, _._3 AS TOTAL)" + - result: [{ "Clothing", "East", 301, 500 }, { "Clothing", "North", 303, 450 }, { "Clothing", "West", 302, 600 }, { "Electronics", "East", 103, 1200 }, { "Electronics", "North", 101, 1500 }, { "Electronics", "North", 104, 1800 }, { "Electronics", "South", 102, 2000 }, { "Furniture", "North", 201, 800 }, { "Furniture", "South", 202, 950 }, { "Furniture", "West", 203, 1100 }] + +--- +test_block: + name: count_star_aggregate + tests: + # Test COUNT(*) aggregate + - + - query: select category, count(*) as cnt from sales_mat_view group by category order by category + - explain: "AISCAN(IDX_MV_COUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS CNT)" + - result: [{ "Clothing", 3 }, { "Electronics", 4 }, { "Furniture", 3 }] + - + - query: select category, count(*) as cnt from sales_index_on group by category order by category + - explain: "AISCAN(IDX_IOT_COUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS CNT)" + - result: [{ "Clothing", 3 }, { "Electronics", 4 }, { "Furniture", 3 }] + +--- +test_block: + name: count_column_aggregate + tests: + # Test COUNT(column) aggregate + - + - query: select region, count(quantity) as cnt from sales_mat_view group by region order by region + - explain: "AISCAN(IDX_MV_COUNT_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS CNT)" + - result: [{ "East", 2 }, { "North", 4 }, { "South", 2 }, { "West", 2 }] + - + - query: select region, count(quantity) as cnt from sales_index_on group by region order by region + - explain: "AISCAN(IDX_IOT_COUNT_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS CNT)" + - result: [{ "East", 2 }, { "North", 4 }, { "South", 2 }, { "West", 2 }] + +--- +test_block: + name: aggregate_with_having + tests: + # Test HAVING clause with aggregates + - + - query: select category, sum(amount) as total from sales_mat_view group by category having sum(amount) > 2000 order by category + - explain: "AISCAN(IDX_MV_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | FILTER _._1 GREATER_THAN promote(@c21 AS LONG) | MAP (_._0 AS CATEGORY, _._1 AS TOTAL)" + - result: [{ "Electronics", 6500 }, { "Furniture", 2850 }] + - + - query: select category, sum(amount) as total from sales_index_on group by category having sum(amount) > 2000 order by category + - explain: "AISCAN(IDX_IOT_SUM_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | FILTER _._1 GREATER_THAN promote(@c21 AS LONG) | MAP (_._0 AS CATEGORY, _._1 AS TOTAL)" + - result: [{ "Electronics", 6500 }, { "Furniture", 2850 }] + +--- +test_block: + name: min_aggregate + tests: + # Test MIN aggregate + - + - query: select category, min(amount) as min_amt from sales_mat_view group by category order by category + - explain: "AISCAN(IDX_MV_MIN_AMOUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_AMT)" + - result: [{ "Clothing", 450 }, { "Electronics", 1200 }, { "Furniture", 800 }] + - + - query: select category, min(amount) as min_amt from sales_index_on group by category order by category + - explain: "AISCAN(IDX_IOT_MIN_AMOUNT_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_AMT)" + - result: [{ "Clothing", 450 }, { "Electronics", 1200 }, { "Furniture", 800 }] + +--- +test_block: + name: max_aggregate + tests: + # Test MAX aggregate + - + - query: select region, max(amount) as max_amt from sales_mat_view group by region order by region + - explain: "AISCAN(IDX_MV_MAX_AMOUNT_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1]]) | MAP (_._0 AS REGION, _._1 AS MAX_AMT)" + - result: [{ "East", 1200 }, { "North", 1800 }, { "South", 2000 }, { "West", 1100 }] + - + - query: select region, max(amount) as max_amt from sales_index_on group by region order by region + - explain: "AISCAN(IDX_IOT_MAX_AMOUNT_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1]]) | MAP (_._0 AS REGION, _._1 AS MAX_AMT)" + - result: [{ "East", 1200 }, { "North", 1800 }, { "South", 2000 }, { "West", 1100 }] + +--- +test_block: + name: min_ever_aggregate + tests: + # Test MIN_EVER aggregate + - + - query: select category, min_ever(quantity) as min_qty from sales_mat_view group by category order by category + - explain: "AISCAN(IDX_MV_MIN_EVER_QTY_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_QTY)" + - result: [{ "Clothing", 18 }, { "Electronics", 8 }, { "Furniture", 5 }] + - + - query: select category, min_ever(quantity) as min_qty from sales_index_on group by category order by category + - explain: "AISCAN(IDX_IOT_MIN_EVER_QTY_BY_CATEGORY <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS CATEGORY, _._1 AS MIN_QTY)" + - result: [{ "Clothing", 18 }, { "Electronics", 8 }, { "Furniture", 5 }] + +--- +test_block: + name: max_ever_aggregate + tests: + # Test MAX_EVER aggregate + - + - query: select region, max_ever(quantity) as max_qty from sales_mat_view group by region order by region + - explain: "AISCAN(IDX_MV_MAX_EVER_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS MAX_QTY)" + - result: [{ "East", 20 }, { "North", 18 }, { "South", 15 }, { "West", 25 }] + - + - query: select region, max_ever(quantity) as max_qty from sales_index_on group by region order by region + - explain: "AISCAN(IDX_IOT_MAX_EVER_QTY_BY_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS REGION, _._1 AS MAX_QTY)" + - result: [{ "East", 20 }, { "North", 18 }, { "South", 15 }, { "West", 25 }] + +--- +test_block: + name: aggregate_in_middle_of_key + tests: + # Test aggregate value in the middle of the key - category, max(amount), region + - + - query: select category, max(amount) as max_amt, region from sales_mat_view group by category, region order by category, region + - explain: "AISCAN(IDX_MV_CAT_MAX_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: KEY:[2]]) | MAP (_._0 AS CATEGORY, _._2 AS MAX_AMT, _._1 AS REGION)" + - result: [{ "Clothing", 500, "East" }, { "Clothing", 450, "North" }, { "Clothing", 600, "West" }, { "Electronics", 1200, "East" }, { "Electronics", 1800, "North" }, { "Electronics", 2000, "South" }, { "Furniture", 800, "North" }, { "Furniture", 950, "South" }, { "Furniture", 1100, "West" }] + - + - query: select category, max(amount) as max_amt, region from sales_index_on group by category, region order by category, region + - explain: "ISCAN(IDX_IOT_MIN_CAT_REGION <,>) | MAP (_ AS _0) | AGG (max_l(_._0.AMOUNT) AS _0) GROUP BY (_._0.CATEGORY AS _0, _._0.REGION AS _1) | MAP (_._0._0 AS CATEGORY, _._1._0 AS MAX_AMT, _._0._1 AS REGION)" + - result: [{ "Clothing", 500, "East" }, { "Clothing", 450, "North" }, { "Clothing", 600, "West" }, { "Electronics", 1200, "East" }, { "Electronics", 1800, "North" }, { "Electronics", 2000, "South" }, { "Furniture", 800, "North" }, { "Furniture", 950, "South" }, { "Furniture", 1100, "West" }] + + # Test aggregate value at the beginning of the key - min(amount), category, region + - + - query: select min(amount) as min_amt, category, region from sales_mat_view group by category, region order by category, region + - explain: "AISCAN(IDX_MV_MIN_CAT_REGION <,> BY_GROUP -> [_0: KEY:[0], _1: KEY:[1], _2: KEY:[2]]) | MAP (_._2 AS MIN_AMT, _._0 AS CATEGORY, _._1 AS REGION)" + - result: [{ 500, "Clothing", "East" }, { 450, "Clothing", "North" }, { 600, "Clothing", "West" }, { 1200, "Electronics", "East" }, { 1500, "Electronics", "North" }, { 2000, "Electronics", "South" }, { 800, "Furniture", "North" }, { 950, "Furniture", "South" }, { 1100, "Furniture", "West" }] + - + - query: select min(amount) as min_amt, category, region from sales_index_on group by category, region order by category, region + - explain: "ISCAN(IDX_IOT_MIN_CAT_REGION <,>) | MAP (_ AS _0) | AGG (min_l(_._0.AMOUNT) AS _0) GROUP BY (_._0.CATEGORY AS _0, _._0.REGION AS _1) | MAP (_._1._0 AS MIN_AMT, _._0._0 AS CATEGORY, _._0._1 AS REGION)" + - result: [{ 500, "Clothing", "East" }, { 450, "Clothing", "North" }, { 600, "Clothing", "West" }, { 1200, "Electronics", "East" }, { 1500, "Electronics", "North" }, { 2000, "Electronics", "South" }, { 800, "Furniture", "North" }, { 950, "Furniture", "South" }, { 1100, "Furniture", "West" }] + +... + diff --git a/yaml-tests/src/test/resources/index-ddl-values-only.metrics.binpb b/yaml-tests/src/test/resources/index-ddl-values-only.metrics.binpb new file mode 100644 index 0000000000..25b7e1a1c9 --- /dev/null +++ b/yaml-tests/src/test/resources/index-ddl-values-only.metrics.binpb @@ -0,0 +1,411 @@ + +c +simple_value_indexMEXPLAIN select price from products_mat_view where price > 18.0 order by price +! ־(y0ܪ8X@COVERING(IDX_MV_PRICE_DESC [[LESS_THAN to_ordered_bytes(promote(@c8 AS DOUBLE), DESC_NULLS_LAST)]] REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q172.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[LESS_THAN to_ordered_bytes(promote(@c8 AS DOUBLE), DESC_NULLS_LAST)]]
direction: reversed
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_PRICE_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q172> label="q172" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +c +simple_value_indexMEXPLAIN select price from products_index_on where price > 18.0 order by price +簣 ٙh(y0朜 +8X@COVERING(IDX_IOT_PRICE_DESC [[LESS_THAN to_ordered_bytes(promote(@c8 AS DOUBLE), DESC_NULLS_LAST)]] REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q172.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[LESS_THAN to_ordered_bytes(promote(@c8 AS DOUBLE), DESC_NULLS_LAST)]]
direction: reversed
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_PRICE_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q172> label="q172" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +c +simple_value_indexMEXPLAIN select category from products_mat_view where category = 'Electronics' + ݍ(08@zCOVERING(IDX_MV_CATEGORY [EQUALS promote(@c8 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[2]]) | MAP (_.CATEGORY AS CATEGORY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q358.CATEGORY AS CATEGORY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c8 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q358> label="q358" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +c +simple_value_indexMEXPLAIN select category from products_index_on where category = 'Electronics' +DŽ (08@{COVERING(IDX_IOT_CATEGORY [EQUALS promote(@c8 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[2]]) | MAP (_.CATEGORY AS CATEGORY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q358.CATEGORY AS CATEGORY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c8 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_CATEGORY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q358> label="q358" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +multi_column_value_indexmEXPLAIN select category, price from products_mat_view where category = 'Electronics' order by category, price +  (t0V8H@COVERING(IDX_MV_CAT_PRICE [EQUALS promote(@c10 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q152.CATEGORY AS CATEGORY, q152.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c10 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_CAT_PRICE
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q152> label="q152" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +multi_column_value_indexmEXPLAIN select category, price from products_index_on where category = 'Electronics' order by category, price +0 뗒(t08H@COVERING(IDX_IOT_CAT_PRICE [EQUALS promote(@c10 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q152.CATEGORY AS CATEGORY, q152.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c10 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_CAT_PRICE
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q152> label="q152" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +f +multi_column_value_indexJEXPLAIN select name, rating from products_mat_view where name = 'Widget A' +q ˡ5(08@COVERING(IDX_MV_NAME_RATING [EQUALS promote(@c10 AS STRING)] -> [ID: KEY[3], NAME: KEY[0], RATING: KEY[1]]) | MAP (_.NAME AS NAME, _.RATING AS RATING) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q364.NAME AS NAME, q364.RATING AS RATING)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, DOUBLE AS RATING)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c10 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_NAME_RATING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q364> label="q364" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +f +multi_column_value_indexJEXPLAIN select name, rating from products_index_on where name = 'Widget A' +q 0(0ދ8@COVERING(IDX_IOT_NAME_RATING [EQUALS promote(@c10 AS STRING)] -> [ID: KEY[3], NAME: KEY[0], RATING: KEY[1]]) | MAP (_.NAME AS NAME, _.RATING AS RATING) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q364.NAME AS NAME, q364.RATING AS RATING)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, DOUBLE AS RATING)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c10 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_NAME_RATING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q364> label="q364" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +u +covering_value_index]EXPLAIN select price, name, supplier from products_mat_view where price > 15.0 order by price +ǔ (q0C8J@COVERING(IDX_MV_PRICE_COVERING [[GREATER_THAN promote(@c12 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.PRICE AS PRICE, _.NAME AS NAME, _.SUPPLIER AS SUPPLIER) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q166.PRICE AS PRICE, q166.NAME AS NAME, q166.SUPPLIER AS SUPPLIER)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE, STRING AS NAME, STRING AS SUPPLIER)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c12 AS DOUBLE)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_PRICE_COVERING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q166> label="q166" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +u +covering_value_index]EXPLAIN select price, name, supplier from products_index_on where price > 15.0 order by price +÷% ι(q0́p8J@COVERING(IDX_IOT_PRICE_COVERING [[GREATER_THAN promote(@c12 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.PRICE AS PRICE, _.NAME AS NAME, _.SUPPLIER AS SUPPLIER) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q166.PRICE AS PRICE, q166.NAME AS NAME, q166.SUPPLIER AS SUPPLIER)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE, STRING AS NAME, STRING AS SUPPLIER)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c12 AS DOUBLE)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_PRICE_COVERING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q166> label="q166" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +covering_value_indexzEXPLAIN select category, price, name, stock from products_mat_view where category = 'Electronics' order by category, price +Й, (n0O8?@COVERING(IDX_MV_CAT_PRICE_COVERING [EQUALS promote(@c14 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[3], NAME: VALUE[0], PRICE: KEY[1], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.NAME AS NAME, _.STOCK AS STOCK) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q152.CATEGORY AS CATEGORY, q152.PRICE AS PRICE, q152.NAME AS NAME, q152.STOCK AS STOCK)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, DOUBLE AS PRICE, STRING AS NAME, INT AS STOCK)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c14 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_CAT_PRICE_COVERING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q152> label="q152" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +covering_value_indexzEXPLAIN select category, price, name, stock from products_index_on where category = 'Electronics' order by category, price +- (n0^8?@COVERING(IDX_IOT_CAT_PRICE_COVERING [EQUALS promote(@c14 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[3], NAME: VALUE[0], PRICE: KEY[1], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.NAME AS NAME, _.STOCK AS STOCK) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q152.CATEGORY AS CATEGORY, q152.PRICE AS PRICE, q152.NAME AS NAME, q152.STOCK AS STOCK)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, DOUBLE AS PRICE, STRING AS NAME, INT AS STOCK)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c14 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_CAT_PRICE_COVERING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q152> label="q152" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +Z +mixed_asc_desc_ordering?EXPLAIN select price from products_mat_view order by price desc + | ٘(b0!8,@}COVERING(IDX_MV_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q131.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_PRICE_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q131> label="q131" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +Z +mixed_asc_desc_ordering?EXPLAIN select price from products_index_on order by price desc +| (b0S8,@~COVERING(IDX_IOT_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q131.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_PRICE_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q131> label="q131" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +r +mixed_asc_desc_orderingWEXPLAIN select category, price from products_mat_view order by category desc, price asc +c Ə(X08@COVERING(IDX_MV_CAT_DESC_PRICE_ASC <,> -> [CATEGORY: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.CATEGORY AS CATEGORY, q110.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_CAT_DESC_PRICE_ASC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q110> label="q110" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +r +mixed_asc_desc_orderingWEXPLAIN select category, price from products_index_on order by category desc, price asc +❙c (X0&8@COVERING(IDX_IOT_CAT_DESC_PRICE_ASC <,> -> [CATEGORY: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.CATEGORY AS CATEGORY, q110.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_CAT_DESC_PRICE_ASC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q110> label="q110" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +^ +nulls_orderingLEXPLAIN select rating from products_mat_view order by rating asc nulls first +c +(X0!8@bCOVERING(IDX_MV_RATING_NULLS_FIRST <,> -> [ID: KEY[2], RATING: KEY[0]]) | MAP (_.RATING AS RATING) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.RATING AS RATING)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS RATING)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_RATING_NULLS_FIRST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q110> label="q110" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +^ +nulls_orderingLEXPLAIN select rating from products_index_on order by rating asc nulls first +ϗ c (X08@cCOVERING(IDX_IOT_RATING_NULLS_FIRST <,> -> [ID: KEY[2], RATING: KEY[0]]) | MAP (_.RATING AS RATING) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.RATING AS RATING)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS RATING)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_RATING_NULLS_FIRST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q110> label="q110" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +] +nulls_orderingKEXPLAIN select rating from products_mat_view order by rating asc nulls last +ݤ c ǎ(X08@COVERING(IDX_MV_RATING_NULLS_LAST <,> -> [ID: KEY[2], RATING: from_ordered_bytes(KEY:[0], ASC_NULLS_LAST)]) | MAP (_.RATING AS RATING) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.RATING AS RATING)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS RATING)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_RATING_NULLS_LAST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q110> label="q110" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +] +nulls_orderingKEXPLAIN select rating from products_index_on order by rating asc nulls last + c (X08@COVERING(IDX_IOT_RATING_NULLS_LAST <,> -> [ID: KEY[2], RATING: from_ordered_bytes(KEY:[0], ASC_NULLS_LAST)]) | MAP (_.RATING AS RATING) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.RATING AS RATING)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS RATING)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_RATING_NULLS_LAST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q110> label="q110" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +c +nulls_orderingQEXPLAIN select supplier from products_mat_view order by supplier desc nulls first +c (X08@COVERING(IDX_MV_SUPPLIER_DESC_NULLS_FIRST <,> -> [ID: KEY[2], SUPPLIER: from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST)]) | MAP (_.SUPPLIER AS SUPPLIER) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.SUPPLIER AS SUPPLIER)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS SUPPLIER)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_SUPPLIER_DESC_NULLS_FIRST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q110> label="q110" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +c +nulls_orderingQEXPLAIN select supplier from products_index_on order by supplier desc nulls first + c ͷ(X08@COVERING(IDX_IOT_SUPPLIER_DESC_NULLS_FIRST <,> -> [ID: KEY[2], SUPPLIER: from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST)]) | MAP (_.SUPPLIER AS SUPPLIER) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q110.SUPPLIER AS SUPPLIER)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS SUPPLIER)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_SUPPLIER_DESC_NULLS_FIRST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q110> label="q110" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +U +reverse_scan_tests?EXPLAIN select price from products_mat_view order by price desc + | ٘(b0!8,@}COVERING(IDX_MV_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q131.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_PRICE_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q131> label="q131" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +U +reverse_scan_tests?EXPLAIN select price from products_index_on order by price desc +| (b0S8,@~COVERING(IDX_IOT_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q131.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_PRICE_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q131> label="q131" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +T +reverse_scan_tests>EXPLAIN select price from products_mat_view order by price asc +έ | Ρ(b008,@COVERING(IDX_MV_PRICE_DESC <,> REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q131.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
direction: reversed
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_PRICE_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q131> label="q131" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +T +reverse_scan_tests>EXPLAIN select price from products_index_on order by price asc + | 㑴(b0ɤ(8,@COVERING(IDX_IOT_PRICE_DESC <,> REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q131.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
direction: reversed
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_PRICE_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q131> label="q131" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +e +filtered_indexesQEXPLAIN select name, price from products_mat_view where price > 20 order by price +Ŗ' (w0o8Q@COVERING(IDX_MV_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q173.NAME AS NAME, q173.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c10 AS DOUBLE)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_PRICE_COVERING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q173> label="q173" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +e +filtered_indexesQEXPLAIN select name, price from products_index_on where price > 20 order by price +# в(u0V8N@COVERING(IDX_IOT_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q172.NAME AS NAME, q172.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c10 AS DOUBLE)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_PRICE_COVERING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q172> label="q172" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +y +filtered_indexeseEXPLAIN select name, price, stock from products_mat_view where category = 'Electronics' order by name +  (c0"8(@COVERING(IDX_MV_FILTERED_ELECTRONICS <,> -> [ID: KEY[2], NAME: KEY[0], PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE, _.STOCK AS STOCK) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q126.NAME AS NAME, q126.PRICE AS PRICE, q126.STOCK AS STOCK)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, DOUBLE AS PRICE, INT AS STOCK)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_FILTERED_ELECTRONICS
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q126> label="q126" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +y +filtered_indexeseEXPLAIN select name, price, stock from products_index_on where category = 'Electronics' order by name + (c0μE8(@COVERING(IDX_IOT_FILTERED_ELECTRONICS <,> -> [ID: KEY[2], NAME: KEY[0], PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE, _.STOCK AS STOCK) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q126.NAME AS NAME, q126.PRICE AS PRICE, q126.STOCK AS STOCK)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, DOUBLE AS PRICE, INT AS STOCK)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_FILTERED_ELECTRONICS
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q126> label="q126" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +filtered_indexesnEXPLAIN select category, price, stock from products_mat_view where price > 15 and stock > 60 order by category + 3 (08}@ COVERING(IDX_MV_FILTERED_MULTI <,> -> [CATEGORY: KEY[0], ID: KEY[2], PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.STOCK AS STOCK) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q223.CATEGORY AS CATEGORY, q223.PRICE AS PRICE, q223.STOCK AS STOCK)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, DOUBLE AS PRICE, INT AS STOCK)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_FILTERED_MULTI
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q223> label="q223" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +filtered_indexesnEXPLAIN select category, price, stock from products_index_on where price > 15 and stock > 60 order by category + ԕ5 (08}@ COVERING(IDX_IOT_FILTERED_MULTI <,> -> [CATEGORY: KEY[0], ID: KEY[2], PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.STOCK AS STOCK) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q223.CATEGORY AS CATEGORY, q223.PRICE AS PRICE, q223.STOCK AS STOCK)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS CATEGORY, DOUBLE AS PRICE, INT AS STOCK)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_FILTERED_MULTI
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q223> label="q223" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +e +filtered_indexesQEXPLAIN select name, price from products_mat_view where price > 50 order by price +Ŗ' (w0o8Q@COVERING(IDX_MV_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q173.NAME AS NAME, q173.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c10 AS DOUBLE)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_MV_PRICE_COVERING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q173> label="q173" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +e +filtered_indexesQEXPLAIN select name, price from products_index_on where price > 50 order by price +# в(u0V8N@COVERING(IDX_IOT_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q172.NAME AS NAME, q172.PRICE AS PRICE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, DOUBLE AS PRICE)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c10 AS DOUBLE)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 [ label=<
Index
IDX_IOT_PRICE_COVERING
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, DOUBLE AS PRICE, STRING AS CATEGORY, INT AS STOCK, STRING AS SUPPLIER, DOUBLE AS RATING)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q172> label="q172" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} \ No newline at end of file diff --git a/yaml-tests/src/test/resources/index-ddl-values-only.metrics.yaml b/yaml-tests/src/test/resources/index-ddl-values-only.metrics.yaml new file mode 100644 index 0000000000..38528f368b --- /dev/null +++ b/yaml-tests/src/test/resources/index-ddl-values-only.metrics.yaml @@ -0,0 +1,429 @@ +simple_value_index: +- query: EXPLAIN select price from products_mat_view where price > 18.0 order by + price + explain: 'COVERING(IDX_MV_PRICE_DESC [[LESS_THAN to_ordered_bytes(promote(@c8 + AS DOUBLE), DESC_NULLS_LAST)]] REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)' + task_count: 907 + task_total_time_ms: 69 + transform_count: 218 + transform_time_ms: 35 + transform_yield_count: 121 + insert_time_ms: 3 + insert_new_count: 88 + insert_reused_count: 5 +- query: EXPLAIN select price from products_index_on where price > 18.0 order by + price + explain: 'COVERING(IDX_IOT_PRICE_DESC [[LESS_THAN to_ordered_bytes(promote(@c8 + AS DOUBLE), DESC_NULLS_LAST)]] REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)' + task_count: 907 + task_total_time_ms: 342 + transform_count: 218 + transform_time_ms: 218 + transform_yield_count: 121 + insert_time_ms: 21 + insert_new_count: 88 + insert_reused_count: 5 +- query: EXPLAIN select category from products_mat_view where category = 'Electronics' + explain: 'COVERING(IDX_MV_CATEGORY [EQUALS promote(@c8 AS STRING)] -> [CATEGORY: + KEY[0], ID: KEY[2]]) | MAP (_.CATEGORY AS CATEGORY)' + task_count: 2234 + task_total_time_ms: 549 + transform_count: 538 + transform_time_ms: 297 + transform_yield_count: 194 + insert_time_ms: 37 + insert_new_count: 262 + insert_reused_count: 22 +- query: EXPLAIN select category from products_index_on where category = 'Electronics' + explain: 'COVERING(IDX_IOT_CATEGORY [EQUALS promote(@c8 AS STRING)] -> [CATEGORY: + KEY[0], ID: KEY[2]]) | MAP (_.CATEGORY AS CATEGORY)' + task_count: 2234 + task_total_time_ms: 546 + transform_count: 538 + transform_time_ms: 312 + transform_yield_count: 194 + insert_time_ms: 35 + insert_new_count: 262 + insert_reused_count: 22 +multi_column_value_index: +- query: EXPLAIN select category, price from products_mat_view where category = + 'Electronics' order by category, price + explain: 'COVERING(IDX_MV_CAT_PRICE [EQUALS promote(@c10 AS STRING)] -> [CATEGORY: + KEY[0], ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE + AS PRICE)' + task_count: 785 + task_total_time_ms: 68 + transform_count: 199 + transform_time_ms: 34 + transform_yield_count: 116 + insert_time_ms: 1 + insert_new_count: 72 + insert_reused_count: 3 +- query: EXPLAIN select category, price from products_index_on where category = + 'Electronics' order by category, price + explain: 'COVERING(IDX_IOT_CAT_PRICE [EQUALS promote(@c10 AS STRING)] -> [CATEGORY: + KEY[0], ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE + AS PRICE)' + task_count: 785 + task_total_time_ms: 100 + transform_count: 199 + transform_time_ms: 54 + transform_yield_count: 116 + insert_time_ms: 2 + insert_new_count: 72 + insert_reused_count: 3 +- query: EXPLAIN select name, rating from products_mat_view where name = 'Widget + A' + explain: 'COVERING(IDX_MV_NAME_RATING [EQUALS promote(@c10 AS STRING)] -> [ID: + KEY[3], NAME: KEY[0], RATING: KEY[1]]) | MAP (_.NAME AS NAME, _.RATING AS + RATING)' + task_count: 2055 + task_total_time_ms: 238 + transform_count: 512 + transform_time_ms: 111 + transform_yield_count: 178 + insert_time_ms: 13 + insert_new_count: 246 + insert_reused_count: 22 +- query: EXPLAIN select name, rating from products_index_on where name = 'Widget + A' + explain: 'COVERING(IDX_IOT_NAME_RATING [EQUALS promote(@c10 AS STRING)] -> [ID: + KEY[3], NAME: KEY[0], RATING: KEY[1]]) | MAP (_.NAME AS NAME, _.RATING AS + RATING)' + task_count: 2055 + task_total_time_ms: 237 + transform_count: 512 + transform_time_ms: 102 + transform_yield_count: 178 + insert_time_ms: 11 + insert_new_count: 246 + insert_reused_count: 22 +covering_value_index: +- query: EXPLAIN select price, name, supplier from products_mat_view where price + > 15.0 order by price + explain: 'COVERING(IDX_MV_PRICE_COVERING [[GREATER_THAN promote(@c12 AS DOUBLE)]] + -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP + (_.PRICE AS PRICE, _.NAME AS NAME, _.SUPPLIER AS SUPPLIER)' + task_count: 789 + task_total_time_ms: 37 + transform_count: 204 + transform_time_ms: 19 + transform_yield_count: 113 + insert_time_ms: 1 + insert_new_count: 74 + insert_reused_count: 3 +- query: EXPLAIN select price, name, supplier from products_index_on where price + > 15.0 order by price + explain: 'COVERING(IDX_IOT_PRICE_COVERING [[GREATER_THAN promote(@c12 AS DOUBLE)]] + -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP + (_.PRICE AS PRICE, _.NAME AS NAME, _.SUPPLIER AS SUPPLIER)' + task_count: 789 + task_total_time_ms: 78 + transform_count: 204 + transform_time_ms: 42 + transform_yield_count: 113 + insert_time_ms: 1 + insert_new_count: 74 + insert_reused_count: 3 +- query: EXPLAIN select category, price, name, stock from products_mat_view where + category = 'Electronics' order by category, price + explain: 'COVERING(IDX_MV_CAT_PRICE_COVERING [EQUALS promote(@c14 AS STRING)] + -> [CATEGORY: KEY[0], ID: KEY[3], NAME: VALUE[0], PRICE: KEY[1], STOCK: VALUE[1]]) + | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.NAME AS NAME, _.STOCK AS + STOCK)' + task_count: 698 + task_total_time_ms: 92 + transform_count: 188 + transform_time_ms: 53 + transform_yield_count: 110 + insert_time_ms: 1 + insert_new_count: 63 + insert_reused_count: 3 +- query: EXPLAIN select category, price, name, stock from products_index_on where + category = 'Electronics' order by category, price + explain: 'COVERING(IDX_IOT_CAT_PRICE_COVERING [EQUALS promote(@c14 AS STRING)] + -> [CATEGORY: KEY[0], ID: KEY[3], NAME: VALUE[0], PRICE: KEY[1], STOCK: VALUE[1]]) + | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.NAME AS NAME, _.STOCK AS + STOCK)' + task_count: 698 + task_total_time_ms: 96 + transform_count: 188 + transform_time_ms: 55 + transform_yield_count: 110 + insert_time_ms: 1 + insert_new_count: 63 + insert_reused_count: 3 +mixed_asc_desc_ordering: +- query: EXPLAIN select price from products_mat_view order by price desc + explain: 'COVERING(IDX_MV_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)' + task_count: 488 + task_total_time_ms: 24 + transform_count: 124 + transform_time_ms: 15 + transform_yield_count: 98 + insert_time_ms: 0 + insert_new_count: 44 + insert_reused_count: 6 +- query: EXPLAIN select price from products_index_on order by price desc + explain: 'COVERING(IDX_IOT_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)' + task_count: 488 + task_total_time_ms: 51 + transform_count: 124 + transform_time_ms: 26 + transform_yield_count: 98 + insert_time_ms: 1 + insert_new_count: 44 + insert_reused_count: 6 +- query: EXPLAIN select category, price from products_mat_view order by category + desc, price asc + explain: 'COVERING(IDX_MV_CAT_DESC_PRICE_ASC <,> -> [CATEGORY: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST), ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, + _.PRICE AS PRICE)' + task_count: 342 + task_total_time_ms: 42 + transform_count: 99 + transform_time_ms: 29 + transform_yield_count: 88 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN select category, price from products_index_on order by category + desc, price asc + explain: 'COVERING(IDX_IOT_CAT_DESC_PRICE_ASC <,> -> [CATEGORY: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST), ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, + _.PRICE AS PRICE)' + task_count: 342 + task_total_time_ms: 46 + transform_count: 99 + transform_time_ms: 27 + transform_yield_count: 88 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +nulls_ordering: +- query: EXPLAIN select rating from products_mat_view order by rating asc nulls + first + explain: 'COVERING(IDX_MV_RATING_NULLS_FIRST <,> -> [ID: KEY[2], RATING: KEY[0]]) + | MAP (_.RATING AS RATING)' + task_count: 342 + task_total_time_ms: 32 + transform_count: 99 + transform_time_ms: 21 + transform_yield_count: 88 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN select rating from products_index_on order by rating asc nulls + first + explain: 'COVERING(IDX_IOT_RATING_NULLS_FIRST <,> -> [ID: KEY[2], RATING: KEY[0]]) + | MAP (_.RATING AS RATING)' + task_count: 342 + task_total_time_ms: 26 + transform_count: 99 + transform_time_ms: 16 + transform_yield_count: 88 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN select rating from products_mat_view order by rating asc nulls + last + explain: 'COVERING(IDX_MV_RATING_NULLS_LAST <,> -> [ID: KEY[2], RATING: from_ordered_bytes(KEY:[0], + ASC_NULLS_LAST)]) | MAP (_.RATING AS RATING)' + task_count: 342 + task_total_time_ms: 27 + transform_count: 99 + transform_time_ms: 17 + transform_yield_count: 88 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN select rating from products_index_on order by rating asc nulls + last + explain: 'COVERING(IDX_IOT_RATING_NULLS_LAST <,> -> [ID: KEY[2], RATING: from_ordered_bytes(KEY:[0], + ASC_NULLS_LAST)]) | MAP (_.RATING AS RATING)' + task_count: 342 + task_total_time_ms: 26 + transform_count: 99 + transform_time_ms: 16 + transform_yield_count: 88 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN select supplier from products_mat_view order by supplier desc nulls + first + explain: 'COVERING(IDX_MV_SUPPLIER_DESC_NULLS_FIRST <,> -> [ID: KEY[2], SUPPLIER: + from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST)]) | MAP (_.SUPPLIER AS SUPPLIER)' + task_count: 342 + task_total_time_ms: 31 + transform_count: 99 + transform_time_ms: 19 + transform_yield_count: 88 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN select supplier from products_index_on order by supplier desc nulls + first + explain: 'COVERING(IDX_IOT_SUPPLIER_DESC_NULLS_FIRST <,> -> [ID: KEY[2], SUPPLIER: + from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST)]) | MAP (_.SUPPLIER AS SUPPLIER)' + task_count: 342 + task_total_time_ms: 28 + transform_count: 99 + transform_time_ms: 17 + transform_yield_count: 88 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +reverse_scan_tests: +- query: EXPLAIN select price from products_mat_view order by price desc + explain: 'COVERING(IDX_MV_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)' + task_count: 488 + task_total_time_ms: 24 + transform_count: 124 + transform_time_ms: 15 + transform_yield_count: 98 + insert_time_ms: 0 + insert_new_count: 44 + insert_reused_count: 6 +- query: EXPLAIN select price from products_index_on order by price desc + explain: 'COVERING(IDX_IOT_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)' + task_count: 488 + task_total_time_ms: 51 + transform_count: 124 + transform_time_ms: 26 + transform_yield_count: 98 + insert_time_ms: 1 + insert_new_count: 44 + insert_reused_count: 6 +- query: EXPLAIN select price from products_mat_view order by price asc + explain: 'COVERING(IDX_MV_PRICE_DESC <,> REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)' + task_count: 488 + task_total_time_ms: 25 + transform_count: 124 + transform_time_ms: 12 + transform_yield_count: 98 + insert_time_ms: 0 + insert_new_count: 44 + insert_reused_count: 6 +- query: EXPLAIN select price from products_index_on order by price asc + explain: 'COVERING(IDX_IOT_PRICE_DESC <,> REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)' + task_count: 488 + task_total_time_ms: 27 + transform_count: 124 + transform_time_ms: 17 + transform_yield_count: 98 + insert_time_ms: 0 + insert_new_count: 44 + insert_reused_count: 6 +filtered_indexes: +- query: EXPLAIN select name, price from products_mat_view where price > 20 order + by price + explain: 'COVERING(IDX_MV_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] + -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP + (_.NAME AS NAME, _.PRICE AS PRICE)' + task_count: 872 + task_total_time_ms: 83 + transform_count: 224 + transform_time_ms: 42 + transform_yield_count: 119 + insert_time_ms: 1 + insert_new_count: 81 + insert_reused_count: 3 +- query: EXPLAIN select name, price from products_index_on where price > 20 order + by price + explain: 'COVERING(IDX_IOT_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] + -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP + (_.NAME AS NAME, _.PRICE AS PRICE)' + task_count: 844 + task_total_time_ms: 73 + transform_count: 220 + transform_time_ms: 38 + transform_yield_count: 117 + insert_time_ms: 1 + insert_new_count: 78 + insert_reused_count: 3 +- query: EXPLAIN select name, price, stock from products_mat_view where category + = 'Electronics' order by name + explain: 'COVERING(IDX_MV_FILTERED_ELECTRONICS <,> -> [ID: KEY[2], NAME: KEY[0], + PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE, + _.STOCK AS STOCK)' + task_count: 507 + task_total_time_ms: 26 + transform_count: 149 + transform_time_ms: 14 + transform_yield_count: 99 + insert_time_ms: 0 + insert_new_count: 40 + insert_reused_count: 2 +- query: EXPLAIN select name, price, stock from products_index_on where category + = 'Electronics' order by name + explain: 'COVERING(IDX_IOT_FILTERED_ELECTRONICS <,> -> [ID: KEY[2], NAME: KEY[0], + PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE, + _.STOCK AS STOCK)' + task_count: 507 + task_total_time_ms: 58 + transform_count: 149 + transform_time_ms: 33 + transform_yield_count: 99 + insert_time_ms: 1 + insert_new_count: 40 + insert_reused_count: 2 +- query: EXPLAIN select category, price, stock from products_mat_view where price + > 15 and stock > 60 order by category + explain: 'COVERING(IDX_MV_FILTERED_MULTI <,> -> [CATEGORY: KEY[0], ID: KEY[2], + PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE + AS PRICE, _.STOCK AS STOCK)' + task_count: 1164 + task_total_time_ms: 108 + transform_count: 288 + transform_time_ms: 55 + transform_yield_count: 130 + insert_time_ms: 3 + insert_new_count: 125 + insert_reused_count: 9 +- query: EXPLAIN select category, price, stock from products_index_on where price + > 15 and stock > 60 order by category + explain: 'COVERING(IDX_IOT_FILTERED_MULTI <,> -> [CATEGORY: KEY[0], ID: KEY[2], + PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE + AS PRICE, _.STOCK AS STOCK)' + task_count: 1164 + task_total_time_ms: 111 + transform_count: 288 + transform_time_ms: 58 + transform_yield_count: 130 + insert_time_ms: 3 + insert_new_count: 125 + insert_reused_count: 9 +- query: EXPLAIN select name, price from products_mat_view where price > 50 order + by price + explain: 'COVERING(IDX_MV_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] + -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP + (_.NAME AS NAME, _.PRICE AS PRICE)' + task_count: 872 + task_total_time_ms: 83 + transform_count: 224 + transform_time_ms: 42 + transform_yield_count: 119 + insert_time_ms: 1 + insert_new_count: 81 + insert_reused_count: 3 +- query: EXPLAIN select name, price from products_index_on where price > 50 order + by price + explain: 'COVERING(IDX_IOT_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] + -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP + (_.NAME AS NAME, _.PRICE AS PRICE)' + task_count: 844 + task_total_time_ms: 73 + transform_count: 220 + transform_time_ms: 38 + transform_yield_count: 117 + insert_time_ms: 1 + insert_new_count: 78 + insert_reused_count: 3 diff --git a/yaml-tests/src/test/resources/index-ddl-values-only.yamsql b/yaml-tests/src/test/resources/index-ddl-values-only.yamsql new file mode 100644 index 0000000000..8cce2a41e8 --- /dev/null +++ b/yaml-tests/src/test/resources/index-ddl-values-only.yamsql @@ -0,0 +1,323 @@ +# +# index-ddl-values-only.yamsql +# +# This source file is part of the FoundationDB open source project +# +# Copyright 2021-2025 Apple Inc. and the FoundationDB project authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Test VALUES indexes with different orderings +# Compares CREATE INDEX AS SELECT vs CREATE INDEX ON syntax +# Both syntaxes should produce equivalent query plans + +--- +options: + supported_version: !current_version +--- +schema_template: + create table products_mat_view( + id integer, + name string, + price double, + category string, + stock integer, + supplier string, + rating double, + primary key(id) + ) + + create index idx_mv_price as select price from products_mat_view order by price + create index idx_mv_category as select category from products_mat_view order by category + + create index idx_mv_cat_price as select category, price from products_mat_view order by category, price + create index idx_mv_name_rating as select name, rating from products_mat_view order by name, rating + + create index idx_mv_price_covering as select price, name, supplier from products_mat_view order by price + create index idx_mv_cat_price_covering as select category, price, name, stock from products_mat_view order by category, price + + create index idx_mv_cat_desc_price_asc as select category, price from products_mat_view order by category desc, price asc + create index idx_mv_price_desc as select price from products_mat_view order by price desc + + create index idx_mv_rating_nulls_first as select rating from products_mat_view order by rating asc nulls first + create index idx_mv_rating_nulls_last as select rating from products_mat_view order by rating asc nulls last + create index idx_mv_supplier_desc_nulls_first as select supplier from products_mat_view order by supplier desc nulls first + + create index idx_mv_filtered_expensive as select name, price from products_mat_view where price > 20 order by price + create index idx_mv_filtered_electronics as select name, price, stock from products_mat_view where category = 'Electronics' order by name + create index idx_mv_filtered_multi as select category, price, stock from products_mat_view where price > 15 and stock > 60 order by category + + create table products_index_on( + id integer, + name string, + price double, + category string, + stock integer, + supplier string, + rating double, + primary key(id) + ) + + create index idx_iot_price on products_index_on(price) + create index idx_iot_category on products_index_on(category) + + create index idx_iot_cat_price on products_index_on(category, price) + create index idx_iot_name_rating on products_index_on(name, rating) + + create index idx_iot_price_covering on products_index_on(price) include(name, supplier) + create index idx_iot_cat_price_covering on products_index_on(category, price) include(name, stock) + + create index idx_iot_cat_desc_price_asc on products_index_on(category desc, price asc) + create index idx_iot_price_desc on products_index_on(price desc) + + create index idx_iot_rating_nulls_first on products_index_on(rating asc nulls first) + create index idx_iot_rating_nulls_last on products_index_on(rating asc nulls last) + create index idx_iot_supplier_desc_nulls_first on products_index_on(supplier desc nulls first) + + create view v_iot_expensive as select name, price from products_index_on where price > 20 + create index idx_iot_filtered_expensive on v_iot_expensive(price) + + create view v_iot_electronics as select name, price, stock from products_index_on where category = 'Electronics' + create index idx_iot_filtered_electronics on v_iot_electronics(name) include (price, stock) + + create view v_iot_multi_filter as select category, price, stock from products_index_on where price > 15 and stock > 60 + create index idx_iot_filtered_multi on v_iot_multi_filter(category) include(price, stock) + +--- +setup: + steps: + # Insert test data with NULLs to test NULLS ordering + # Additional rows added for filtered index tests + - query: INSERT INTO products_mat_view VALUES + (1, 'Widget A', 19.99, 'Electronics', 100, 'SupplierX', 4.5), + (2, 'Widget B', 29.99, 'Electronics', 50, 'SupplierY', NULL), + (3, 'Gadget C', NULL, 'Home', 75, NULL, 3.8), + (4, 'Tool D', 15.50, NULL, 200, 'SupplierZ', 4.2), + (5, 'Phone X', 799.99, 'Electronics', 120, 'SupplierX', 4.8), + (6, 'Tablet Y', 299.99, 'Electronics', 80, 'SupplierY', 4.3), + (7, 'Speaker Z', 89.99, 'Electronics', 150, 'SupplierZ', 4.1), + (8, 'Lamp A', 25.50, 'Home', 90, 'SupplierX', 3.9), + (9, 'Chair B', 12.99, 'Home', 30, 'SupplierY', 4.0) + + - query: INSERT INTO products_index_on VALUES + (1, 'Widget A', 19.99, 'Electronics', 100, 'SupplierX', 4.5), + (2, 'Widget B', 29.99, 'Electronics', 50, 'SupplierY', NULL), + (3, 'Gadget C', NULL, 'Home', 75, NULL, 3.8), + (4, 'Tool D', 15.50, NULL, 200, 'SupplierZ', 4.2), + (5, 'Phone X', 799.99, 'Electronics', 120, 'SupplierX', 4.8), + (6, 'Tablet Y', 299.99, 'Electronics', 80, 'SupplierY', 4.3), + (7, 'Speaker Z', 89.99, 'Electronics', 150, 'SupplierZ', 4.1), + (8, 'Lamp A', 25.50, 'Home', 90, 'SupplierX', 3.9), + (9, 'Chair B', 12.99, 'Home', 30, 'SupplierY', 4.0) + + +--- +test_block: + name: simple_value_index + tests: + # Test simple single-column value index on price + - + - query: select price from products_mat_view where price > 18.0 order by price + - explain: "COVERING(IDX_MV_PRICE_DESC [[LESS_THAN to_ordered_bytes(promote(@c8 AS DOUBLE), DESC_NULLS_LAST)]] REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)" + - result: [{ 19.99 }, { 25.50 }, { 29.99 }, { 89.99 }, { 299.99 }, { 799.99 }] + - + - query: select price from products_index_on where price > 18.0 order by price + - explain: "COVERING(IDX_IOT_PRICE_DESC [[LESS_THAN to_ordered_bytes(promote(@c8 AS DOUBLE), DESC_NULLS_LAST)]] REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)" + - result: [{ 19.99 }, { 25.50 }, { 29.99 }, { 89.99 }, { 299.99 }, { 799.99 }] + + # Test simple single-column value index on category + - + - query: select category from products_mat_view where category = 'Electronics' + - explain: "COVERING(IDX_MV_CATEGORY [EQUALS promote(@c8 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[2]]) | MAP (_.CATEGORY AS CATEGORY)" + - result: [{ "Electronics" }, { "Electronics" }, { "Electronics" }, { "Electronics" }, { "Electronics" }] + - + - query: select category from products_index_on where category = 'Electronics' + - explain: "COVERING(IDX_IOT_CATEGORY [EQUALS promote(@c8 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[2]]) | MAP (_.CATEGORY AS CATEGORY)" + - result: [{ "Electronics" }, { "Electronics" }, { "Electronics" }, { "Electronics" }, { "Electronics" }] + +--- +test_block: + name: multi_column_value_index + tests: + # Test multi-column index (category, price) + - + - query: select category, price from products_mat_view where category = 'Electronics' order by category, price + - explain: "COVERING(IDX_MV_CAT_PRICE [EQUALS promote(@c10 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE)" + - result: [{ "Electronics", 19.99 }, { "Electronics", 29.99 }, { "Electronics", 89.99 }, { "Electronics", 299.99 }, { "Electronics", 799.99 }] + - + - query: select category, price from products_index_on where category = 'Electronics' order by category, price + - explain: "COVERING(IDX_IOT_CAT_PRICE [EQUALS promote(@c10 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE)" + - result: [{ "Electronics", 19.99 }, { "Electronics", 29.99 }, { "Electronics", 89.99 }, { "Electronics", 299.99 }, { "Electronics", 799.99 }] + + # Test multi-column index (name, rating) + - + - query: select name, rating from products_mat_view where name = 'Widget A' + - explain: "COVERING(IDX_MV_NAME_RATING [EQUALS promote(@c10 AS STRING)] -> [ID: KEY[3], NAME: KEY[0], RATING: KEY[1]]) | MAP (_.NAME AS NAME, _.RATING AS RATING)" + - result: [{ "Widget A", 4.5 }] + - + - query: select name, rating from products_index_on where name = 'Widget A' + - explain: "COVERING(IDX_IOT_NAME_RATING [EQUALS promote(@c10 AS STRING)] -> [ID: KEY[3], NAME: KEY[0], RATING: KEY[1]]) | MAP (_.NAME AS NAME, _.RATING AS RATING)" + - result: [{ "Widget A", 4.5 }] + +--- +test_block: + name: covering_value_index + tests: + # Test covering index - price in key, name and supplier in values + - + - query: select price, name, supplier from products_mat_view where price > 15.0 order by price + - explain: "COVERING(IDX_MV_PRICE_COVERING [[GREATER_THAN promote(@c12 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.PRICE AS PRICE, _.NAME AS NAME, _.SUPPLIER AS SUPPLIER)" + - result: [{ 15.50, "Tool D", "SupplierZ" }, { 19.99, "Widget A", "SupplierX" }, { 25.50, "Lamp A", "SupplierX" }, { 29.99, "Widget B", "SupplierY" }, { 89.99, "Speaker Z", "SupplierZ" }, { 299.99, "Tablet Y", "SupplierY" }, { 799.99, "Phone X", "SupplierX" }] + - + - query: select price, name, supplier from products_index_on where price > 15.0 order by price + - explain: "COVERING(IDX_IOT_PRICE_COVERING [[GREATER_THAN promote(@c12 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.PRICE AS PRICE, _.NAME AS NAME, _.SUPPLIER AS SUPPLIER)" + - result: [{ 15.50, "Tool D", "SupplierZ" }, { 19.99, "Widget A", "SupplierX" }, { 25.50, "Lamp A", "SupplierX" }, { 29.99, "Widget B", "SupplierY" }, { 89.99, "Speaker Z", "SupplierZ" }, { 299.99, "Tablet Y", "SupplierY" }, { 799.99, "Phone X", "SupplierX" }] + + # Test covering index - category and price in key, name and stock in values + - + - query: select category, price, name, stock from products_mat_view where category = 'Electronics' order by category, price + - explain: "COVERING(IDX_MV_CAT_PRICE_COVERING [EQUALS promote(@c14 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[3], NAME: VALUE[0], PRICE: KEY[1], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.NAME AS NAME, _.STOCK AS STOCK)" + - result: [{ "Electronics", 19.99, "Widget A", 100 }, { "Electronics", 29.99, "Widget B", 50 }, { "Electronics", 89.99, "Speaker Z", 150 }, { "Electronics", 299.99, "Tablet Y", 80 }, { "Electronics", 799.99, "Phone X", 120 }] + - + - query: select category, price, name, stock from products_index_on where category = 'Electronics' order by category, price + - explain: "COVERING(IDX_IOT_CAT_PRICE_COVERING [EQUALS promote(@c14 AS STRING)] -> [CATEGORY: KEY[0], ID: KEY[3], NAME: VALUE[0], PRICE: KEY[1], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.NAME AS NAME, _.STOCK AS STOCK)" + - result: [{ "Electronics", 19.99, "Widget A", 100 }, { "Electronics", 29.99, "Widget B", 50 }, { "Electronics", 89.99, "Speaker Z", 150 }, { "Electronics", 299.99, "Tablet Y", 80 }, { "Electronics", 799.99, "Phone X", 120 }] + +--- +test_block: + name: mixed_asc_desc_ordering + tests: + # Test DESC ordering on price + - + - query: select price from products_mat_view order by price desc + - explain: "COVERING(IDX_MV_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)" + - result: [{ 799.99 }, { 299.99 }, { 89.99 }, { 29.99 }, { 25.50 }, { 19.99 }, { 15.50 }, { 12.99 }, { !null }] + - + - query: select price from products_index_on order by price desc + - explain: "COVERING(IDX_IOT_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)" + - result: [{ 799.99 }, { 299.99 }, { 89.99 }, { 29.99 }, { 25.50 }, { 19.99 }, { 15.50 }, { 12.99 }, { !null }] + + # Test mixed ASC/DESC (category DESC, price ASC) + - + - query: select category, price from products_mat_view order by category desc, price asc + - explain: "COVERING(IDX_MV_CAT_DESC_PRICE_ASC <,> -> [CATEGORY: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE)" + - result: [{ "Home", !null }, { "Home", 12.99 }, { "Home", 25.50 }, { "Electronics", 19.99 }, { "Electronics", 29.99 }, { "Electronics", 89.99 }, { "Electronics", 299.99 }, { "Electronics", 799.99 }, { !null , 15.50 }] + - + - query: select category, price from products_index_on order by category desc, price asc + - explain: "COVERING(IDX_IOT_CAT_DESC_PRICE_ASC <,> -> [CATEGORY: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[3], PRICE: KEY[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE)" + - result: [{ "Home", !null }, { "Home", 12.99 }, { "Home", 25.50 }, { "Electronics", 19.99 }, { "Electronics", 29.99 }, { "Electronics", 89.99 }, { "Electronics", 299.99 }, { "Electronics", 799.99 }, { !null , 15.50 }] + +--- +test_block: + name: nulls_ordering + tests: + # Test NULLS FIRST ordering on rating + - + - query: select rating from products_mat_view order by rating asc nulls first + - explain: "COVERING(IDX_MV_RATING_NULLS_FIRST <,> -> [ID: KEY[2], RATING: KEY[0]]) | MAP (_.RATING AS RATING)" + - result: [{ !null }, { 3.8 }, { 3.9 }, { 4.0 }, { 4.1 }, { 4.2 }, { 4.3 }, { 4.5 }, { 4.8 }] + - + - query: select rating from products_index_on order by rating asc nulls first + - explain: "COVERING(IDX_IOT_RATING_NULLS_FIRST <,> -> [ID: KEY[2], RATING: KEY[0]]) | MAP (_.RATING AS RATING)" + - result: [{ !null }, { 3.8 }, { 3.9 }, { 4.0 }, { 4.1 }, { 4.2 }, { 4.3 }, { 4.5 }, { 4.8 }] + + # Test NULLS LAST ordering on rating + - + - query: select rating from products_mat_view order by rating asc nulls last + - explain: "COVERING(IDX_MV_RATING_NULLS_LAST <,> -> [ID: KEY[2], RATING: from_ordered_bytes(KEY:[0], ASC_NULLS_LAST)]) | MAP (_.RATING AS RATING)" + - result: [{ 3.8 }, { 3.9 }, { 4.0 }, { 4.1 }, { 4.2 }, { 4.3 }, { 4.5 }, { 4.8 }, { !null }] + - + - query: select rating from products_index_on order by rating asc nulls last + - explain: "COVERING(IDX_IOT_RATING_NULLS_LAST <,> -> [ID: KEY[2], RATING: from_ordered_bytes(KEY:[0], ASC_NULLS_LAST)]) | MAP (_.RATING AS RATING)" + - result: [{ 3.8 }, { 3.9 }, { 4.0 }, { 4.1 }, { 4.2 }, { 4.3 }, { 4.5 }, { 4.8 }, { !null }] + + # Test DESC NULLS FIRST on supplier + - + - query: select supplier from products_mat_view order by supplier desc nulls first + - explain: "COVERING(IDX_MV_SUPPLIER_DESC_NULLS_FIRST <,> -> [ID: KEY[2], SUPPLIER: from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST)]) | MAP (_.SUPPLIER AS SUPPLIER)" + - result: [{ !null }, { "SupplierZ" }, { "SupplierZ" }, { "SupplierY" }, { "SupplierY" }, { "SupplierY" }, { "SupplierX" }, { "SupplierX" }, { "SupplierX" }] + - + - query: select supplier from products_index_on order by supplier desc nulls first + - explain: "COVERING(IDX_IOT_SUPPLIER_DESC_NULLS_FIRST <,> -> [ID: KEY[2], SUPPLIER: from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST)]) | MAP (_.SUPPLIER AS SUPPLIER)" + - result: [{ !null }, { "SupplierZ" }, { "SupplierZ" }, { "SupplierY" }, { "SupplierY" }, { "SupplierY" }, { "SupplierX" }, { "SupplierX" }, { "SupplierX" }] + +--- +test_block: + name: reverse_scan_tests + tests: + # Test reverse scan - query DESC on ASC index + - + - query: select price from products_mat_view order by price desc + - explain: "COVERING(IDX_MV_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)" + - result: [{ 799.99 }, { 299.99 }, { 89.99 }, { 29.99 }, { 25.50 }, { 19.99 }, { 15.50 }, { 12.99 }, { !null }] + - + - query: select price from products_index_on order by price desc + - explain: "COVERING(IDX_IOT_PRICE_DESC <,> -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)" + - result: [{ 799.99 }, { 299.99 }, { 89.99 }, { 29.99 }, { 25.50 }, { 19.99 }, { 15.50 }, { 12.99 }, { !null }] + + # Test reverse scan - query ASC on DESC index + - + - query: select price from products_mat_view order by price asc + - explain: "COVERING(IDX_MV_PRICE_DESC <,> REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)" + - result: [{ !null }, { 12.99 }, { 15.50 }, { 19.99 }, { 25.50 }, { 29.99 }, { 89.99 }, { 299.99 }, { 799.99 }] + - + - query: select price from products_index_on order by price asc + - explain: "COVERING(IDX_IOT_PRICE_DESC <,> REVERSE -> [ID: KEY[2], PRICE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST)]) | MAP (_.PRICE AS PRICE)" + - result: [{ !null }, { 12.99 }, { 15.50 }, { 19.99 }, { 25.50 }, { 29.99 }, { 89.99 }, { 299.99 }, { 799.99 }] + +--- +test_block: + name: filtered_indexes + tests: + # Test filtered index - expensive products (price > 20) + - + - query: select name, price from products_mat_view where price > 20 order by price + - explain: "COVERING(IDX_MV_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE)" + - result: [{ "Lamp A", 25.50 }, { "Widget B", 29.99 }, { "Speaker Z", 89.99 }, { "Tablet Y", 299.99 }, { "Phone X", 799.99 }] + - + - query: select name, price from products_index_on where price > 20 order by price + - explain: "COVERING(IDX_IOT_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE)" + - result: [{ "Lamp A", 25.50 }, { "Widget B", 29.99 }, { "Speaker Z", 89.99 }, { "Tablet Y", 299.99 }, { "Phone X", 799.99 }] + + # Test filtered index - electronics category + - + - query: select name, price, stock from products_mat_view where category = 'Electronics' order by name + - explain: "COVERING(IDX_MV_FILTERED_ELECTRONICS <,> -> [ID: KEY[2], NAME: KEY[0], PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE, _.STOCK AS STOCK)" + - result: [{ "Phone X", 799.99, 120 }, { "Speaker Z", 89.99, 150 }, { "Tablet Y", 299.99, 80 }, { "Widget A", 19.99, 100 }, { "Widget B", 29.99, 50 }] + - + - query: select name, price, stock from products_index_on where category = 'Electronics' order by name + - explain: "COVERING(IDX_IOT_FILTERED_ELECTRONICS <,> -> [ID: KEY[2], NAME: KEY[0], PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE, _.STOCK AS STOCK)" + - result: [{ "Phone X", 799.99, 120 }, { "Speaker Z", 89.99, 150 }, { "Tablet Y", 299.99, 80 }, { "Widget A", 19.99, 100 }, { "Widget B", 29.99, 50 }] + + # Test filtered index - multiple predicates (price > 15 AND stock > 60) + - + - query: select category, price, stock from products_mat_view where price > 15 and stock > 60 order by category + - explain: "COVERING(IDX_MV_FILTERED_MULTI <,> -> [CATEGORY: KEY[0], ID: KEY[2], PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.STOCK AS STOCK)" + - unorderedResult: [{!null _, 15.5, 200}, { "Electronics", 19.99, 100 }, { "Electronics", 89.99, 150 }, { "Electronics", 299.99, 80 }, { "Electronics", 799.99, 120 }, { "Home", 25.50, 90 }] + - + - query: select category, price, stock from products_index_on where price > 15 and stock > 60 order by category + - explain: "COVERING(IDX_IOT_FILTERED_MULTI <,> -> [CATEGORY: KEY[0], ID: KEY[2], PRICE: VALUE[0], STOCK: VALUE[1]]) | MAP (_.CATEGORY AS CATEGORY, _.PRICE AS PRICE, _.STOCK AS STOCK)" + - unorderedResult: [{!null _, 15.5, 200}, { "Electronics", 19.99, 100 }, { "Electronics", 89.99, 150 }, { "Electronics", 299.99, 80 }, { "Electronics", 799.99, 120 }, { "Home", 25.50, 90 }] + + # Test filtered index with additional filter in query + - + - query: select name, price from products_mat_view where price > 50 order by price + - explain: "COVERING(IDX_MV_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE)" + - result: [{ "Speaker Z", 89.99 }, { "Tablet Y", 299.99 }, { "Phone X", 799.99 }] + - + - query: select name, price from products_index_on where price > 50 order by price + - explain: "COVERING(IDX_IOT_PRICE_COVERING [[GREATER_THAN promote(@c10 AS DOUBLE)]] -> [ID: KEY[2], NAME: VALUE[0], PRICE: KEY[0], SUPPLIER: VALUE[1]]) | MAP (_.NAME AS NAME, _.PRICE AS PRICE)" + - result: [{ "Speaker Z", 89.99 }, { "Tablet Y", 299.99 }, { "Phone X", 799.99 }] + +... + diff --git a/yaml-tests/src/test/resources/index-ddl.metrics.binpb b/yaml-tests/src/test/resources/index-ddl.metrics.binpb new file mode 100644 index 0000000000..1ae6b9fe8f --- /dev/null +++ b/yaml-tests/src/test/resources/index-ddl.metrics.binpb @@ -0,0 +1,535 @@ + +X + basic_indexIEXPLAIN select name from customers_materialized_view where name = 'Alice' +Χ b(0 8@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) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q347.NAME AS NAME)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c8 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_INCLUDE
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q347> label="q347" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +U + basic_indexFEXPLAIN select name from customers_index_on_table where name = 'Alice' +o ҕ=(08@COVERING(IDX_IOT_INCLUDE [EQUALS promote(@c8 AS STRING)] -> [COUNTRY: VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q347.NAME AS NAME)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c8 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_INCLUDE
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q347> label="q347" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +X + basic_indexIEXPLAIN select email from customers_materialized_view order by email desc +Ui D(\08@xCOVERING(IDX_MV_EMAIL <,> -> [EMAIL: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[2]]) | MAP (_.EMAIL AS EMAIL) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q126.EMAIL AS EMAIL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS EMAIL)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_EMAIL
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q126> label="q126" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +U + basic_indexFEXPLAIN select email from customers_index_on_table order by email desc +셾ji V(\08@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 (q126.EMAIL AS EMAIL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS EMAIL)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_EMAIL
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q126> label="q126" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +j + basic_index[EXPLAIN select age, city from customers_materialized_view where age > 25 order by age, city +䝟X ƼB(e08&@COVERING(IDX_MV_MULTI [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q141.AGE AS AGE, q141.CITY AS CITY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE, STRING AS CITY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c10 AS INT)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_MULTI
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q141> label="q141" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +g + basic_indexXEXPLAIN select age, city from customers_index_on_table where age > 25 order by age, city +% (e0c8&@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 (q141.AGE AS AGE, q141.CITY AS CITY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE, STRING AS CITY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c10 AS INT)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_MULTI
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q141> label="q141" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +k +include_clauseYEXPLAIN select name, email, country from customers_materialized_view where name = 'Alice' +Ƭg ,(08@COVERING(IDX_MV_INCLUDE [EQUALS promote(@c12 AS STRING)] -> [COUNTRY: VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME, _.EMAIL AS EMAIL, _.COUNTRY AS COUNTRY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q346.NAME AS NAME, q346.EMAIL AS EMAIL, q346.COUNTRY AS COUNTRY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, STRING AS EMAIL, STRING AS COUNTRY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c12 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_INCLUDE
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q346> label="q346" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +h +include_clauseVEXPLAIN select name, email, country from customers_index_on_table where name = 'Alice' +n њ0(08@COVERING(IDX_IOT_INCLUDE [EQUALS promote(@c12 AS STRING)] -> [COUNTRY: VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME, _.EMAIL AS EMAIL, _.COUNTRY AS COUNTRY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q346.NAME AS NAME, q346.EMAIL AS EMAIL, q346.COUNTRY AS COUNTRY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS NAME, STRING AS EMAIL, STRING AS COUNTRY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c12 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_INCLUDE
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q346> label="q346" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +v +mixed_asc_descdEXPLAIN select age, city from customers_materialized_view where age > 30 order by age asc, city desc + (e058&@COVERING(IDX_MV_ASC_DESC [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q141.AGE AS AGE, q141.CITY AS CITY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE, STRING AS CITY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c10 AS INT)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_ASC_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q141> label="q141" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +s +mixed_asc_descaEXPLAIN select age, city from customers_index_on_table where age > 30 order by age asc, city desc + ՜ (e0*8&@COVERING(IDX_IOT_ASC_DESC [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q141.AGE AS AGE, q141.CITY AS CITY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE, STRING AS CITY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[GREATER_THAN promote(@c10 AS INT)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_ASC_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q141> label="q141" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +w +mixed_asc_desceEXPLAIN select age, city from customers_materialized_view where age < 40 order by age desc, city desc + (e068&@COVERING(IDX_MV_MULTI [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q141.AGE AS AGE, q141.CITY AS CITY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE, STRING AS CITY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[LESS_THAN promote(@c10 AS INT)]]
direction: reversed
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_MULTI
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q141> label="q141" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +t +mixed_asc_descbEXPLAIN select age, city from customers_index_on_table where age < 40 order by age desc, city desc + (e0'8&@COVERING(IDX_IOT_MULTI [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q141.AGE AS AGE, q141.CITY AS CITY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE, STRING AS CITY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[LESS_THAN promote(@c10 AS INT)]]
direction: reversed
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_MULTI
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q141> label="q141" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +v +mixed_asc_descdEXPLAIN select age, city from customers_materialized_view where age < 50 order by age desc, city asc + (e018&@COVERING(IDX_MV_ASC_DESC [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q141.AGE AS AGE, q141.CITY AS CITY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE, STRING AS CITY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[LESS_THAN promote(@c10 AS INT)]]
direction: reversed
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_ASC_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q141> label="q141" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +s +mixed_asc_descaEXPLAIN select age, city from customers_index_on_table where age < 50 order by age desc, city asc +泘 (e0/8&@COVERING(IDX_IOT_ASC_DESC [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q141.AGE AS AGE, q141.CITY AS CITY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE, STRING AS CITY)" ]; + 2 [ label=<
Covering Index Scan
comparisons: [[LESS_THAN promote(@c10 AS INT)]]
direction: reversed
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_ASC_DESC
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q141> label="q141" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +l +nulls_first_lastXEXPLAIN SELECT country FROM customers_materialized_view ORDER BY country ASC NULLS FIRST +i +(\08@^COVERING(IDX_MV_NULLS_FIRST <,> -> [COUNTRY: KEY[0], ID: KEY[2]]) | MAP (_.COUNTRY AS COUNTRY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q126.COUNTRY AS COUNTRY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_NULLS_FIRST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q126> label="q126" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +i +nulls_first_lastUEXPLAIN SELECT country FROM customers_index_on_table ORDER BY country ASC NULLS FIRST +i Ӿ(\08@_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 (q126.COUNTRY AS COUNTRY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_NULLS_FIRST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q126> label="q126" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +k +nulls_first_lastWEXPLAIN SELECT country FROM customers_materialized_view ORDER BY country ASC NULLS LAST +i (\08@COVERING(IDX_MV_NULLS_LAST <,> -> [COUNTRY: from_ordered_bytes(KEY:[0], ASC_NULLS_LAST), ID: KEY[2]]) | MAP (_.COUNTRY AS COUNTRY) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q126.COUNTRY AS COUNTRY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_NULLS_LAST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q126> label="q126" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +h +nulls_first_lastTEXPLAIN SELECT country FROM customers_index_on_table ORDER BY country ASC NULLS LAST +i (\08@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 (q126.COUNTRY AS COUNTRY)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_NULLS_LAST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q126> label="q126" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +e +nulls_first_lastQEXPLAIN SELECT age FROM customers_materialized_view ORDER BY age DESC NULLS FIRST +i Յ +(\0ˬ8@~COVERING(IDX_MV_DESC_NULLS_FIRST <,> -> [AGE: from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST), ID: KEY[2]]) | MAP (_.AGE AS AGE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q126.AGE AS AGE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_DESC_NULLS_FIRST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q126> label="q126" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +b +nulls_first_lastNEXPLAIN SELECT age FROM customers_index_on_table ORDER BY age DESC NULLS FIRST +i (\0 8@COVERING(IDX_IOT_DESC_NULLS_FIRST <,> -> [AGE: from_ordered_bytes(KEY:[0], DESC_NULLS_FIRST), ID: KEY[2]]) | MAP (_.AGE AS AGE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q126.AGE AS AGE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_DESC_NULLS_FIRST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q126> label="q126" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +d +nulls_first_lastPEXPLAIN SELECT age FROM customers_materialized_view ORDER BY age DESC NULLS LAST +ņ (h008.@|COVERING(IDX_MV_DESC_NULLS_LAST <,> -> [AGE: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[2]]) | MAP (_.AGE AS AGE) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q146.AGE AS AGE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_MV_DESC_NULLS_LAST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q146> label="q146" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +a +nulls_first_lastMEXPLAIN SELECT age FROM customers_index_on_table ORDER BY age DESC NULLS LAST + (h0L8.@}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 (q146.AGE AS AGE)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS AGE)" ]; + 2 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 [ label=<
Index
IDX_IOT_DESC_NULLS_LAST
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q146> label="q146" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +#extremum_ever_without_legacy_optionmEXPLAIN SELECT country, min_ever(age) FROM customers_materialized_view WHERE country = 'USA' GROUP BY country +6 Ղ(~0x8K@AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_MV_AGE_MIN_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +#extremum_ever_without_legacy_optionjEXPLAIN SELECT country, min_ever(age) FROM customers_index_on_table WHERE country = 'USA' GROUP BY country +9 &(~08K@AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_AGE_MIN_EXTREMUM
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +#extremum_ever_without_legacy_optionpEXPLAIN SELECT country, max_ever(age) FROM customers_materialized_view WHERE country = 'Canada' GROUP BY country +5 (~0z8K@AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_MV_AGE_MAX_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +#extremum_ever_without_legacy_optionmEXPLAIN SELECT country, max_ever(age) FROM customers_index_on_table WHERE country = 'Canada' GROUP BY country +; &(~0ɧ8K@AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_AGE_MAX_EXTREMUM
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +#extremum_ever_without_legacy_option|EXPLAIN SELECT country, min_ever(age), max_ever(age) FROM customers_materialized_view WHERE country = 'USA' GROUP BY country + +ƐX <(08y@AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) COMPARE BY () WITH q0, q1 RETURN (q0._0 AS _0, q0._1 AS _1, q1._1 AS _2) | MAP (_._0 AS COUNTRY, _._1 AS _1, _._2 AS _2)digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1, q6._2 AS _2)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1, INT AS _2)" ]; + 2 [ label=<
Intersection
COMPARE BY ()
RESULT (q239._0 AS _0, q239._1 AS _1, q241._1 AS _2)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1, INT AS _2)" ]; + 3 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c18 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 4 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c18 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 5 [ label=<
Index
IDX_MV_AGE_MIN_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 6 [ label=<
Index
IDX_MV_AGE_MAX_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ label=< q239> label="q239" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 2 [ label=< q241> label="q241" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 3 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 6 -> 4 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +#extremum_ever_without_legacy_optionyEXPLAIN SELECT country, min_ever(age), max_ever(age) FROM customers_index_on_table WHERE country = 'USA' GROUP BY country + +A -(08y@AISCAN(IDX_IOT_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_IOT_AGE_MAX_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) COMPARE BY () WITH q0, q1 RETURN (q0._0 AS _0, q0._1 AS _1, q1._1 AS _2) | MAP (_._0 AS COUNTRY, _._1 AS _1, _._2 AS _2)digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1, q6._2 AS _2)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1, INT AS _2)" ]; + 2 [ label=<
Intersection
COMPARE BY ()
RESULT (q239._0 AS _0, q239._1 AS _1, q241._1 AS _2)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1, INT AS _2)" ]; + 3 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c18 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 4 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c18 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 5 [ label=<
Index
IDX_IOT_AGE_MAX_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 6 [ label=<
Index
IDX_IOT_AGE_MIN_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ label=< q241> label="q241" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 2 [ label=< q239> label="q239" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 3 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 6 -> 4 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +#extremum_ever_without_legacy_optionpEXPLAIN SELECT country, min_ever(age) FROM customers_materialized_view WHERE country = 'France' GROUP BY country +6 Ղ(~0x8K@AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_MV_AGE_MIN_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +#extremum_ever_without_legacy_optionmEXPLAIN SELECT country, min_ever(age) FROM customers_index_on_table WHERE country = 'France' GROUP BY country +9 &(~08K@AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_AGE_MIN_EXTREMUM
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +#extremum_ever_without_legacy_optionpEXPLAIN SELECT country, max_ever(age) FROM customers_materialized_view WHERE country = 'France' GROUP BY country +5 (~0z8K@AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_MV_AGE_MAX_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +#extremum_ever_without_legacy_optionmEXPLAIN SELECT country, max_ever(age) FROM customers_index_on_table WHERE country = 'France' GROUP BY country +; &(~0ɧ8K@AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_AGE_MAX_EXTREMUM
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + + extremum_ever_with_legacy_optionmEXPLAIN SELECT country, min_ever(age) FROM customers_materialized_view WHERE country = 'USA' GROUP BY country +6 Ղ(~0x8K@AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_MV_AGE_MIN_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + + extremum_ever_with_legacy_optionjEXPLAIN SELECT country, min_ever(age) FROM customers_index_on_table WHERE country = 'USA' GROUP BY country +9 &(~08K@AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_AGE_MIN_EXTREMUM
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + + extremum_ever_with_legacy_optionpEXPLAIN SELECT country, max_ever(age) FROM customers_materialized_view WHERE country = 'Canada' GROUP BY country +5 (~0z8K@AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_MV_AGE_MAX_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + + extremum_ever_with_legacy_optionmEXPLAIN SELECT country, max_ever(age) FROM customers_index_on_table WHERE country = 'Canada' GROUP BY country +; &(~0ɧ8K@AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_AGE_MAX_EXTREMUM
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + + extremum_ever_with_legacy_option|EXPLAIN SELECT country, min_ever(age), max_ever(age) FROM customers_materialized_view WHERE country = 'USA' GROUP BY country + +ƐX <(08y@AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) COMPARE BY () WITH q0, q1 RETURN (q0._0 AS _0, q0._1 AS _1, q1._1 AS _2) | MAP (_._0 AS COUNTRY, _._1 AS _1, _._2 AS _2)digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1, q6._2 AS _2)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1, INT AS _2)" ]; + 2 [ label=<
Intersection
COMPARE BY ()
RESULT (q239._0 AS _0, q239._1 AS _1, q241._1 AS _2)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1, INT AS _2)" ]; + 3 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c18 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 4 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c18 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 5 [ label=<
Index
IDX_MV_AGE_MAX_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 6 [ label=<
Index
IDX_MV_AGE_MIN_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ label=< q241> label="q241" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 2 [ label=< q239> label="q239" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 3 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 6 -> 4 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + + extremum_ever_with_legacy_optionyEXPLAIN SELECT country, min_ever(age), max_ever(age) FROM customers_index_on_table WHERE country = 'USA' GROUP BY country + +A -(08y@AISCAN(IDX_IOT_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_IOT_AGE_MAX_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) COMPARE BY () WITH q0, q1 RETURN (q0._0 AS _0, q0._1 AS _1, q1._1 AS _2) | MAP (_._0 AS COUNTRY, _._1 AS _1, _._2 AS _2)digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1, q6._2 AS _2)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1, INT AS _2)" ]; + 2 [ label=<
Intersection
COMPARE BY ()
RESULT (q239._0 AS _0, q239._1 AS _1, q241._1 AS _2)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1, INT AS _2)" ]; + 3 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c18 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 4 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c18 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 5 [ label=<
Index
IDX_IOT_AGE_MIN_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 6 [ label=<
Index
IDX_IOT_AGE_MAX_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ label=< q239> label="q239" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 2 [ label=< q241> label="q241" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 3 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 6 -> 4 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + + extremum_ever_with_legacy_optionpEXPLAIN SELECT country, min_ever(age) FROM customers_materialized_view WHERE country = 'France' GROUP BY country +6 Ղ(~0x8K@AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_MV_AGE_MIN_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + + extremum_ever_with_legacy_optionmEXPLAIN SELECT country, min_ever(age) FROM customers_index_on_table WHERE country = 'France' GROUP BY country +9 &(~08K@AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_AGE_MIN_EXTREMUM
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + + extremum_ever_with_legacy_optionpEXPLAIN SELECT country, max_ever(age) FROM customers_materialized_view WHERE country = 'France' GROUP BY country +5 (~0z8K@AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_MV_AGE_MAX_NO_LEGACY
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + + extremum_ever_with_legacy_optionmEXPLAIN SELECT country, max_ever(age) FROM customers_index_on_table WHERE country = 'France' GROUP BY country +; &(~0ɧ8K@AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1) digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q6._0 AS COUNTRY, q6._1 AS _1)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS COUNTRY, INT AS _1)" ]; + 2 [ label=<
Index Scan
scan type: BY_GROUP
comparisons: [EQUALS promote(@c13 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS _0, INT AS _1)" ]; + 3 [ label=<
Index
IDX_IOT_AGE_MAX_EXTREMUM
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS EMAIL, INT AS AGE, STRING AS CITY, STRING AS COUNTRY, STRING AS PROFESSION)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} \ No newline at end of file diff --git a/yaml-tests/src/test/resources/index-ddl.metrics.yaml b/yaml-tests/src/test/resources/index-ddl.metrics.yaml new file mode 100644 index 0000000000..4605422a8f --- /dev/null +++ b/yaml-tests/src/test/resources/index-ddl.metrics.yaml @@ -0,0 +1,524 @@ +basic_index: +- query: EXPLAIN select name from customers_materialized_view where name = 'Alice' + explain: 'COVERING(IDX_MV_INCLUDE [EQUALS promote(@c8 AS STRING)] -> [COUNTRY: + VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME)' + task_count: 1873 + task_total_time_ms: 351 + transform_count: 469 + transform_time_ms: 207 + transform_yield_count: 173 + insert_time_ms: 20 + insert_new_count: 219 + insert_reused_count: 21 +- query: EXPLAIN select name from customers_index_on_table where name = 'Alice' + explain: 'COVERING(IDX_IOT_INCLUDE [EQUALS promote(@c8 AS STRING)] -> [COUNTRY: + VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME)' + task_count: 1873 + task_total_time_ms: 234 + transform_count: 469 + transform_time_ms: 128 + transform_yield_count: 173 + insert_time_ms: 16 + insert_new_count: 219 + insert_reused_count: 21 +- query: EXPLAIN select email from customers_materialized_view order by email desc + explain: 'COVERING(IDX_MV_EMAIL <,> -> [EMAIL: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), + ID: KEY[2]]) | MAP (_.EMAIL AS EMAIL)' + task_count: 350 + task_total_time_ms: 180 + transform_count: 105 + transform_time_ms: 144 + transform_yield_count: 92 + insert_time_ms: 4 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN select email from customers_index_on_table order by email desc + explain: 'COVERING(IDX_IOT_EMAIL <,> -> [EMAIL: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), + ID: KEY[2]]) | MAP (_.EMAIL AS EMAIL)' + task_count: 350 + task_total_time_ms: 223 + transform_count: 105 + transform_time_ms: 181 + transform_yield_count: 92 + insert_time_ms: 8 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN select age, city from customers_materialized_view where age > 25 + order by age, city + explain: 'COVERING(IDX_MV_MULTI [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: + KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)' + task_count: 504 + task_total_time_ms: 185 + transform_count: 141 + transform_time_ms: 139 + transform_yield_count: 101 + insert_time_ms: 7 + insert_new_count: 38 + insert_reused_count: 2 +- query: EXPLAIN select age, city from customers_index_on_table where age > 25 order + by age, city + explain: 'COVERING(IDX_IOT_MULTI [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: + KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)' + task_count: 504 + task_total_time_ms: 78 + transform_count: 141 + transform_time_ms: 50 + transform_yield_count: 101 + insert_time_ms: 1 + insert_new_count: 38 + insert_reused_count: 2 +include_clause: +- query: EXPLAIN select name, email, country from customers_materialized_view where + name = 'Alice' + explain: 'COVERING(IDX_MV_INCLUDE [EQUALS promote(@c12 AS STRING)] -> [COUNTRY: + VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME, + _.EMAIL AS EMAIL, _.COUNTRY AS COUNTRY)' + task_count: 1814 + task_total_time_ms: 216 + transform_count: 465 + transform_time_ms: 93 + transform_yield_count: 169 + insert_time_ms: 11 + insert_new_count: 213 + insert_reused_count: 21 +- query: EXPLAIN select name, email, country from customers_index_on_table where + name = 'Alice' + explain: 'COVERING(IDX_IOT_INCLUDE [EQUALS promote(@c12 AS STRING)] -> [COUNTRY: + VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME, + _.EMAIL AS EMAIL, _.COUNTRY AS COUNTRY)' + task_count: 1814 + task_total_time_ms: 230 + transform_count: 465 + transform_time_ms: 101 + transform_yield_count: 169 + insert_time_ms: 13 + insert_new_count: 213 + insert_reused_count: 21 +mixed_asc_desc: +- query: EXPLAIN select age, city from customers_materialized_view where age > 30 + order by age asc, city desc + explain: 'COVERING(IDX_MV_ASC_DESC [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: + KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | + MAP (_.AGE AS AGE, _.CITY AS CITY)' + task_count: 504 + task_total_time_ms: 62 + transform_count: 141 + transform_time_ms: 36 + transform_yield_count: 101 + insert_time_ms: 0 + insert_new_count: 38 + insert_reused_count: 2 +- query: EXPLAIN select age, city from customers_index_on_table where age > 30 order + by age asc, city desc + explain: 'COVERING(IDX_IOT_ASC_DESC [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: + KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | + MAP (_.AGE AS AGE, _.CITY AS CITY)' + task_count: 504 + task_total_time_ms: 40 + transform_count: 141 + transform_time_ms: 25 + transform_yield_count: 101 + insert_time_ms: 0 + insert_new_count: 38 + insert_reused_count: 2 +- query: EXPLAIN select age, city from customers_materialized_view where age < 40 + order by age desc, city desc + explain: 'COVERING(IDX_MV_MULTI [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> + [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)' + task_count: 504 + task_total_time_ms: 59 + transform_count: 141 + transform_time_ms: 35 + transform_yield_count: 101 + insert_time_ms: 0 + insert_new_count: 38 + insert_reused_count: 2 +- query: EXPLAIN select age, city from customers_index_on_table where age < 40 order + by age desc, city desc + explain: 'COVERING(IDX_IOT_MULTI [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> + [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)' + task_count: 504 + task_total_time_ms: 43 + transform_count: 141 + transform_time_ms: 24 + transform_yield_count: 101 + insert_time_ms: 0 + insert_new_count: 38 + insert_reused_count: 2 +- query: EXPLAIN select age, city from customers_materialized_view where age < 50 + order by age desc, city asc + explain: 'COVERING(IDX_MV_ASC_DESC [[LESS_THAN promote(@c10 AS INT)]] REVERSE + -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) + | MAP (_.AGE AS AGE, _.CITY AS CITY)' + task_count: 504 + task_total_time_ms: 63 + transform_count: 141 + transform_time_ms: 36 + transform_yield_count: 101 + insert_time_ms: 0 + insert_new_count: 38 + insert_reused_count: 2 +- query: EXPLAIN select age, city from customers_index_on_table where age < 50 order + by age desc, city asc + explain: 'COVERING(IDX_IOT_ASC_DESC [[LESS_THAN promote(@c10 AS INT)]] REVERSE + -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) + | MAP (_.AGE AS AGE, _.CITY AS CITY)' + task_count: 504 + task_total_time_ms: 61 + transform_count: 141 + transform_time_ms: 36 + transform_yield_count: 101 + insert_time_ms: 0 + insert_new_count: 38 + insert_reused_count: 2 +nulls_first_last: +- query: EXPLAIN SELECT country FROM customers_materialized_view ORDER BY country + ASC NULLS FIRST + explain: 'COVERING(IDX_MV_NULLS_FIRST <,> -> [COUNTRY: KEY[0], ID: KEY[2]]) | + MAP (_.COUNTRY AS COUNTRY)' + task_count: 350 + task_total_time_ms: 33 + transform_count: 105 + transform_time_ms: 21 + transform_yield_count: 92 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN SELECT country FROM customers_index_on_table ORDER BY country ASC + NULLS FIRST + explain: 'COVERING(IDX_IOT_NULLS_FIRST <,> -> [COUNTRY: KEY[0], ID: KEY[2]]) | + MAP (_.COUNTRY AS COUNTRY)' + task_count: 350 + task_total_time_ms: 16 + transform_count: 105 + transform_time_ms: 10 + transform_yield_count: 92 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN SELECT country FROM customers_materialized_view ORDER BY country + ASC NULLS LAST + explain: 'COVERING(IDX_MV_NULLS_LAST <,> -> [COUNTRY: from_ordered_bytes(KEY:[0], + ASC_NULLS_LAST), ID: KEY[2]]) | MAP (_.COUNTRY AS COUNTRY)' + task_count: 350 + task_total_time_ms: 37 + transform_count: 105 + transform_time_ms: 23 + transform_yield_count: 92 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN SELECT country FROM customers_index_on_table ORDER BY country ASC + NULLS LAST + explain: 'COVERING(IDX_IOT_NULLS_LAST <,> -> [COUNTRY: from_ordered_bytes(KEY:[0], + ASC_NULLS_LAST), ID: KEY[2]]) | MAP (_.COUNTRY AS COUNTRY)' + task_count: 350 + task_total_time_ms: 10 + transform_count: 105 + transform_time_ms: 7 + transform_yield_count: 92 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN SELECT age FROM customers_materialized_view ORDER BY age DESC NULLS + FIRST + explain: 'COVERING(IDX_MV_DESC_NULLS_FIRST <,> -> [AGE: from_ordered_bytes(KEY:[0], + DESC_NULLS_FIRST), ID: KEY[2]]) | MAP (_.AGE AS AGE)' + task_count: 350 + task_total_time_ms: 33 + transform_count: 105 + transform_time_ms: 21 + transform_yield_count: 92 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN SELECT age FROM customers_index_on_table ORDER BY age DESC NULLS + FIRST + explain: 'COVERING(IDX_IOT_DESC_NULLS_FIRST <,> -> [AGE: from_ordered_bytes(KEY:[0], + DESC_NULLS_FIRST), ID: KEY[2]]) | MAP (_.AGE AS AGE)' + task_count: 350 + task_total_time_ms: 37 + transform_count: 105 + transform_time_ms: 27 + transform_yield_count: 92 + insert_time_ms: 0 + insert_new_count: 20 + insert_reused_count: 2 +- query: EXPLAIN SELECT age FROM customers_materialized_view ORDER BY age DESC NULLS + LAST + explain: 'COVERING(IDX_MV_DESC_NULLS_LAST <,> -> [AGE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST), ID: KEY[2]]) | MAP (_.AGE AS AGE)' + task_count: 516 + task_total_time_ms: 35 + transform_count: 133 + transform_time_ms: 20 + transform_yield_count: 104 + insert_time_ms: 0 + insert_new_count: 46 + insert_reused_count: 6 +- query: EXPLAIN SELECT age FROM customers_index_on_table ORDER BY age DESC NULLS + LAST + explain: 'COVERING(IDX_IOT_DESC_NULLS_LAST <,> -> [AGE: from_ordered_bytes(KEY:[0], + DESC_NULLS_LAST), ID: KEY[2]]) | MAP (_.AGE AS AGE)' + task_count: 516 + task_total_time_ms: 54 + transform_count: 133 + transform_time_ms: 33 + transform_yield_count: 104 + insert_time_ms: 1 + insert_new_count: 46 + insert_reused_count: 6 +extremum_ever_without_legacy_option: +- query: EXPLAIN SELECT country, min_ever(age) FROM customers_materialized_view + WHERE country = 'USA' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 113 + transform_count: 257 + transform_time_ms: 66 + transform_yield_count: 126 + insert_time_ms: 1 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, min_ever(age) FROM customers_index_on_table WHERE + country = 'USA' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 120 + transform_count: 257 + transform_time_ms: 80 + transform_yield_count: 126 + insert_time_ms: 2 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, max_ever(age) FROM customers_materialized_view + WHERE country = 'Canada' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 113 + transform_count: 257 + transform_time_ms: 66 + transform_yield_count: 126 + insert_time_ms: 1 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, max_ever(age) FROM customers_index_on_table WHERE + country = 'Canada' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 124 + transform_count: 257 + transform_time_ms: 81 + transform_yield_count: 126 + insert_time_ms: 2 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, min_ever(age), max_ever(age) FROM customers_materialized_view + WHERE country = 'USA' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS + promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) COMPARE + BY () WITH q0, q1 RETURN (q0._0 AS _0, q0._1 AS _1, q1._1 AS _2) | MAP (_._0 + AS COUNTRY, _._1 AS _1, _._2 AS _2)' + task_count: 1289 + task_total_time_ms: 184 + transform_count: 347 + transform_time_ms: 126 + transform_yield_count: 146 + insert_time_ms: 5 + insert_new_count: 121 + insert_reused_count: 6 +- query: EXPLAIN SELECT country, min_ever(age), max_ever(age) FROM customers_index_on_table + WHERE country = 'USA' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_IOT_AGE_MAX_NO_LEGACY [EQUALS + promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) COMPARE + BY () WITH q0, q1 RETURN (q0._0 AS _0, q0._1 AS _1, q1._1 AS _2) | MAP (_._0 + AS COUNTRY, _._1 AS _1, _._2 AS _2)' + task_count: 1289 + task_total_time_ms: 136 + transform_count: 347 + transform_time_ms: 95 + transform_yield_count: 146 + insert_time_ms: 4 + insert_new_count: 121 + insert_reused_count: 6 +- query: EXPLAIN SELECT country, min_ever(age) FROM customers_materialized_view + WHERE country = 'France' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 113 + transform_count: 257 + transform_time_ms: 66 + transform_yield_count: 126 + insert_time_ms: 1 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, min_ever(age) FROM customers_index_on_table WHERE + country = 'France' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 120 + transform_count: 257 + transform_time_ms: 80 + transform_yield_count: 126 + insert_time_ms: 2 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, max_ever(age) FROM customers_materialized_view + WHERE country = 'France' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 113 + transform_count: 257 + transform_time_ms: 66 + transform_yield_count: 126 + insert_time_ms: 1 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, max_ever(age) FROM customers_index_on_table WHERE + country = 'France' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 124 + transform_count: 257 + transform_time_ms: 81 + transform_yield_count: 126 + insert_time_ms: 2 + insert_new_count: 75 + insert_reused_count: 4 +extremum_ever_with_legacy_option: +- query: EXPLAIN SELECT country, min_ever(age) FROM customers_materialized_view + WHERE country = 'USA' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 113 + transform_count: 257 + transform_time_ms: 66 + transform_yield_count: 126 + insert_time_ms: 1 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, min_ever(age) FROM customers_index_on_table WHERE + country = 'USA' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 120 + transform_count: 257 + transform_time_ms: 80 + transform_yield_count: 126 + insert_time_ms: 2 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, max_ever(age) FROM customers_materialized_view + WHERE country = 'Canada' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 113 + transform_count: 257 + transform_time_ms: 66 + transform_yield_count: 126 + insert_time_ms: 1 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, max_ever(age) FROM customers_index_on_table WHERE + country = 'Canada' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 124 + transform_count: 257 + transform_time_ms: 81 + transform_yield_count: 126 + insert_time_ms: 2 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, min_ever(age), max_ever(age) FROM customers_materialized_view + WHERE country = 'USA' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS + promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) COMPARE + BY () WITH q0, q1 RETURN (q0._0 AS _0, q0._1 AS _1, q1._1 AS _2) | MAP (_._0 + AS COUNTRY, _._1 AS _1, _._2 AS _2)' + task_count: 1289 + task_total_time_ms: 184 + transform_count: 347 + transform_time_ms: 126 + transform_yield_count: 146 + insert_time_ms: 5 + insert_new_count: 121 + insert_reused_count: 6 +- query: EXPLAIN SELECT country, min_ever(age), max_ever(age) FROM customers_index_on_table + WHERE country = 'USA' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_IOT_AGE_MAX_NO_LEGACY [EQUALS + promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) COMPARE + BY () WITH q0, q1 RETURN (q0._0 AS _0, q0._1 AS _1, q1._1 AS _2) | MAP (_._0 + AS COUNTRY, _._1 AS _1, _._2 AS _2)' + task_count: 1289 + task_total_time_ms: 136 + transform_count: 347 + transform_time_ms: 95 + transform_yield_count: 146 + insert_time_ms: 4 + insert_new_count: 121 + insert_reused_count: 6 +- query: EXPLAIN SELECT country, min_ever(age) FROM customers_materialized_view + WHERE country = 'France' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 113 + transform_count: 257 + transform_time_ms: 66 + transform_yield_count: 126 + insert_time_ms: 1 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, min_ever(age) FROM customers_index_on_table WHERE + country = 'France' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 120 + transform_count: 257 + transform_time_ms: 80 + transform_yield_count: 126 + insert_time_ms: 2 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, max_ever(age) FROM customers_materialized_view + WHERE country = 'France' GROUP BY country + explain: 'AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 113 + transform_count: 257 + transform_time_ms: 66 + transform_yield_count: 126 + insert_time_ms: 1 + insert_new_count: 75 + insert_reused_count: 4 +- query: EXPLAIN SELECT country, max_ever(age) FROM customers_index_on_table WHERE + country = 'France' GROUP BY country + explain: 'AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP + -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)' + task_count: 883 + task_total_time_ms: 124 + transform_count: 257 + transform_time_ms: 81 + transform_yield_count: 126 + insert_time_ms: 2 + insert_new_count: 75 + insert_reused_count: 4 diff --git a/yaml-tests/src/test/resources/index-ddl.yamsql b/yaml-tests/src/test/resources/index-ddl.yamsql new file mode 100644 index 0000000000..a81eb120db --- /dev/null +++ b/yaml-tests/src/test/resources/index-ddl.yamsql @@ -0,0 +1,339 @@ +# +# index-ddl.yamsql +# +# This source file is part of the FoundationDB open source project +# +# Copyright 2021-2025 Apple Inc. and the FoundationDB project authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Test alternate CREATE INDEX ON syntax vs traditional CREATE INDEX AS SELECT +# Both syntaxes should produce equivalent query plans + +--- +options: + supported_version: !current_version +--- +schema_template: + # Table with indexes defined via materialized view + create table customers_materialized_view( + id integer, + name string, + email string, + age integer, + city string, + country string, + profession string, + primary key(id) + ) + create index idx_mv_name as select name from customers_materialized_view order by name + create index idx_mv_email as select email from customers_materialized_view order by email desc + create index idx_mv_multi as select age, city from customers_materialized_view order by age asc, city asc + create index idx_mv_asc_desc as select age, city from customers_materialized_view order by age asc, city desc + create index idx_mv_include as select name, email, country from customers_materialized_view order by name + create index idx_mv_nulls_first as select country from customers_materialized_view order by country asc nulls first + create index idx_mv_nulls_last as select country from customers_materialized_view order by country asc nulls last + create index idx_mv_desc_nulls_first as select age from customers_materialized_view order by age desc nulls first + create index idx_mv_desc_nulls_last as select age from customers_materialized_view order by age desc nulls last + create table customers_index_on_table( + id integer, + name string, + email string, + age integer, + city string, + country string, + profession string, + primary key(id) + ) + + create index idx_iot_name on customers_index_on_table(name) + create index idx_iot_email on customers_index_on_table(email desc) + create index idx_iot_multi on customers_index_on_table(age, city) + create index idx_iot_asc_desc on customers_index_on_table(age, city desc) + create index idx_iot_include on customers_index_on_table(name) include(email, country) + + create index idx_iot_nulls_first on customers_index_on_table(country asc nulls first) + create index idx_iot_nulls_last on customers_index_on_table(country asc nulls last) + create index idx_iot_desc_nulls_first on customers_index_on_table(age desc nulls first) + create index idx_iot_desc_nulls_last on customers_index_on_table(age desc nulls last) + + create unique index idx_mv_unique_profession as select profession from customers_materialized_view order by profession + create unique index idx_iot_unique_profession on customers_index_on_table(profession) + + create index idx_mv_age_min_no_legacy as select min_ever(age) from customers_materialized_view group by country + create index idx_mv_age_max_no_legacy as select max_ever(age) from customers_materialized_view group by country + + create view view_iot_age_min_no_legacy as select country, min_ever(age) as min_age from customers_index_on_table group by country + create view view_iot_age_max_no_legacy as select country, max_ever(age) as max_age from customers_index_on_table group by country + + create index idx_iot_age_min_no_legacy on view_iot_age_min_no_legacy(min_age, country) + create index idx_iot_age_max_no_legacy on view_iot_age_max_no_legacy(max_age, country) + + create index idx_mv_age_min_extremum as select min_ever(age) from customers_materialized_view group by country with attributes legacy_extremum_ever + create index idx_mv_age_max_extremum as select max_ever(age) from customers_materialized_view group by country with attributes legacy_extremum_ever + + create view view_iot_age_min as select country, min_ever(age) as min_age from customers_index_on_table group by country + create view view_iot_age_max as select country, max_ever(age) as max_age from customers_index_on_table group by country + + create index idx_iot_age_min_extremum on view_iot_age_min(min_age, country) options (legacy_extremum_ever) + create index idx_iot_age_max_extremum on view_iot_age_max(max_age, country) options (legacy_extremum_ever) +--- +setup: + steps: + # Insert test data including NULL values to test NULLS ordering + # Each table gets different profession values to test UNIQUE constraints independently + - query: INSERT INTO customers_materialized_view VALUES + (1, 'Alice', 'alice@example.com', 25, 'New York', 'USA', 'Engineer'), + (2, 'Bob', 'bob@example.com', NULL, 'London', NULL, 'Designer'), + (3, 'Charlie', NULL, 35, 'Paris', 'France', 'Manager'), + (4, NULL, 'null@example.com', 30, NULL, 'Canada', 'Analyst') + - query: INSERT INTO customers_index_on_table VALUES + (1, 'Alice', 'alice@example.com', 25, 'New York', 'USA', 'Engineer'), + (2, 'Bob', 'bob@example.com', NULL, 'London', NULL, 'Designer'), + (3, 'Charlie', NULL, 35, 'Paris', 'France', 'Manager'), + (4, NULL, 'null@example.com', 30, NULL, 'Canada', 'Analyst') +--- +test_block: + name: basic_index + tests: + # Test basic name index - plans should use respective indexes + - + - query: select name from customers_materialized_view where name = 'Alice' + - explain: "COVERING(IDX_MV_INCLUDE [EQUALS promote(@c8 AS STRING)] -> [COUNTRY: VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME)" + - result: [{ Alice }] + - + - query: select name from customers_index_on_table where name = 'Alice' + - explain: "COVERING(IDX_IOT_INCLUDE [EQUALS promote(@c8 AS STRING)] -> [COUNTRY: VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME)" + - result: [{ Alice }] + # Test descending email index + - + - query: select email from customers_materialized_view order by email desc + - explain: "COVERING(IDX_MV_EMAIL <,> -> [EMAIL: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[2]]) | MAP (_.EMAIL AS EMAIL)" + - result: [{ null@example.com }, { bob@example.com }, { alice@example.com }, { !null }] + - + - query: select email from customers_index_on_table order by email desc + - explain: "COVERING(IDX_IOT_EMAIL <,> -> [EMAIL: from_ordered_bytes(KEY:[0], DESC_NULLS_LAST), ID: KEY[2]]) | MAP (_.EMAIL AS EMAIL)" + - result: [{ null@example.com }, { bob@example.com }, { alice@example.com }, { !null }] + # Test multi-column index - should use correct indexes now + - + - query: select age, city from customers_materialized_view where age > 25 order by age, city + - explain: "COVERING(IDX_MV_MULTI [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)" + - result: [{ 30, !null }, { 35, Paris }] + - + - query: select age, city from customers_index_on_table where age > 25 order by age, city + - explain: "COVERING(IDX_IOT_MULTI [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)" + - result: [{ 30, !null }, { 35, Paris }] +--- +test_block: + name: include_clause + tests: + - + - query: select name, email, country from customers_materialized_view where name = 'Alice' + - explain: "COVERING(IDX_MV_INCLUDE [EQUALS promote(@c12 AS STRING)] -> [COUNTRY: VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME, _.EMAIL AS EMAIL, _.COUNTRY AS COUNTRY)" + - result: [{ Alice, alice@example.com, USA }] + - + - query: select name, email, country from customers_index_on_table where name = 'Alice' + - explain: "COVERING(IDX_IOT_INCLUDE [EQUALS promote(@c12 AS STRING)] -> [COUNTRY: VALUE[1], EMAIL: VALUE[0], ID: KEY[2], NAME: KEY[0]]) | MAP (_.NAME AS NAME, _.EMAIL AS EMAIL, _.COUNTRY AS COUNTRY)" + - result: [{ Alice, alice@example.com, USA }] +--- +test_block: + name: mixed_asc_desc + tests: + # Test mixed ASC/DESC ordering (age ASC, city DESC) + - + - query: select age, city from customers_materialized_view where age > 30 order by age asc, city desc + - explain: "COVERING(IDX_MV_ASC_DESC [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)" + - result: [{ 35, Paris }] + - + - query: select age, city from customers_index_on_table where age > 30 order by age asc, city desc + - explain: "COVERING(IDX_IOT_ASC_DESC [[GREATER_THAN promote(@c10 AS INT)]] -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)" + - result: [{ 35, Paris }] + # Test DESC DESC ordering (should use REVERSE on ASC ASC index) + - + - query: select age, city from customers_materialized_view where age < 40 order by age desc, city desc + - explain: "COVERING(IDX_MV_MULTI [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)" + - result: [{ 35, Paris }, { 30, !null }, { 25, New York }] + - + - query: select age, city from customers_index_on_table where age < 40 order by age desc, city desc + - explain: "COVERING(IDX_IOT_MULTI [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> [AGE: KEY[0], CITY: KEY[1], ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)" + - result: [{ 35, Paris }, { 30, !null }, { 25, New York }] + # Test DESC ASC ordering (should use REVERSE on ASC DESC index) + - + - query: select age, city from customers_materialized_view where age < 50 order by age desc, city asc + - explain: "COVERING(IDX_MV_ASC_DESC [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)" + - result: [{ 35, Paris }, { 30, !null }, { 25, New York }] + - + - query: select age, city from customers_index_on_table where age < 50 order by age desc, city asc + - explain: "COVERING(IDX_IOT_ASC_DESC [[LESS_THAN promote(@c10 AS INT)]] REVERSE -> [AGE: KEY[0], CITY: from_ordered_bytes(KEY:[1], DESC_NULLS_LAST), ID: KEY[3]]) | MAP (_.AGE AS AGE, _.CITY AS CITY)" + - result: [{ 35, Paris }, { 30, !null }, { 25, New York }] +--- +test_block: + name: nulls_first_last + tests: + # Test NULLS FIRST/LAST ordering with actual results and index usage + - + - query: SELECT country FROM customers_materialized_view ORDER BY country ASC NULLS FIRST + - explain: "COVERING(IDX_MV_NULLS_FIRST <,> -> [COUNTRY: KEY[0], ID: KEY[2]]) | MAP (_.COUNTRY AS COUNTRY)" + - result: [{ !null }, { Canada }, { France }, { USA }] + - + - query: SELECT country FROM customers_index_on_table ORDER BY country ASC NULLS FIRST + - explain: "COVERING(IDX_IOT_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, 'David', 'david@example.com', 28, 'Boston', 'USA', 'Engineer') + - error: "23505" + # Test that different professions still work + - + - query: INSERT INTO customers_materialized_view VALUES (5, 'David', 'david@example.com', 28, 'Boston', 'USA', 'Architect') + - count: 1 + - + - query: INSERT INTO customers_index_on_table VALUES (5, 'David', 'david@example.com', 28, 'Seattle', 'USA', 'Architect') + - count: 1 +--- +test_block: + name: extremum_ever_without_legacy_option + tests: + # Test MIN_EVER and MAX_EVER WITHOUT legacy_extremum_ever option + # These tests verify that aggregate indexes work correctly without the legacy option + # Using legacy index style (AS SELECT) without attributes + # Test MIN_EVER with specific country filter + - + - query: SELECT country, min_ever(age) FROM customers_materialized_view WHERE country = 'USA' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{USA, 25}] + - + - query: SELECT country, min_ever(age) FROM customers_index_on_table WHERE country = 'USA' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{USA, 25}] + # Test MAX_EVER with specific country filter + - + - query: SELECT country, max_ever(age) FROM customers_materialized_view WHERE country = 'Canada' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{Canada, 30}] + - + - query: SELECT country, max_ever(age) FROM customers_index_on_table WHERE country = 'Canada' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{Canada, 30}] + # Test combined MIN_EVER and MAX_EVER in same query + - + - query: SELECT country, min_ever(age), max_ever(age) FROM customers_materialized_view WHERE country = 'USA' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) COMPARE BY () WITH q0, q1 RETURN (q0._0 AS _0, q0._1 AS _1, q1._1 AS _2) | MAP (_._0 AS COUNTRY, _._1 AS _1, _._2 AS _2)" + - result: [{USA, 25, 28}] + - + - query: SELECT country, min_ever(age), max_ever(age) FROM customers_index_on_table WHERE country = 'USA' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_IOT_AGE_MAX_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) COMPARE BY () WITH q0, q1 RETURN (q0._0 AS _0, q0._1 AS _1, q1._1 AS _2) | MAP (_._0 AS COUNTRY, _._1 AS _1, _._2 AS _2)" + - result: [{USA, 25, 28}] + # Test with France to verify different data point + - + - query: SELECT country, min_ever(age) FROM customers_materialized_view WHERE country = 'France' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{France, 35}] + - + - query: SELECT country, min_ever(age) FROM customers_index_on_table WHERE country = 'France' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{France, 35}] + # Test MAX_EVER with France + - + - query: SELECT country, max_ever(age) FROM customers_materialized_view WHERE country = 'France' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{France, 35}] + - + - query: SELECT country, max_ever(age) FROM customers_index_on_table WHERE country = 'France' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{France, 35}] +--- +test_block: + name: extremum_ever_with_legacy_option + tests: + # Test MIN_EVER and MAX_EVER WITH legacy_extremum_ever option + # These tests verify that aggregate indexes created with LEGACY_EXTREMUM_EVER work correctly + # Using legacy index style (AS SELECT with ATTRIBUTES) + # Test MIN_EVER with specific country filter + - + - query: SELECT country, min_ever(age) FROM customers_materialized_view WHERE country = 'USA' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{USA, 25}] + - + - query: SELECT country, min_ever(age) FROM customers_index_on_table WHERE country = 'USA' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{USA, 25}] + # Test MAX_EVER with specific country filter + - + - query: SELECT country, max_ever(age) FROM customers_materialized_view WHERE country = 'Canada' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{Canada, 30}] + - + - query: SELECT country, max_ever(age) FROM customers_index_on_table WHERE country = 'Canada' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{Canada, 30}] + # Test combined MIN_EVER and MAX_EVER in same query + - + - query: SELECT country, min_ever(age), max_ever(age) FROM customers_materialized_view WHERE country = 'USA' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) COMPARE BY () WITH q0, q1 RETURN (q0._0 AS _0, q0._1 AS _1, q1._1 AS _2) | MAP (_._0 AS COUNTRY, _._1 AS _1, _._2 AS _2)" + - result: [{USA, 25, 28}] + - + - query: SELECT country, min_ever(age), max_ever(age) FROM customers_index_on_table WHERE country = 'USA' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MIN_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) ∩ AISCAN(IDX_IOT_AGE_MAX_NO_LEGACY [EQUALS promote(@c18 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) COMPARE BY () WITH q0, q1 RETURN (q0._0 AS _0, q0._1 AS _1, q1._1 AS _2) | MAP (_._0 AS COUNTRY, _._1 AS _1, _._2 AS _2)" + - result: [{USA, 25, 28}] + # Test with France to verify different data point + - + - query: SELECT country, min_ever(age) FROM customers_materialized_view WHERE country = 'France' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MIN_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{France, 35}] + - + - query: SELECT country, min_ever(age) FROM customers_index_on_table WHERE country = 'France' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MIN_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{France, 35}] + # Test MAX_EVER with France + - + - query: SELECT country, max_ever(age) FROM customers_materialized_view WHERE country = 'France' GROUP BY country + - explain: "AISCAN(IDX_MV_AGE_MAX_NO_LEGACY [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{France, 35}] + - + - query: SELECT country, max_ever(age) FROM customers_index_on_table WHERE country = 'France' GROUP BY country + - explain: "AISCAN(IDX_IOT_AGE_MAX_EXTREMUM [EQUALS promote(@c13 AS STRING)] BY_GROUP -> [_0: KEY:[0], _1: VALUE:[0]]) | MAP (_._0 AS COUNTRY, _._1 AS _1)" + - result: [{France, 35}] +...