Repository navigation
Replace ExplodeExpression with a table function over an ExplodeValue - #4816
g31pranjal wants to merge 31 commits into
Conversation
324d296 to
ac94ca6
Compare
ac94ca6 to
fcf5662
Compare
…essages (#4631) Makes `MessageHelpers.deepCopyMessage()` match the fields of the source and target descriptors by position rather than by field number. ## Problem A record type read off a descriptor keeps the descriptor's field numbers, which may skip, while a type built up field by field (e.g. by `Type.maximumType()` for the element type of a multi-row `VALUES` list) is numbered by position. Two descriptors of the same structure can therefore number the same field differently, and a copy that matches fields by number writes a value into the wrong field. For a table whose proto is numbered `first_name=1, <2 skipped>, last_name=3, id=4`, `last_name` ends up in `id`: ``` IllegalArgumentException: Wrong object type used with protocol message reflection. Field number: 3, field java type: LONG, value type: java.lang.String ``` ## Changes - **`deepCopyMessage()`** matches fields by position. The target descriptor may have additional trailing fields, such as the pseudo-fields of a record type. - Tests in `MessageHelperTests` cover nested records, enums, repeated and nullable repeated records, and a target with pseudo-fields. The defect is latent on `main`. It shows up once every explode wraps its elements in a record constructor, as in #4816, where `skipped-field-number-proto.yamsql` and `field-index-tests-proto.yamsql` fail without this fix.
c8f9e16 to
c83eeb6
Compare
fcf5662 to
4189a88
Compare
c83eeb6 to
c631a7f
Compare
4189a88 to
6dcaba7
Compare
6dcaba7 to
5c132ff
Compare
Registers `PushInJoinThroughFetchRule` for `RecordQueryInComparandJoinPlan`, as it already is for `RecordQueryInValuesJoinPlan` and `RecordQueryInParameterJoinPlan`. The rule turns `InJoin(Fetch(inner))` into `Fetch(InJoin(inner))`. SQL `IN` predicates are planned as in-joins over an `InComparandSource`, so their plans can now run the in-join over a covering index scan and fetch the records above it. For example, in `join-with-order-by-tests.yamsql`: ``` before: [IN arrayDistinct(promote(@c21 AS ARRAY(LONG))) SORTED] | INJOIN q0 -> { ISCAN(t1$a1_a2 [EQUALS q0]) } | FLATMAP q1 -> { ... } after: [IN arrayDistinct(promote(@c21 AS ARRAY(LONG))) SORTED] | INJOIN q0 -> { COVERING(t1$a1_a2 [EQUALS q0] -> [a1: KEY:[0], a2: KEY:[1], id: KEY:[2]]) } | FETCH | FLATMAP q1 -> { ... } ``` ## Plan changes - `join-with-order-by-tests.yamsql` (4 queries) and `join-tests-outer.yamsql:460`: the in-join runs over a covering index scan, and the records are fetched above it. - `join-tests.yamsql:399`: the join order flips, and the in-join over `emp` now drives the lookup of `dept`. Both sides are primary-key lookups. - Planner metrics change in `array-join-at`, `in-predicate`, `join-tests`, `join-tests-outer`, `join-with-order-by-tests`, `right-deep-plan-tests` and `versions-tests`. No results change. - `PreparedStatementTests.prepareInListOfTuple` asserted the order of the rows of a query without `ORDER BY`, which changes with the new plan. It now checks the set of rows, as #4816 does. #4816 plans every IN predicate as an in-join over an `InExplodeSource`, which is a comparand source, and registers this rule too. The changes it lists as not yet accepted (`join-tests.yamsql:332`, `join-tests-outer.yamsql:456` and `right-deep-plan-tests.yamsql:37`) come from this rule.
6dbf12e to
6f6655b
Compare
633179c to
cc0725d
Compare
ab7ea75 to
3d877a9
Compare
48fef87 to
1492507
Compare
3d877a9 to
8277791
Compare
1492507 to
fee82ec
Compare
8277791 to
14b2dfe
Compare
Ports 37 `fdb-record-layer-core` record query tests to the yamsql suite under `yaml-tests/src/test/resources/cascades-query-tests/`. A trimmed-down version of #4588 holding only these tests. These are the tests whose `IN` source (a `LiteralValue` list or a named parameter) is no longer compatible with the Cascades run of the JUnit test after #4816, so their Cascades coverage moves to SQL. The JUnit tests stay in place, are marked with `// SQL Port status: (#4843) …`, and skip the Cascades run via `Assumptions.assumeFalse(useCascadesPlanner)` (`FDBInQueryTest`) or are restricted to the old planner. ## What is in the PR - 17 yamsql files (84 queries) plus the shared `includes/simple-record-100.yamsql`, registered in `CascadesQueryTests`. Metrics are generated for every file. - `MIGRATION.md` (per-method state) and `README.md`, cut down to these three classes. - `SQL Port status` / `SQL Port` comments on all 37 JUnit tests. ## How the `IN` source is ported | Core test uses | Port | |---|---| | literal list (`ListComparison`), or `ConstantObjectValue` | `IN (…)` literal list. `AstNormalizer` lifts every literal into a `ConstantObjectValue`, so both are the same SQL | | named parameter (`ParameterComparison`, `equalsParameter`) | parameter injection, `in !! !in {[…]} !!` / `= !! 0 !!`, in a separate test block with `statement_type: prepared` (named `<file>-prepared`) | Every query has its own test: nothing is shared between two tests, and each `reverse` / `p` / `str_list` / ordering variant is its own query. The remaining parameterizations (`InAsOrUnionMode`, `omitPrimaryKeyInOrdering`, `replans`, `normalizeNestedFields`, `attemptFailedInJoinAsUnionMaxSize`) are planner or old-planner settings with no SQL surface and collapse. `LIMIT` is not supported in SQL, so `testInWithLimit` pages with `maxRows: 3`. Where a variant needs its own schema (`dropNumValue3Index`), it has its own file. ## The 37 tests `qN` is the position among the executed queries in the file; the `-prepared` block follows the literal one. ### `FDBInQueryTest` (31) | Test | IN source in core | yamsql (queries) | State | |---|---|---|---| | `testInQueryNoIndex` | literal `(0, 2)`, residual filter | in-query q1 | ported | | `testInQueryNoIndexWithParameter` | parameter `valuesThree`, residual filter | in-query q10 (prepared) | ported | | `testInQueryIndex` | literal `(1, 2, 4)`, forward and reverse | in-query q2-q3 | ported | | `testInQueryIndexSorted` | literal `(1, 4, 2)` (out of order), forward and reverse | in-query q4-q5 | ported | | `testInQueryCoveringIndex` | literal `(1, 2, 4)`, forward and reverse, covering | in-query q6-q7 | ported | | `testInQueryIndexSortedDifferently` | literal `(1, 4, 2)`, sorted on another column, forward and reverse | in-query q8-q9 | ported | | `testInQueryParameter` | parameter `valueThrees`, no sort | in-query q11 (prepared) | ported | | `testInQuerySortedByParameter` | parameter `valueThrees`, forward and reverse | in-query q12-q13 (prepared) | ported | | `testInQueryParameter2` | parameter `valueThrees` + `equalsParameter("p")`, p = 0..4, forward and reverse | in-query-multi-index q1-q10 (prepared) | ported | | `testInQueryParameter2UsingCoveringIndexScans` | same, covering | in-query-multi-index q11-q20 (prepared) | ported | | `testInQueryParameterBad` | parameter bound to a non-list | none | not portable: a SQL IN list is always a list | | `testListInQuerySortedWithExtraUnorderedColumns` | literal `(1, 2, 4)`, forward and reverse | in-query-sorted-extra-unordered-columns q1-q2 | ported | | `testParameterizedInQuerySortedWithExtraUnorderedColumns` | parameter list + `equalsParameter`, forward and reverse | in-query-sorted-extra-unordered-columns q3-q4 (prepared) | ported | | `inQueryWithSortBySecondFieldOfCompoundIndex` | literal `(1, 4, 2)` | in-query-compound-index q1 | ported | | `inQueryWithSortAndRangePredicateOnSecondFieldOfCompoundIndex` | literal `(1, 4, 2)` + range | in-query-compound-index q2 | ported | | `inQueryWithSortAndParameter` | parameter `inList` + range | in-query-compound-index q3 (prepared) | ported | | `testInWithNesting` | literal, 4 strings, nested field | in-query-header q1 | ported | | `testMultipleInQueryIndex` | two literals (`rec_no`, `path`) | in-query-header q2 | ported | | `testMultipleInQueryIndexSorted` | two literals, index order requested | in-query-header q3 | ported | | `testInWithLimit` | literal `ls`, returned-row limit | in-query-header q4 (`maxRows: 3`) | ported | | `testInWithContinuation` | literal `ls`, continuations | in-query-header q5 (`maxRows: 1`) | ported | | `testInQueryOr` | literal `(903, 905, 901)` OR a range | in-query-or q1 | ported | | `testInQueryOrDifferentCondition` | literal `(2, 0)` OR a range, residual filter | in-query-or q2 | ported | | `testInQueryOrMultipleIndexes` | literal `(1, 3)` OR another index | in-query-or q3 | ported | | `testInUnionWithNonSargable` | literal `(1, 3, 5, 7, 9)`, residual filter | in-query-or q4 | ported | | `testInQueryEmptyList` | empty literal list | in-query-or q5 (prepared; `in ()` does not parse) | ported | | `testInQueryOrOverlap` | two overlapping literals | in-query-or-overlap q1 | ported | | `testInQueryOrCompound` | literal `(1, 3)` in OR in AND | in-query-or-compound q1 | ported | | `testInUnionWithSomeSargable` | parameters `nv2_list`, `str_list` | in-query-union-sargable q1-q3 (prepared) | ported | | `testInUnionWithIntersectionOnTwoPredicates` | parameters `nv2_list`, `str_list`; `dropNumValue3Index` changes the schema | in-query-union-intersection q1-q3, in-query-union-intersection-no-nv3 q1-q3 (prepared) | ported | | `enumIn` | literal list of enum values | none | deferred: needs `CREATE TYPE AS ENUM` | ### `FDBPermutedMinMaxQueryTest` (4) Each is parameterized over `InComparisonCase`: `byLiteral` and `byConstantObjectValue` are the literal block, `byParameter` is the `-prepared` block. | Test | yamsql (queries) | State | |---|---|---| | `selectMaxWithInOrderByMax` | permuted-min-max q1-q3 literal, q4-q6 prepared | ported | | `testMaxWithInAndDupes` | permuted-min-max-num-value-2 q1 literal, q2 prepared | ported | | `testSortedMaxWithInOnRepeater` | permuted-min-max-repeater q1-q2 literal, q3-q4 prepared | ported | | `testMaxUniqueByStr2And3WithDifferentOrderingKeys` | permuted-min-max-ordering-keys q1-q4 literal, q5-q8 prepared (one per ordering key) | ported | ### `FDBSimpleQueryGraphTest` (2) | Test | IN source in core | yamsql (queries) | State | |---|---|---|---| | `testSimpleExistentialPredicateOnSimpleIndex` | parameter `t`; `inComparison` flips `EQUALS` / `IN` | simple-query-graph-tag-index q1 (`=`), q2 (`IN`), prepared | ported | | `testEqualityAndSimpleExistentialPredicate` | parameters on `name` and `t` | simple-query-graph-name-tag-index q1 (prepared) | ported | Totals: 35 ported, 1 not portable (`testInQueryParameterBad`), 1 deferred (`enumIn`). ## Notes - Expectations are generated against current `main`, so plans carry the covering-scan-plus-fetch shape from #4830. - Plan-shape divergence from the JUnit plans: for the extra-columns tests the in-union comparison key has three values instead of four, and SQL always emits a `MAP` above a covering scan. This is documented in the header of `in-query-sorted-extra-unordered-columns.yamsql`.
80ac3f9 to
30f79e7
Compare
14b2dfe to
1f13218
Compare
The explains of the plain explodes now read `TF explode(..)` and navigate `_._0`, and the planner metrics moved with them. The nested repeated exists query keeps its covering scan expectation, as the plan it now gets is a regression to be fixed separately.
The in-join plans keep the shape they have on main, with the element read as `q0._0`.
…nsion The explode quantifier now flows an (element) struct, so pulling up its flowed columns gave one opaque column per level where a query's select over the same explodes flows the fields of each element. The pulled-up query result then was a projection out of that pair and needed a result compensation, which a join match cannot take, so the covering scan of an index over nested repeated fields was lost. Pull up the element's fields instead, as before the explode flowed a struct, and restore the planner metrics that the regression had changed.
The IN list of the left join over the null-supplying side is planned with the explode as the outer side of the join with dept. The two orders tie on every cost tiebreaker, and the plan hash that decides between them moved with the explode's representation.
…eir explode matchers The ported FDBInQueryTest tests no longer run under Cascades, so their Cascades assertions, and the matchers for literal and parameter explodes only they used, are unreachable.
Only an IN over a value comparison is turned into an explode. The other INs stay predicates, so the in-join and in-union rules need not special-case literal and parameter collections.
…y to preserveOrder
…dex definition's values
deb72dd to
6668b36
Compare
Its only use was in a test that aborts before planning, so restore main's inParameterJoinPlan there.
fdb616e to
4aa5ec2
Compare
📊 Metrics Diff Analysis ReportSummary
ℹ️ About this analysisThis automated analysis compares query planner metrics between the base branch and this PR. It categorizes changes into:
The last category in particular may indicate planner regressions that should be investigated. Plan and Metrics ChangedThese queries experienced both plan and metrics changes. This generally indicates that there was some planner change Total: 11 queries Statistical Summary (Plan and Metrics Changed)
There were no queries with significant regressions detected. Minor Changes (Plan and Metrics Changed)In addition, there were 11 queries with minor changes. Only Metrics ChangedThese queries experienced only metrics changes without any plan changes. If these metrics have substantially changed, Total: 4 queries Statistical Summary (Only Metrics Changed)
There were no queries with significant regressions detected. Minor Changes (Only Metrics Changed)In addition, there were 4 queries with minor changes. |
Replaces
ExplodeExpressionwith aTableFunctionExpressionover anExplodeValue, so that every explode in the Cascades planner is a streaming value that produces an(element, ordinal)struct per element. Builds on #4819, which addedExplodeValue,InExplodeSourceand their serialization.Changes
ExplodeExpressionandImplementExplodeRuleare removed. IN predicates,oneOfThem, fan-out key expressions,UnnestedRecordTypeexpansion and SQL unnesting andVALUESall create aTableFunctionExpression(ExplodeValue). Elements are read withExplodeValue.elementValue(_0), ordinals withExplodeValue.ordinalValue(_1).UnnestedRecordTypeexpansion and SQLAT. Any other explode may be reordered.InComparisonToExplodeRuleexplodes only an IN over a value comparison whose element type is resolved. An IN over a literal list or a parameter stays a predicate.InExplodeSource. A sorted source is the explodesorted(...), which is only possible for an explode that does not preserve its order, and only establishes the order of a value compared to the element itself.Plan hashes of plans with explodes change, and the expected plans, hashes and planner metrics in the tests and yaml-tests are updated accordingly.