From 76f304e8dc6d9be271fed3ed228ed523f0ac67ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Fri, 2 Oct 2026 11:00:59 +0200 Subject: [PATCH] Generate the index search of every operator from the catalog tools/codegen_duck_udfs.py writes src/include/generated/index_search_ops.hpp from the catalog's per-signature indexSearch {columnLeft, columnRight}: one row per name a signature is reached under, its SQL name and its operator's symbol, the symbol read from the doc comment of its columnLeft value in the catalog's IndexSearchOp enum, carrying the search of `column query` and, when the operator's MobilityDB declaration has a COMMUTATOR, the search of `query column`. A name routed to two different searches fails generation. The hand-written index_search_ops.hpp keeps only what the catalog does not state, the axis each search compares along and the box types carrying it, and reads every name from the generated table. The R-tree and SP-tree scan optimizers ask that table before taking the index, so a predicate whose operand order has no search stays a filter answered by a scan. Why: the hand-written table answered the overlapping orderings with the query on the left by the opposite ordering, `query &< column` through INDEX_OVERRIGHT, which states a different predicate: INDEX_OVERRIGHT accepts every stored box whose start is not before the query's start, while `query &< column` holds of a box whose end is not before the query's end. No search states the latter, which is why MobilityDB declares `&<` without a COMMUTATOR and its operator classes leave it to a scan. The catalog states each operator's searches from that declaration, so reading them from it makes the routing the MobilityDB one. Witness: over 2999 points (i, i), with the query box X [500, 501], the index answered `query &< t` with 2500 rows where a scan answers 2499, `query &> t` with 501 for 500 and stboxOverbefore(query, t) with 2634 for 2633; now the plan of each holds no MOBILITY RTREE INDEX and the three answer as the scan, while `query << t` still reaches the index through its commuted INDEX_RIGHT and agrees with the scan. Measured: the generated table holds 90 names from the catalog of the pinned commit, every overlapping ordering without a commuted search and every strict one commuting to its opposite; the generated UDF sources do not change. test/sql/parity/095_index_ordering_query_on_left.test fails on the hand-written table at its first `&<` plan check and passes its 23 assertions now; the full suite passes 3089 assertions in 109 test cases, with no compiler warning in the changed files. --- src/include/generated/index_search_ops.hpp | 117 +++++++++++ src/include/index/index_search_ops.hpp | 181 +++++------------- src/index/rtree_optimize_scan.cpp | 9 + src/index/sptree_optimize_scan.cpp | 9 + .../095_index_ordering_query_on_left.test | 72 +++++++ tools/codegen_duck_udfs.py | 70 +++++++ 6 files changed, 330 insertions(+), 128 deletions(-) create mode 100644 src/include/generated/index_search_ops.hpp create mode 100644 test/sql/parity/095_index_ordering_query_on_left.test diff --git a/src/include/generated/index_search_ops.hpp b/src/include/generated/index_search_ops.hpp new file mode 100644 index 00000000..9970a5e6 --- /dev/null +++ b/src/include/generated/index_search_ops.hpp @@ -0,0 +1,117 @@ +// GENERATED by tools/codegen_duck_udfs.py from the MEOS-API catalog — DO NOT EDIT. +// The index search of every operator name, `column query` and `query column`, +// from the per-signature indexSearch of the catalog. +#pragma once + +#include "meos_wrapper_simple.hpp" + +namespace duckdb { + +//! A name an in-memory index answers and the MEOS search it asks for. A predicate is read as +//! `column query`; with the query on the left it asks for `commuted`, and an operator +//! whose MobilityDB declaration has no COMMUTATOR (the overlapping orderings) has none, so a +//! predicate written that way is answered by a scan. +struct IndexOperatorEntry { + const char *name; + //! The search of `column query` + IndexSearchOp direct; + //! True when `query column` has a search + bool has_commuted; + //! The search of `query column`, meaningful only when has_commuted + IndexSearchOp commuted; +}; + +static constexpr IndexOperatorEntry INDEX_OPERATORS[] = { + {"#&>", INDEX_OVERAFTER, false, INDEX_OVERAFTER}, + {"#>>", INDEX_AFTER, true, INDEX_BEFORE}, + {"&&", INDEX_OVERLAPS, true, INDEX_OVERLAPS}, + {"&<", INDEX_OVERLEFT, false, INDEX_OVERLEFT}, + {"&<#", INDEX_OVERBEFORE, false, INDEX_OVERBEFORE}, + {"&", INDEX_OVERRIGHT, false, INDEX_OVERRIGHT}, + {"-|-", INDEX_ADJACENT, true, INDEX_ADJACENT}, + {"/&>", INDEX_OVERBACK, false, INDEX_OVERBACK}, + {"/>>", INDEX_BACK, true, INDEX_FRONT}, + {"<<", INDEX_LEFT, true, INDEX_RIGHT}, + {"<<#", INDEX_BEFORE, true, INDEX_AFTER}, + {"<>", INDEX_RIGHT, true, INDEX_LEFT}, + {"@>", INDEX_CONTAINS, true, INDEX_CONTAINED_BY}, + {"adjacent", INDEX_ADJACENT, true, INDEX_ADJACENT}, + {"contained", INDEX_CONTAINED_BY, true, INDEX_CONTAINS}, + {"contains", INDEX_CONTAINS, true, INDEX_CONTAINED_BY}, + {"overlaps", INDEX_OVERLAPS, true, INDEX_OVERLAPS}, + {"same", INDEX_SAME, true, INDEX_SAME}, + {"setAfter", INDEX_AFTER, true, INDEX_BEFORE}, + {"setBefore", INDEX_BEFORE, true, INDEX_AFTER}, + {"setLeft", INDEX_LEFT, true, INDEX_RIGHT}, + {"setOverafter", INDEX_OVERAFTER, false, INDEX_OVERAFTER}, + {"setOverbefore", INDEX_OVERBEFORE, false, INDEX_OVERBEFORE}, + {"setOverleft", INDEX_OVERLEFT, false, INDEX_OVERLEFT}, + {"setOverright", INDEX_OVERRIGHT, false, INDEX_OVERRIGHT}, + {"setRight", INDEX_RIGHT, true, INDEX_LEFT}, + {"spanAfter", INDEX_AFTER, true, INDEX_BEFORE}, + {"spanBefore", INDEX_BEFORE, true, INDEX_AFTER}, + {"spanLeft", INDEX_LEFT, true, INDEX_RIGHT}, + {"spanOverafter", INDEX_OVERAFTER, false, INDEX_OVERAFTER}, + {"spanOverbefore", INDEX_OVERBEFORE, false, INDEX_OVERBEFORE}, + {"spanOverleft", INDEX_OVERLEFT, false, INDEX_OVERLEFT}, + {"spanOverright", INDEX_OVERRIGHT, false, INDEX_OVERRIGHT}, + {"spanRight", INDEX_RIGHT, true, INDEX_LEFT}, + {"spansetAfter", INDEX_AFTER, true, INDEX_BEFORE}, + {"spansetBefore", INDEX_BEFORE, true, INDEX_AFTER}, + {"spansetLeft", INDEX_LEFT, true, INDEX_RIGHT}, + {"spansetOverafter", INDEX_OVERAFTER, false, INDEX_OVERAFTER}, + {"spansetOverbefore", INDEX_OVERBEFORE, false, INDEX_OVERBEFORE}, + {"spansetOverleft", INDEX_OVERLEFT, false, INDEX_OVERLEFT}, + {"spansetOverright", INDEX_OVERRIGHT, false, INDEX_OVERRIGHT}, + {"spansetRight", INDEX_RIGHT, true, INDEX_LEFT}, + {"stboxAbove", INDEX_ABOVE, true, INDEX_BELOW}, + {"stboxAfter", INDEX_AFTER, true, INDEX_BEFORE}, + {"stboxBack", INDEX_BACK, true, INDEX_FRONT}, + {"stboxBefore", INDEX_BEFORE, true, INDEX_AFTER}, + {"stboxBelow", INDEX_BELOW, true, INDEX_ABOVE}, + {"stboxFront", INDEX_FRONT, true, INDEX_BACK}, + {"stboxLeft", INDEX_LEFT, true, INDEX_RIGHT}, + {"stboxOverabove", INDEX_OVERABOVE, false, INDEX_OVERABOVE}, + {"stboxOverafter", INDEX_OVERAFTER, false, INDEX_OVERAFTER}, + {"stboxOverback", INDEX_OVERBACK, false, INDEX_OVERBACK}, + {"stboxOverbefore", INDEX_OVERBEFORE, false, INDEX_OVERBEFORE}, + {"stboxOverbelow", INDEX_OVERBELOW, false, INDEX_OVERBELOW}, + {"stboxOverfront", INDEX_OVERFRONT, false, INDEX_OVERFRONT}, + {"stboxOverleft", INDEX_OVERLEFT, false, INDEX_OVERLEFT}, + {"stboxOverright", INDEX_OVERRIGHT, false, INDEX_OVERRIGHT}, + {"stboxRight", INDEX_RIGHT, true, INDEX_LEFT}, + {"tboxAfter", INDEX_AFTER, true, INDEX_BEFORE}, + {"tboxBefore", INDEX_BEFORE, true, INDEX_AFTER}, + {"tboxLeft", INDEX_LEFT, true, INDEX_RIGHT}, + {"tboxOverafter", INDEX_OVERAFTER, false, INDEX_OVERAFTER}, + {"tboxOverbefore", INDEX_OVERBEFORE, false, INDEX_OVERBEFORE}, + {"tboxOverleft", INDEX_OVERLEFT, false, INDEX_OVERLEFT}, + {"tboxOverright", INDEX_OVERRIGHT, false, INDEX_OVERRIGHT}, + {"tboxRight", INDEX_RIGHT, true, INDEX_LEFT}, + {"tpcboxAbove", INDEX_ABOVE, true, INDEX_BELOW}, + {"tpcboxAfter", INDEX_AFTER, true, INDEX_BEFORE}, + {"tpcboxBack", INDEX_BACK, true, INDEX_FRONT}, + {"tpcboxBefore", INDEX_BEFORE, true, INDEX_AFTER}, + {"tpcboxBelow", INDEX_BELOW, true, INDEX_ABOVE}, + {"tpcboxFront", INDEX_FRONT, true, INDEX_BACK}, + {"tpcboxLeft", INDEX_LEFT, true, INDEX_RIGHT}, + {"tpcboxOverabove", INDEX_OVERABOVE, false, INDEX_OVERABOVE}, + {"tpcboxOverafter", INDEX_OVERAFTER, false, INDEX_OVERAFTER}, + {"tpcboxOverback", INDEX_OVERBACK, false, INDEX_OVERBACK}, + {"tpcboxOverbefore", INDEX_OVERBEFORE, false, INDEX_OVERBEFORE}, + {"tpcboxOverbelow", INDEX_OVERBELOW, false, INDEX_OVERBELOW}, + {"tpcboxOverfront", INDEX_OVERFRONT, false, INDEX_OVERFRONT}, + {"tpcboxOverleft", INDEX_OVERLEFT, false, INDEX_OVERLEFT}, + {"tpcboxOverright", INDEX_OVERRIGHT, false, INDEX_OVERRIGHT}, + {"tpcboxRight", INDEX_RIGHT, true, INDEX_LEFT}, + {"|&>", INDEX_OVERABOVE, false, INDEX_OVERABOVE}, + {"|>>", INDEX_ABOVE, true, INDEX_BELOW}, + {"~=", INDEX_SAME, true, INDEX_SAME}, +}; + +} // namespace duckdb diff --git a/src/include/index/index_search_ops.hpp b/src/include/index/index_search_ops.hpp index 1a22c9b7..afb459ed 100644 --- a/src/include/index/index_search_ops.hpp +++ b/src/include/index/index_search_ops.hpp @@ -1,9 +1,8 @@ #pragma once -#include - #include "duckdb.hpp" #include "meos_wrapper_simple.hpp" +#include "generated/index_search_ops.hpp" namespace duckdb { @@ -19,86 +18,41 @@ enum IndexOperatorAxis { INDEX_AXIS_TIME, }; -//! A bounding-box operator an in-memory index answers, and the MEOS operation it asks the index -//! for. A predicate is read as `column query`, so an operator whose two sides play -//! different roles asks for a different operation once the query sits on the left: `a @> b` with -//! the indexed column on the left looks for stored boxes that CONTAIN the query, and the same -//! operator with the column on the right looks for stored boxes CONTAINED BY it. Both directions -//! are named here so that a caller reads the operation off the operand order rather than -//! assuming one. -struct IndexOperatorEntry { - const char *name; - //! The operation of `column query` - IndexSearchOp direct; - //! The operation of `query column` - IndexSearchOp commuted; - //! The axis the operator compares along, which decides the box types answering it - IndexOperatorAxis axis; -}; - -//! Every operator an index answers. An operator absent from this table is answered by a scan. -//! -//! The ordering operators are the negated duals of one another under an exchange of operands -- -//! `a << b` holds exactly when `b >> a` -- so the commuted column of each is its opposite. The set -//! mirrors the operators the PostgreSQL operator classes of the same box types index, so a -//! predicate that reaches an index here reaches one there. -//! -//! Equality and adjacency are their own opposites -- a box equals, or meets, another exactly when -//! that other equals, or meets, it -- so each names one operation in both columns, and both compare -//! whole extents, so every box type answers them. -//! -//! A bounding-box predicate is registered under two spellings, a symbol and a word, and both are -//! named here so that a query reaches the index however it is written. The temporal orderings are -//! the exception: they carry no symbol, `<<#` and its siblings not being operators DuckDB lexes, -//! so the word is the only spelling to route. -//! -//! A word is also reached through its class-prefixed spelling: the functions behind the position -//! operators carry the name of the class they compare, `stboxLeft`, `tboxBefore`, `spanOverleft`, -//! and such a spelling reaches the entry of the word it prefixes (see IndexOperatorWord). -static constexpr IndexOperatorEntry INDEX_OPERATORS[] = { - {"&&", INDEX_OVERLAPS, INDEX_OVERLAPS, INDEX_AXIS_EXTENT}, - {"overlaps", INDEX_OVERLAPS, INDEX_OVERLAPS, INDEX_AXIS_EXTENT}, - {"@>", INDEX_CONTAINS, INDEX_CONTAINED_BY, INDEX_AXIS_EXTENT}, - {"contains", INDEX_CONTAINS, INDEX_CONTAINED_BY, INDEX_AXIS_EXTENT}, - {"<@", INDEX_CONTAINED_BY, INDEX_CONTAINS, INDEX_AXIS_EXTENT}, - {"contained", INDEX_CONTAINED_BY, INDEX_CONTAINS, INDEX_AXIS_EXTENT}, - {"~=", INDEX_SAME, INDEX_SAME, INDEX_AXIS_EXTENT}, - {"same", INDEX_SAME, INDEX_SAME, INDEX_AXIS_EXTENT}, - {"-|-", INDEX_ADJACENT, INDEX_ADJACENT, INDEX_AXIS_EXTENT}, - {"adjacent", INDEX_ADJACENT, INDEX_ADJACENT, INDEX_AXIS_EXTENT}, - - {"<<", INDEX_LEFT, INDEX_RIGHT, INDEX_AXIS_HORIZONTAL}, - {"left", INDEX_LEFT, INDEX_RIGHT, INDEX_AXIS_HORIZONTAL}, - {"&<", INDEX_OVERLEFT, INDEX_OVERRIGHT, INDEX_AXIS_HORIZONTAL}, - {"overleft", INDEX_OVERLEFT, INDEX_OVERRIGHT, INDEX_AXIS_HORIZONTAL}, - {">>", INDEX_RIGHT, INDEX_LEFT, INDEX_AXIS_HORIZONTAL}, - {"right", INDEX_RIGHT, INDEX_LEFT, INDEX_AXIS_HORIZONTAL}, - {"&>", INDEX_OVERRIGHT, INDEX_OVERLEFT, INDEX_AXIS_HORIZONTAL}, - {"overright", INDEX_OVERRIGHT, INDEX_OVERLEFT, INDEX_AXIS_HORIZONTAL}, - - {"<<|", INDEX_BELOW, INDEX_ABOVE, INDEX_AXIS_VERTICAL}, - {"below", INDEX_BELOW, INDEX_ABOVE, INDEX_AXIS_VERTICAL}, - {"&<|", INDEX_OVERBELOW, INDEX_OVERABOVE, INDEX_AXIS_VERTICAL}, - {"overbelow", INDEX_OVERBELOW, INDEX_OVERABOVE, INDEX_AXIS_VERTICAL}, - {"|>>", INDEX_ABOVE, INDEX_BELOW, INDEX_AXIS_VERTICAL}, - {"above", INDEX_ABOVE, INDEX_BELOW, INDEX_AXIS_VERTICAL}, - {"|&>", INDEX_OVERABOVE, INDEX_OVERBELOW, INDEX_AXIS_VERTICAL}, - {"overabove", INDEX_OVERABOVE, INDEX_OVERBELOW, INDEX_AXIS_VERTICAL}, - - {"<>", INDEX_BACK, INDEX_FRONT, INDEX_AXIS_DEPTH}, - {"back", INDEX_BACK, INDEX_FRONT, INDEX_AXIS_DEPTH}, - {"/&>", INDEX_OVERBACK, INDEX_OVERFRONT, INDEX_AXIS_DEPTH}, - {"overback", INDEX_OVERBACK, INDEX_OVERFRONT, INDEX_AXIS_DEPTH}, - - {"before", INDEX_BEFORE, INDEX_AFTER, INDEX_AXIS_TIME}, - {"overbefore", INDEX_OVERBEFORE, INDEX_OVERAFTER, INDEX_AXIS_TIME}, - {"after", INDEX_AFTER, INDEX_BEFORE, INDEX_AXIS_TIME}, - {"overafter", INDEX_OVERAFTER, INDEX_OVERBEFORE, INDEX_AXIS_TIME}, -}; +//! The axis the MEOS search `op` compares along. The overlap, containment, equality and adjacency +//! searches compare whole extents; each ordering search, strict or overlapping, compares along +//! the one axis it names. +inline IndexOperatorAxis IndexSearchAxis(IndexSearchOp op) { + switch (op) { + case INDEX_OVERLAPS: + case INDEX_CONTAINS: + case INDEX_CONTAINED_BY: + case INDEX_SAME: + case INDEX_ADJACENT: + return INDEX_AXIS_EXTENT; + case INDEX_LEFT: + case INDEX_OVERLEFT: + case INDEX_RIGHT: + case INDEX_OVERRIGHT: + return INDEX_AXIS_HORIZONTAL; + case INDEX_BELOW: + case INDEX_OVERBELOW: + case INDEX_ABOVE: + case INDEX_OVERABOVE: + return INDEX_AXIS_VERTICAL; + case INDEX_FRONT: + case INDEX_OVERFRONT: + case INDEX_BACK: + case INDEX_OVERBACK: + return INDEX_AXIS_DEPTH; + case INDEX_BEFORE: + case INDEX_OVERBEFORE: + case INDEX_AFTER: + case INDEX_OVERAFTER: + return INDEX_AXIS_TIME; + default: + throw InternalException("IndexSearchAxis: unknown IndexSearchOp %d", (int) op); + } +} //! Return true if a box of `bbox_type` carries `axis`, and so answers the operators ordering //! along it. A spatiotemporal box carries every axis; a temporal box carries a value extent and a @@ -125,63 +79,34 @@ inline bool IndexBboxHasAxis(MeosType bbox_type, IndexOperatorAxis axis) { } } -//! The class prefixes a word of INDEX_OPERATORS takes in its class-prefixed spelling, longest first -//! so that `spansetLeft` is read as `spanset` + `Left` and never as `span` + `setLeft`. -static constexpr const char *INDEX_OPERATOR_CLASS_PREFIXES[] = { - "spanset", "tpcbox", "stbox", "tbox", "span", "set", -}; - -//! Return the word of INDEX_OPERATORS that `name` spells. A class-prefixed spelling, one of -//! INDEX_OPERATOR_CLASS_PREFIXES followed by an uppercase letter, spells the remainder with its -//! first letter lowercased: `stboxOverleft` spells `overleft`. Every other name, a symbol or an -//! unprefixed word, spells itself. -inline string IndexOperatorWord(const string &name) { - for (auto prefix : INDEX_OPERATOR_CLASS_PREFIXES) { - const size_t length = strlen(prefix); - if (name.size() > length && name.compare(0, length, prefix) == 0 && name[length] >= 'A' && - name[length] <= 'Z') { - string word = name.substr(length); - word[0] = static_cast(word[0] - 'A' + 'a'); - return word; - } - } - return name; -} - -//! Return the operation `name` asks of an index over `bbox_type`, false when such an index answers -//! no such operator. `name` is a symbol, a word or the word's class-prefixed spelling. -//! `query_on_left` names the operand order: true when the query is the left argument. +//! Return the search `name` asks of an index over `bbox_type`, false when such an index answers no +//! such operator. `name` is an operator symbol or the SQL name of a function backing it, as the +//! generated INDEX_OPERATORS states them. `query_on_left` names the operand order: true when the +//! query is the left argument, which asks for the commuted search, and an operator with none is +//! answered by a scan. inline bool IndexSearchOpFromName(const string &name, MeosType bbox_type, bool query_on_left, IndexSearchOp &result) { - const string word = IndexOperatorWord(name); for (auto &entry : INDEX_OPERATORS) { - if (word == entry.name && IndexBboxHasAxis(bbox_type, entry.axis)) { - result = query_on_left ? entry.commuted : entry.direct; - return true; + if (name != entry.name || !IndexBboxHasAxis(bbox_type, IndexSearchAxis(entry.direct))) { + continue; } + if (query_on_left && !entry.has_commuted) { + return false; + } + result = query_on_left ? entry.commuted : entry.direct; + return true; } return false; } -//! The operator names an index over `bbox_type` answers, each word under its bare and its -//! class-prefixed spellings. Overlap and containment compare whole extents, so every box type -//! answers those; an ordering operator is answered only where the box carries the axis it orders -//! along. +//! The operator names an index over `bbox_type` answers in at least one operand order: overlap and +//! containment compare whole extents, so every box type answers those, and an ordering operator is +//! answered only where the box carries the axis it orders along. inline unordered_set IndexOperatorNames(MeosType bbox_type) { unordered_set names; for (auto &entry : INDEX_OPERATORS) { - if (!IndexBboxHasAxis(bbox_type, entry.axis)) { - continue; - } - const string name = entry.name; - names.insert(name); - if (name[0] < 'a' || name[0] > 'z') { - continue; - } - string capitalized = name; - capitalized[0] = static_cast(capitalized[0] - 'a' + 'A'); - for (auto prefix : INDEX_OPERATOR_CLASS_PREFIXES) { - names.insert(prefix + capitalized); + if (IndexBboxHasAxis(bbox_type, IndexSearchAxis(entry.direct))) { + names.insert(entry.name); } } return names; diff --git a/src/index/rtree_optimize_scan.cpp b/src/index/rtree_optimize_scan.cpp index c9ceee02..16947e72 100644 --- a/src/index/rtree_optimize_scan.cpp +++ b/src/index/rtree_optimize_scan.cpp @@ -18,6 +18,7 @@ #include "index/rtree_module.hpp" #include "index/rtree_index_scan.hpp" +#include "index/index_search_ops.hpp" #include "time_util.hpp" @@ -95,6 +96,14 @@ class TRTreeIndexScanOptimizer : public OptimizerExtension { } else { return false; } + // The index answers an operator only in an operand order it has a search for: an + // overlapping ordering with the query on the left (`query &< column`) has none, so + // the predicate stays a filter and a scan answers it. + IndexSearchOp search_op; + if (!IndexSearchOpFromName(function_name, rtree_index.GetBboxType(), query_on_left, + search_op)) { + return false; + } Expression *const_expr = nullptr; diff --git a/src/index/sptree_optimize_scan.cpp b/src/index/sptree_optimize_scan.cpp index c5c70ffa..7fc91321 100644 --- a/src/index/sptree_optimize_scan.cpp +++ b/src/index/sptree_optimize_scan.cpp @@ -18,6 +18,7 @@ #include "index/sptree_module.hpp" #include "index/sptree_index_scan.hpp" +#include "index/index_search_ops.hpp" #include "time_util.hpp" @@ -95,6 +96,14 @@ class TSPTreeIndexScanOptimizer : public OptimizerExtension { } else { return false; } + // The index answers an operator only in an operand order it has a search for: an + // overlapping ordering with the query on the left (`query &< column`) has none, so + // the predicate stays a filter and a scan answers it. + IndexSearchOp search_op; + if (!IndexSearchOpFromName(function_name, sptree_index.GetBboxType(), query_on_left, + search_op)) { + return false; + } Expression *const_expr = nullptr; diff --git a/test/sql/parity/095_index_ordering_query_on_left.test b/test/sql/parity/095_index_ordering_query_on_left.test new file mode 100644 index 00000000..7d0eee7d --- /dev/null +++ b/test/sql/parity/095_index_ordering_query_on_left.test @@ -0,0 +1,72 @@ +# name: test/sql/parity/095_index_ordering_query_on_left.test +# description: the position operators with the query on the left agree with a scan of the same rows +# group: [sql] + +require mobilityduck + +statement ok +CREATE TABLE ord_idx(t tgeompoint); + +statement ok +CREATE TABLE ord_scan(t tgeompoint); + +statement ok +INSERT INTO ord_idx SELECT ('Point(' || i || ' ' || i || ')@' || strftime(DATE '2000-01-01' + INTERVAL (i) DAY, '%Y-%m-%d'))::tgeompoint FROM range(1,3000) tbl(i); + +statement ok +INSERT INTO ord_scan SELECT * FROM ord_idx; + +# only one of the two tables is indexed, so the answers below are the index's against a scan's +statement ok +CREATE INDEX i_ord ON ord_idx USING TRTREE (t); + +# A strict ordering with the query on the left asks the index for its opposite: `query << column` +# holds exactly when `column >> query` does. + +query II +EXPLAIN SELECT count(*) FROM ord_idx WHERE 'STBOX XT(((500,500),(501,501)),[2001-01-01,2001-01-02])'::stbox << t; +---- +physical_plan :.*MOBILITY RTREE INDEX.* + +query II +SELECT (SELECT count(*) FROM ord_idx WHERE 'STBOX XT(((500,500),(501,501)),[2001-01-01,2001-01-02])'::stbox << t), (SELECT count(*) FROM ord_scan WHERE 'STBOX XT(((500,500),(501,501)),[2001-01-01,2001-01-02])'::stbox << t); +---- +2498 2498 + +query II +SELECT (SELECT count(*) FROM ord_idx WHERE 'STBOX XT(((500,500),(501,501)),[2001-01-01,2001-01-02])'::stbox >> t), (SELECT count(*) FROM ord_scan WHERE 'STBOX XT(((500,500),(501,501)),[2001-01-01,2001-01-02])'::stbox >> t); +---- +499 499 + +# An overlapping ordering has no opposite: `query &< column` asks for a stored box whose end is not +# before the end of the query, which no index search states, so the predicate is answered by a scan. + +query II +EXPLAIN SELECT count(*) FROM ord_idx WHERE 'STBOX XT(((500,500),(501,501)),[2001-01-01,2001-01-02])'::stbox &< t; +---- +physical_plan :.*MOBILITY RTREE INDEX.* + +query II +SELECT (SELECT count(*) FROM ord_idx WHERE 'STBOX XT(((500,500),(501,501)),[2001-01-01,2001-01-02])'::stbox &< t), (SELECT count(*) FROM ord_scan WHERE 'STBOX XT(((500,500),(501,501)),[2001-01-01,2001-01-02])'::stbox &< t); +---- +2499 2499 + +query II +SELECT (SELECT count(*) FROM ord_idx WHERE stboxOverleft('STBOX XT(((500,500),(501,501)),[2001-01-01,2001-01-02])'::stbox, t)), (SELECT count(*) FROM ord_scan WHERE stboxOverleft('STBOX XT(((500,500),(501,501)),[2001-01-01,2001-01-02])'::stbox, t)); +---- +2499 2499 + +query II +SELECT (SELECT count(*) FROM ord_idx WHERE 'STBOX XT(((500,500),(501,501)),[2001-01-01,2001-01-02])'::stbox &> t), (SELECT count(*) FROM ord_scan WHERE 'STBOX XT(((500,500),(501,501)),[2001-01-01,2001-01-02])'::stbox &> t); +---- +500 500 + +query II +SELECT (SELECT count(*) FROM ord_idx WHERE stboxOverbefore('STBOX XT(((500,500),(501,501)),[2001-01-01,2001-01-02])'::stbox, t)), (SELECT count(*) FROM ord_scan WHERE stboxOverbefore('STBOX XT(((500,500),(501,501)),[2001-01-01,2001-01-02])'::stbox, t)); +---- +2633 2633 + +query II +SELECT (SELECT count(*) FROM ord_idx WHERE stboxOverafter('STBOX XT(((500,500),(501,501)),[2001-01-01,2001-01-02])'::stbox, t)), (SELECT count(*) FROM ord_scan WHERE stboxOverafter('STBOX XT(((500,500),(501,501)),[2001-01-01,2001-01-02])'::stbox, t)); +---- +366 366 diff --git a/tools/codegen_duck_udfs.py b/tools/codegen_duck_udfs.py index d03f1959..0c0a0972 100644 --- a/tools/codegen_duck_udfs.py +++ b/tools/codegen_duck_udfs.py @@ -5353,6 +5353,70 @@ def gen_temporal_covering(catalog, out_path): open(out_path, "w").write("\n".join(lines)) return len(entries) + +def gen_index_search_ops(catalog, out_path): + """The index search each operator name asks of an in-memory index, from the catalog's + per-signature `indexSearch` {columnLeft, columnRight} (MEOS-API docs/index-search.md), as + #gen_temporal_covering emits the covering from the catalog's temporalCovering. A signature is + reached under its SQL name and under its operator's symbol, the symbol read from the doc + comment of its columnLeft value in the catalog's IndexSearchOp enum ("..., `&<` operator"). + A name answering two different pairs fails generation: one name routes one way.""" + enum = next((e for e in catalog.get("enums", []) if e.get("name") == "IndexSearchOp"), None) + if enum is None: + raise SystemExit("FATAL: the catalog carries no IndexSearchOp enum") + symbol = {} + for v in enum["values"]: + m = re.search(r"`([^`]+)`\s+operator", v.get("doc") or "") + if not m: + raise SystemExit("FATAL: IndexSearchOp %s names no operator in its doc" % v["name"]) + symbol[v["name"]] = m.group(1) + routes = {} + for f in catalog["functions"]: + for sig in f.get("sqlSignatures") or []: + ix = sig.get("indexSearch") + if not ix: + continue + pair = (ix["columnLeft"], ix.get("columnRight")) + for name in (symbol[ix["columnLeft"]], sig.get("sqlName") or f["sqlfn"]): + routes.setdefault(name, set()).add(pair) + clash = sorted(n for n, pairs in routes.items() if len(pairs) > 1) + if clash: + raise SystemExit("FATAL: names routed to two index searches: %s" % ", ".join(clash)) + lines = [ + "// GENERATED by tools/codegen_duck_udfs.py from the MEOS-API catalog — DO NOT EDIT.", + "// The index search of every operator name, `column query` and `query column`,", + "// from the per-signature indexSearch of the catalog.", + "#pragma once", + "", + "#include \"meos_wrapper_simple.hpp\"", + "", + "namespace duckdb {", + "", + "//! A name an in-memory index answers and the MEOS search it asks for. A predicate is read as", + "//! `column query`; with the query on the left it asks for `commuted`, and an operator", + "//! whose MobilityDB declaration has no COMMUTATOR (the overlapping orderings) has none, so a", + "//! predicate written that way is answered by a scan.", + "struct IndexOperatorEntry {", + "\tconst char *name;", + "\t//! The search of `column query`", + "\tIndexSearchOp direct;", + "\t//! True when `query column` has a search", + "\tbool has_commuted;", + "\t//! The search of `query column`, meaningful only when has_commuted", + "\tIndexSearchOp commuted;", + "};", + "", + "static constexpr IndexOperatorEntry INDEX_OPERATORS[] = {", + ] + for name in sorted(routes): + left, right = next(iter(routes[name])) + lines.append(' {"%s", %s, %s, %s},' % (name, left, "true" if right else "false", + right or left)) + lines += ["};", "", "} // namespace duckdb", ""] + os.makedirs(os.path.dirname(out_path), exist_ok=True) + open(out_path, "w").write("\n".join(lines)) + return len(routes) + def main(): global RETIRED_GROUPS pos = [a for a in sys.argv[1:] if not a.startswith("--")] @@ -5440,6 +5504,12 @@ def main(): tcov = os.path.join(os.path.dirname(out), "generated_temporal_covering.cpp") ncov = gen_temporal_covering(d, tcov) print(f"wrote the TemporalParquet covering of {ncov} temporal types -> {tcov}") + # The index search of every operator name, from the catalog's per-signature indexSearch, + # emitted beside the hand-written routing that reads it (src/include/index). + isearch = os.path.join(os.path.dirname(os.path.dirname(out)), "include", "generated", + "index_search_ops.hpp") + nis = gen_index_search_ops(d, isearch) + print(f"wrote the index search of {nis} operator names -> {isearch}") # Regularity invariant (build-failing): MEOS keeps locale/collation, session # timezone, PROJ context and RNGs in thread-local storage, so every UDF body # must run EnsureMeosThreadInitialized() before any MEOS call. Verify it for