Repository navigation
Support for Indexes over joinedRecordTypes in Relational - #4653
g31pranjal wants to merge 5 commits into
Conversation
1af4681 to
8be6d28
Compare
8be6d28 to
74e50b5
Compare
0253487 to
090f325
Compare
eab11d4 to
b52220b
Compare
b52220b to
3e96455
Compare
32befa5 to
daff945
Compare
3e96455 to
c58b57c
Compare
7bc3d5f to
9c1bb12
Compare
…4706) Preparatory refactor, split out of #4653. `MaterializedViewIndexGenerator` named `RecordLayerUnnestedSyntheticTableGenerator` in four places, so supporting a second kind of synthetic table meant branching on which-of-two at each of them. This introduces: - **`SyntheticTableGenerator`** — what a kind of synthetic table has to provide: `checkSupported`, `rewrite`, `getType`, `generate`. - **`SyntheticTableGeneratorFactory`** — the single place that decides which kind a definition needs. `MaterializedViewIndexGenerator` now holds no reference to any concrete generator. Unnesting is the only kind here, so this is **behaviour-preserving**. There are no test changes, by design: the existing `UnnestedSyntheticTableIndexTest`, `IndexTest`, `DdlStatementParsingTest` and `SchemaTemplateSerDeTests` passing unchanged is the acceptance criterion. #4653 (DDL support for indexes over `JoinedRecordType`s) is stacked on top of this and adds the second implementation.
An index whose columns come from more than one table is defined on a joined synthetic record type, with one inner-joined constituent per table and key expressions rooted at a constituent alias. The equalities relating the tables become the synthetic table's join conditions; any remaining filter is stored as the index predicate and evaluated against the joined record. Builds on the generator interface introduced in the parent commit, adding the second implementation.
273e837 to
6bb8b65
Compare
12b6629 to
ff6f15a
Compare
| } | ||
|
|
||
| boolean hasSingleSource() { | ||
| return syntheticTableBuilder() == null || tables.size() == 1; |
There was a problem hiding this comment.
Was this meant to be inverted?
| return syntheticTableBuilder() == null || tables.size() == 1; | |
| return syntheticTableBuilder() != null || tables.size() == 1; |
| "Unsupported query, expected to find exactly one type filter operator"); | ||
| "Unsupported index definition, multiscan is not supported"); | ||
| // throws unless there is exactly one source | ||
| Assert.notNullUnchecked(source.hasSingleSource() , ErrorCode.UNSUPPORTED_OPERATION, |
There was a problem hiding this comment.
Was this meant to be an assert-that?
| Assert.notNullUnchecked(source.hasSingleSource() , ErrorCode.UNSUPPORTED_OPERATION, | |
| Assert.thatUnchecked(source.hasSingleSource(), ErrorCode.UNSUPPORTED_OPERATION, |
| @Nonnull | ||
| private final Map<CorrelationIdentifier, NonnullPair<String, RelationalExpression>> constituents = new HashMap<>(); |
There was a problem hiding this comment.
The CorrelationIdentifier numbering is arbitrary. We probably rather want this map to be in a well-defined order, i.e. FROM-clause order?
| final boolean hasOuterJoin) { | ||
| this.valuesByQuantifier = valuesByQuantifier; | ||
| this.explodes = explodes; | ||
| this.constituents = Map.copyOf(constituents); |
There was a problem hiding this comment.
| this.constituents = Map.copyOf(constituents); | |
| this.constituents = ImmutableMap.copyOf(constituents); |
| * | ||
| * This source file is part of the FoundationDB open source project | ||
| * | ||
| * Copyright 2021-2025 Apple Inc. and the FoundationDB project authors |
There was a problem hiding this comment.
| * Copyright 2021-2025 Apple Inc. and the FoundationDB project authors | |
| * Copyright 2021-2026 Apple Inc. and the FoundationDB project authors |
| } | ||
|
|
||
| @Nonnull | ||
| private AbstractMetadataOperationsFactory assertingFactory(@Nonnull final java.util.function.Consumer<SchemaTemplate> assertions) { |
There was a problem hiding this comment.
Nit: Import java.util.function.Consumer.
| return RecordLayerUnnestedSyntheticTableGenerator.initIfNeeded(spec, indexName, quantifierValues); | ||
| final var scanCount = spec.scanCount(); | ||
| final var hasUnnesting = !quantifierValues.getExplodes().isEmpty(); | ||
| if (scanCount != 1 && hasUnnesting) { |
There was a problem hiding this comment.
Was this also meant to be > 1 here?
| if (scanCount != 1 && hasUnnesting) { | |
| if (scanCount > 1 && hasUnnesting) { |
| } | ||
| final var joinConditions = ImmutableList.<JoinCondition>builder(); | ||
| final var residuals = ImmutableList.<QueryPredicate>builder(); | ||
| final var childPredicates = predicate instanceof AndPredicate |
There was a problem hiding this comment.
| final var childPredicates = predicate instanceof AndPredicate | |
| final var childPredicates = predicate instanceof AndPredicate predicate |
| * What an index is defined on: the stored tables its definition reads, as collected, or the synthetic record type a | ||
| * {@link SyntheticTableGenerator} rewrote it onto. | ||
| */ | ||
| record Source(@Nonnull Map<RecordLayerTable, Integer> tables, |
There was a problem hiding this comment.
What are the integers for?
|
|
||
| Type.Record getRecordType() { | ||
| return syntheticTableBuilder == null ? Iterables.getOnlyElement(tables.keySet()).getType() | ||
| : syntheticTableBuilder.build().getType(); |
There was a problem hiding this comment.
Can the syntheticTableBuilder.build().getType() be made more efficient? We’re doing a build() just to get the type.
| Assert.thatUnchecked(scanCount == 1, ErrorCode.UNSUPPORTED_OPERATION, | ||
| "Unsupported index definition, no iteration generator found"); | ||
| // throws unless exactly one type filter was found | ||
| Assert.notNullUnchecked(table, ErrorCode.UNSUPPORTED_OPERATION, | ||
| "Unsupported query, expected to find exactly one type filter operator"); | ||
| "Unsupported index definition, multiscan is not supported"); |
There was a problem hiding this comment.
Can scanCount also be 0? Then the multiscan message would be confusing.
| @Nonnull | ||
| @Override | ||
| public IndexSpec rewrite(@Nonnull final IndexSpec spec) { | ||
| Assert.isNullUnchecked(spec.groupBy(), "group by on an index over a joined synthetic table"); |
There was a problem hiding this comment.
Also ErrorCode.UNSUPPORTED_OPERATION here?
| Assert.thatUnchecked(projection.aggregate() == null, ErrorCode.UNSUPPORTED_OPERATION, | ||
| "Unsupported index definition, an aggregate cannot be defined on a joined synthetic table"); |
There was a problem hiding this comment.
Also enforce projection.groupBy() == null?
This PR adds support for indexes with shapes that warrant the need of
JoinedRecordType.What is supported
Syntax. Six spellings, converging on identical metadata: a comma join with the equality in
WHERE, an explicitINNER JOIN ... ON, orINNER JOIN ... USING, each declared withCREATE INDEX ... AS SELECTorCREATE INDEX ... ON <view>. They converge because the join condition is conjoined into theWHEREpredicate while the query graph is built.Shapes. N-way joins, self-joins (the same table under two constituents), columns of one table at non-adjacent key positions, and
keyWithValuesplit points.Predicates. The equalities relating two constituents become the synthetic table's join conditions. Anything else the definition asks for stays a filter, re-rooted at its constituent and stored as the index predicate, so only the joined records asked for are indexed.
Limitations
Rejected: outer joins; cross product; join with condition not an equality; a filter comparing two of the joined tables (as an unsupported predicate); a join of subqueries; aggregate indexes; version columns (the row version belongs to a stored record, so taking one constituent's would make the index depend on which side it was read through); and unnesting combined with a join.
Some of these are logical extension to the work in this PR, but has been deferred to be either be part of separate work or out of scope.
No planner support. This PR only adds support in the Relational. The query will fail to match the index owing to lack of support in the Cascades.
Implementation notes
RecordLayerJoinedSyntheticTableGeneratorfollows the same contract as its unnested counterpart —initIfNeeded/checkSupported/rewrite/generate— soMaterializedViewIndexGeneratoronly decides which kind applies. What it generates is theRecordLayerJoinedSyntheticTablefrom AddRecordLayerJoinedSyntheticTableand its metadata serialization #4737; this PR only adds the DDL path that builds one. The two kinds are exclusive; only a single-table definition goes on to need an unnested one.Constituent aliases are derived from the definition (
joined_0,joined_1, … in the order the tables were found).QuantifierValuesdo not "resolve" a joined constituent's correlation.Tests
JoinedSyntheticTableIndexTest(new): the four spellings agreeing; non-adjacent columns; three-way and self-joins; split point; residual predicate; an unnested and a joined table coexisting in one template; and one case per rejection above. Every positive case also round-trips its own output throughRecordMetaData, which validates, so the metadata is shown to be legal rather than merely well-formed.IndexTest— three existing tests updated, since joins are no longer rejected outright.