From 6283e00467ed677097b5e889d353af1ffe844565 Mon Sep 17 00:00:00 2001 From: madschemas <155993105+MadSchemas@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:34:25 +0300 Subject: [PATCH 1/3] [fea] Implement nested joins support --- .../java/ru/rt/restream/reindexer/Query.java | 72 +++- .../reindexer/QueryResultIterator.java | 40 +- .../connector/BuiltinNestedJoinTest.java | 27 ++ .../connector/CprotoNestedJoinTest.java | 27 ++ .../reindexer/connector/NestedJoinTest.java | 351 ++++++++++++++++++ 5 files changed, 477 insertions(+), 40 deletions(-) create mode 100644 src/test/java/ru/rt/restream/reindexer/connector/BuiltinNestedJoinTest.java create mode 100644 src/test/java/ru/rt/restream/reindexer/connector/CprotoNestedJoinTest.java create mode 100644 src/test/java/ru/rt/restream/reindexer/connector/NestedJoinTest.java diff --git a/src/main/java/ru/rt/restream/reindexer/Query.java b/src/main/java/ru/rt/restream/reindexer/Query.java index e288583..de1c2a5 100644 --- a/src/main/java/ru/rt/restream/reindexer/Query.java +++ b/src/main/java/ru/rt/restream/reindexer/Query.java @@ -213,14 +213,11 @@ public enum Condition { private Query root; - private final int queryFormatVersion; - Query(Reindexer reindexer, ReindexerNamespace namespace, TransactionContext transactionContext) { logBuilder.namespace(namespace.getName()); this.reindexer = reindexer; this.namespace = namespace; this.transactionContext = transactionContext; - this.queryFormatVersion = reindexer.getBinding().queryFormatVersion(); buffer.putUInt8(0); buffer.putVString(namespace.getName()); } @@ -249,7 +246,9 @@ public Query selectAllFields() { } /** - * Inner joins 2 queries, alias for innerJoin. + * Inner joins 2 queries, alias for {@link #innerJoin(Query, String)}. + *

+ * Nested joins are supported the same way as in {@link #innerJoin(Query, String)}. * * @param type of joined items * @param joinQuery query to join @@ -264,6 +263,20 @@ public Query join(Query joinQuery, String field) { /** * Inner joins 2 queries. + *

+ * {@code joinQuery} may itself contain nested inner/left joins; attach those joins to the subquery + * before passing it here. Nested joins require QueryFormatV2 (always used by builtin; negotiated for + * cproto). With QueryFormatV1 {@link #execute()} throws {@link IllegalStateException}. + *

+ * Call {@link #on(String, Condition, String)} on the join subquery, not on this query: + *

{@code
+     * Query locations = db.query("locations", Location.class)
+     *         .on("locationId", EQ, "id");
+     * Query authors = db.query("authors", Author.class)
+     *         .innerJoin(locations, "locations")
+     *         .on("authorId", EQ, "id");
+     * db.query("books", Book.class).innerJoin(authors, "authors").toList();
+     * }
* * @param type of joined items * @param joinQuery query to join @@ -283,6 +296,8 @@ public Query innerJoin(Query joinQuery, String field) { /** * Left joins 2 queries. + *

+ * Nested joins are supported the same way as in {@link #innerJoin(Query, String)}. * * @param type of joined items * @param joinQuery query to join @@ -316,10 +331,13 @@ private Query join(Query joinQuery, String field, int joinType) { /** * Specify the join condition. + *

+ * Call this on the join subquery (the right side), before or after attaching it with + * {@link #innerJoin(Query, String)} / {@link #leftJoin(Query, String)}. * - * @param joinField the join field of the right side of the join + * @param joinField the join field of the left side of the join * @param condition the joining condition. {@link Condition} - * @param joinIndex the join index of the left side of join + * @param joinIndex the join index of the right side of the join * @return the {@link Query} for further customizations */ public Query on(String joinField, Condition condition, String joinIndex) { @@ -1050,6 +1068,7 @@ private byte[] buildSelectQueryBytes() { } int formatVersion = reindexer.getBinding().queryFormatVersion(); + ensureNoMergeNestedInJoin(); ByteBuffer queryBuffer = new ByteBuffer(getQueryBytes(formatVersion)); queryBuffer.putVarUInt32(QUERY_END); if (formatVersion == QUERY_FORMAT_V2) { @@ -1313,23 +1332,26 @@ String getSql() { } public byte[] bytes() { - return toSubQueryBytes(queryFormatVersion); + return toSubQueryBytes(reindexer.getBinding().queryFormatVersion()); } private byte[] toSubQueryBytes(int formatVersion) { - byte[] queryBytes = getQueryBytes(formatVersion); - if (formatVersion == QUERY_FORMAT_V2 || hasNestedJoins()) { - ByteBuffer copy = new ByteBuffer(queryBytes); - copy.putVarUInt32(QUERY_END); - copy.putVarUInt32(0); - copy.putVarUInt32(0); - return copy.bytes(); + if (formatVersion == QUERY_FORMAT_V2) { + ByteBuffer queryBuffer = new ByteBuffer(getQueryBytes(formatVersion)); + queryBuffer.putVarUInt32(QUERY_END); + appendJoinQueries(queryBuffer, new ArrayList<>(), formatVersion); + appendMergeQueries(queryBuffer, new ArrayList<>(), formatVersion); + return queryBuffer.bytes(); } - return queryBytes; + if (!joinQueries.isEmpty() || !mergeQueries.isEmpty()) { + throw new IllegalStateException("Join and merge queries in subquery are not supported by QueryFormatV1"); + } + return getQueryBytes(formatVersion); } private byte[] toExecutableBytes() { int formatVersion = reindexer.getBinding().queryFormatVersion(); + ensureNoMergeNestedInJoin(); ByteBuffer queryBuffer = new ByteBuffer(getQueryBytes(formatVersion)); queryBuffer.putVarUInt32(QUERY_END); if (formatVersion == QUERY_FORMAT_V2) { @@ -1424,6 +1446,26 @@ private boolean hasNestedJoins() { return false; } + private void ensureNoMergeNestedInJoin() { + if (hasMergeNestedInJoin()) { + throw new IllegalStateException("MERGEs nested into the JOINs are not supported"); + } + } + + private boolean hasMergeNestedInJoin() { + for (Query joinQuery : joinQueries) { + if (!joinQuery.mergeQueries.isEmpty() || joinQuery.hasMergeNestedInJoin()) { + return true; + } + } + for (Query mergeQuery : mergeQueries) { + if (mergeQuery.hasMergeNestedInJoin()) { + return true; + } + } + return false; + } + /** * Returns the string representation of the query. * diff --git a/src/main/java/ru/rt/restream/reindexer/QueryResultIterator.java b/src/main/java/ru/rt/restream/reindexer/QueryResultIterator.java index b8e530f..051465a 100644 --- a/src/main/java/ru/rt/restream/reindexer/QueryResultIterator.java +++ b/src/main/java/ru/rt/restream/reindexer/QueryResultIterator.java @@ -94,12 +94,7 @@ private void parseQueryResult(QueryResult queryResult) { if (queryResult.isJson()) { throw new UnsupportedOperationException("Query result in json format is not supported"); } else { - CtagMatcher ctagMatcher = new CtagMatcher(); - PayloadType payloadType = namespace.getPayloadType(); - if (payloadType != null) { - ctagMatcher.read(payloadType); - } - itemReader = new CjsonItemReader<>(itemClass, ctagMatcher); + itemReader = newItemReader(itemClass, namespace); } } } @@ -128,22 +123,15 @@ public T next() { fetchResults(); } - T item = itemClass.cast(readItem(namespace, itemReader, query)); + T item = itemClass.cast(readItem(itemReader, query)); position++; return item; - } - private S readItem(ReindexerNamespace expectedNamespace, ItemReader reader, Query queryContext) { + private S readItem(ItemReader reader, Query queryContext) { ItemParams params = readItemParams(); Query itemQueryContext = getItemQueryContext(queryContext, params.nsId); - - ReindexerNamespace itemNamespace = expectedNamespace; - if (query != null && params.nsId < query.getNamespaces().size()) { - itemNamespace = query.getNamespaces().get(params.nsId); - } - - S item = readItemData(params, reader, itemNamespace); + S item = readItemData(params, reader); readJoinedItems(item, itemQueryContext, params.nsId); return item; } @@ -161,7 +149,7 @@ private Query getItemQueryContext(Query defaultQueryContext, int nsId) { return defaultQueryContext; } - private S readItemData(ItemParams params, ItemReader reader, ReindexerNamespace itemNamespace) { + private S readItemData(ItemParams params, ItemReader reader) { if (params.cptr != 0) { ByteBuffer nativeBuffer = NativeUtils.getNativeBuffer(queryResult.getResultsPtr(), params.cptr, params.nsId); @@ -194,10 +182,10 @@ private void readJoinedItems(Object item, Query queryContext, int nsId) { Query joinQuery = queryContext.getJoinQueries().get(joinedField); ReindexerNamespace joinedNamespace = joinQuery.getNamespace(); - CjsonItemReader joinedItemReader = newItemReader(joinedNamespace); + CjsonItemReader joinedItemReader = newItemReader(joinedNamespace.getItemClass(), joinedNamespace); List subItems = new ArrayList<>(itemsCount); for (int i = 0; i < itemsCount; i++) { - subItems.add(readItem(joinedNamespace, joinedItemReader, joinQuery)); + subItems.add(readItem(joinedItemReader, joinQuery)); } subItemsMap.computeIfAbsent(queryContext.getJoinFields().get(joinedField), field -> new ArrayList<>()) .addAll(subItems); @@ -219,11 +207,11 @@ private void readJoinedItemsV1(Object item, int nsId) { for (int nsIndex = 0; nsIndex < joinedFields; nsIndex++) { int itemsCount = (int) buffer.getVarUInt(); ReindexerNamespace joinedNamespace = query.getNamespaces().get(nsIndex + namespaceIndexOffset); - CjsonItemReader joinedItemReader = newItemReader(joinedNamespace); + CjsonItemReader joinedItemReader = newItemReader(joinedNamespace.getItemClass(), joinedNamespace); List subItems = new ArrayList<>(itemsCount); for (int j = 0; j < itemsCount; j++) { ItemParams subItemParams = readItemParams(); - subItems.add(readItemData(subItemParams, joinedItemReader, joinedNamespace)); + subItems.add(readItemData(subItemParams, joinedItemReader)); } String joinField = query.getJoinFields().get(nsIndex); @@ -281,11 +269,13 @@ private int getJoinedNsIndexOffset(int nsId) { return offset; } - private CjsonItemReader newItemReader(ReindexerNamespace itemNamespace) { - PayloadType payloadType = itemNamespace.getPayloadType(); + private CjsonItemReader newItemReader(Class itemClass, ReindexerNamespace itemNamespace) { CtagMatcher ctagMatcher = new CtagMatcher(); - ctagMatcher.read(payloadType); - return new CjsonItemReader<>(itemNamespace.getItemClass(), ctagMatcher); + PayloadType payloadType = itemNamespace.getPayloadType(); + if (payloadType != null) { + ctagMatcher.read(payloadType); + } + return new CjsonItemReader<>(itemClass, ctagMatcher); } private void writeJoinResult(Object item, String fieldName, List subItems) { diff --git a/src/test/java/ru/rt/restream/reindexer/connector/BuiltinNestedJoinTest.java b/src/test/java/ru/rt/restream/reindexer/connector/BuiltinNestedJoinTest.java new file mode 100644 index 0000000..ec7d2e4 --- /dev/null +++ b/src/test/java/ru/rt/restream/reindexer/connector/BuiltinNestedJoinTest.java @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Restream + * + * 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. + */ + +package ru.rt.restream.reindexer.connector; + +import ru.rt.restream.category.BuiltinTest; + +/** + * Tests for Builtin implementation. + */ +@BuiltinTest +public class BuiltinNestedJoinTest extends NestedJoinTest { + +} diff --git a/src/test/java/ru/rt/restream/reindexer/connector/CprotoNestedJoinTest.java b/src/test/java/ru/rt/restream/reindexer/connector/CprotoNestedJoinTest.java new file mode 100644 index 0000000..cdb04ab --- /dev/null +++ b/src/test/java/ru/rt/restream/reindexer/connector/CprotoNestedJoinTest.java @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Restream + * + * 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. + */ + +package ru.rt.restream.reindexer.connector; + +import ru.rt.restream.category.CprotoTest; + +/** + * Tests for Cproto implementation. + */ +@CprotoTest +public class CprotoNestedJoinTest extends NestedJoinTest { + +} diff --git a/src/test/java/ru/rt/restream/reindexer/connector/NestedJoinTest.java b/src/test/java/ru/rt/restream/reindexer/connector/NestedJoinTest.java new file mode 100644 index 0000000..2311e04 --- /dev/null +++ b/src/test/java/ru/rt/restream/reindexer/connector/NestedJoinTest.java @@ -0,0 +1,351 @@ +/* + * Copyright 2020 Restream + * + * 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. + */ +package ru.rt.restream.reindexer.connector; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.junit.jupiter.api.Test; +import ru.rt.restream.reindexer.NamespaceOptions; +import ru.rt.restream.reindexer.Query; +import ru.rt.restream.reindexer.annotations.Reindex; +import ru.rt.restream.reindexer.annotations.Transient; +import ru.rt.restream.reindexer.db.DbBaseTest; +import ru.rt.restream.reindexer.exceptions.ReindexerException; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static ru.rt.restream.reindexer.Query.Condition.EQ; +import static ru.rt.restream.reindexer.Query.Condition.SET; + +/** + * Base nested join test. + */ +public abstract class NestedJoinTest extends DbBaseTest { + + private static final String BOOKS_NS = "nested_join_books"; + private static final String AUTHORS_NS = "nested_join_authors"; + private static final String LOCATIONS_NS = "nested_join_locations"; + private static final String COUNTRIES_NS = "nested_join_countries"; + + @Test + public void testNestedInnerJoin() { + openNamespaces(); + insertFixture(); + + Query authors = db.query(AUTHORS_NS, Author.class) + .innerJoin(db.query(LOCATIONS_NS, Location.class) + .on("locationId", EQ, "id"), "locations") + .on("authorId", EQ, "id"); + + Map booksById = byId(db.query(BOOKS_NS, Book.class) + .innerJoin(authors, "authors") + .toList()); + + assertThat(booksById.size(), is(3)); + assertAuthorLocation(booksById.get(1000), 100, "Author1", "Moscow"); + assertAuthorLocation(booksById.get(1002), 100, "Author1", "Moscow"); + assertAuthorLocation(booksById.get(1003), 101, "Author2", "Paris"); + assertThat(booksById.containsKey(1001), is(false)); + assertThat(booksById.containsKey(1004), is(false)); + } + + @Test + public void testLeftJoinWithNestedInnerJoin() { + openNamespaces(); + insertFixture(); + + Query authors = db.query(AUTHORS_NS, Author.class) + .innerJoin(db.query(LOCATIONS_NS, Location.class) + .on("locationId", EQ, "id"), "locations") + .on("authorId", EQ, "id"); + + Map booksById = byId(db.query(BOOKS_NS, Book.class) + .leftJoin(authors, "authors") + .toList()); + + assertThat(booksById.size(), is(5)); + assertAuthorLocation(booksById.get(1000), 100, "Author1", "Moscow"); + assertThat(booksById.get(1001).authors.size(), is(0)); + assertAuthorLocation(booksById.get(1002), 100, "Author1", "Moscow"); + assertAuthorLocation(booksById.get(1003), 101, "Author2", "Paris"); + assertThat(booksById.get(1004).authors.size(), is(0)); + } + + @Test + public void testInnerJoinWithNestedEmptyLeftJoin() { + openNamespaces(); + insertFixture(); + + Query authors = db.query(AUTHORS_NS, Author.class) + .leftJoin(db.query(LOCATIONS_NS, Location.class) + .where("city", EQ, "NoSuchCity") + .on("locationId", EQ, "id"), "locations") + .on("authorId", EQ, "id"); + + List books = db.query(BOOKS_NS, Book.class) + .where("title", EQ, "Book1") + .innerJoin(authors, "authors") + .toList(); + + assertThat(books.size(), is(1)); + Book book = books.get(0); + assertThat(book.authors.size(), is(1)); + Author author = book.authors.get(0); + assertThat(author.id, is(book.authorId)); + assertThat(author.name, is("Author1")); + assertThat(author.locations.size(), is(0)); + } + + @Test + public void testMergeWithNestedJoins() { + openNamespaces(); + insertFixture(); + + Query first = db.query(BOOKS_NS, Book.class) + .where("title", EQ, "Book1") + .innerJoin(db.query(AUTHORS_NS, Author.class) + .innerJoin(db.query(LOCATIONS_NS, Location.class) + .on("locationId", EQ, "id"), "locations") + .on("authorId", EQ, "id"), "authors"); + + Query second = db.query(BOOKS_NS, Book.class) + .where("title", EQ, "OtherBook") + .innerJoin(db.query(AUTHORS_NS, Author.class) + .innerJoin(db.query(LOCATIONS_NS, Location.class) + .on("locationId", EQ, "id"), "locations") + .on("authorId", EQ, "id"), "authors"); + + List books = first.merge(second).toList(); + assertThat(books.size(), is(2)); + + Map booksById = byId(books); + assertAuthorLocation(booksById.get(1000), 100, "Author1", "Moscow"); + assertAuthorLocation(booksById.get(1002), 100, "Author1", "Moscow"); + } + + @Test + public void testNestedJoinDepthTwoPlus() { + openNamespaces(); + insertFixture(); + + Query locations = db.query(LOCATIONS_NS, Location.class) + .innerJoin(db.query(COUNTRIES_NS, Country.class) + .on("countryId", EQ, "id"), "countries") + .on("locationId", EQ, "id"); + + Query authors = db.query(AUTHORS_NS, Author.class) + .innerJoin(locations, "locations") + .on("authorId", EQ, "id"); + + Map booksById = byId(db.query(BOOKS_NS, Book.class) + .innerJoin(authors, "authors") + .toList()); + + assertThat(booksById.size(), is(3)); + assertAuthorLocation(booksById.get(1000), 100, "Author1", "Moscow"); + assertThat(booksById.get(1000).authors.get(0).locations.get(0).countries.get(0).name, is("Northern")); + assertAuthorLocation(booksById.get(1002), 100, "Author1", "Moscow"); + assertAuthorLocation(booksById.get(1003), 101, "Author2", "Paris"); + assertThat(booksById.get(1003).authors.get(0).locations.get(0).countries.get(0).name, is("Southern")); + } + + @Test + public void testCannotJoinWithMerge() { + openNamespaces(); + insertFixture(); + + Query authors = db.query(AUTHORS_NS, Author.class) + .merge(db.query(AUTHORS_NS, Author.class)) + .on("authorId", EQ, "id"); + + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> db.query(BOOKS_NS, Book.class) + .innerJoin(authors, "authors") + .execute()); + + assertThat(exception.getMessage(), containsString("MERGEs nested into the JOINs are not supported")); + } + + @Test + public void testCannotUseJoinInSubquery() { + openNamespaces(); + insertFixture(); + + Query subQuery = db.query(AUTHORS_NS, Author.class) + .select("id") + .innerJoin(db.query(LOCATIONS_NS, Location.class) + .on("locationId", EQ, "id"), "locations"); + + ReindexerException exception = assertThrows(ReindexerException.class, + () -> db.query(BOOKS_NS, Book.class) + .where("authorId", SET, subQuery) + .execute()); + + assertThat(exception.getMessage(), containsString("Join cannot be in subquery")); + } + + @Test + public void testCannotUseMergeInSubquery() { + openNamespaces(); + insertFixture(); + + Query subQuery = db.query(AUTHORS_NS, Author.class) + .select("id") + .merge(db.query(AUTHORS_NS, Author.class).select("id")); + + ReindexerException exception = assertThrows(ReindexerException.class, + () -> db.query(BOOKS_NS, Book.class) + .where("authorId", SET, subQuery) + .execute()); + + assertThat(exception.getMessage(), containsString("Merge cannot be in subquery")); + } + + private void openNamespaces() { + db.openNamespace(BOOKS_NS, NamespaceOptions.defaultOptions(), Book.class); + db.openNamespace(AUTHORS_NS, NamespaceOptions.defaultOptions(), Author.class); + db.openNamespace(LOCATIONS_NS, NamespaceOptions.defaultOptions(), Location.class); + db.openNamespace(COUNTRIES_NS, NamespaceOptions.defaultOptions(), Country.class); + } + + private void insertFixture() { + db.upsert(COUNTRIES_NS, new Country(1, "Northern")); + db.upsert(COUNTRIES_NS, new Country(2, "Southern")); + db.upsert(LOCATIONS_NS, new Location(10, "Moscow", 1)); + db.upsert(LOCATIONS_NS, new Location(20, "Paris", 2)); + db.upsert(AUTHORS_NS, new Author(100, "Author1", 10)); + db.upsert(AUTHORS_NS, new Author(101, "Author2", 20)); + db.upsert(AUTHORS_NS, new Author(102, "AuthorNoLoc", 999)); + db.upsert(BOOKS_NS, new Book(1000, "Book1", 100)); + db.upsert(BOOKS_NS, new Book(1001, "Book2", 999)); + db.upsert(BOOKS_NS, new Book(1002, "OtherBook", 100)); + db.upsert(BOOKS_NS, new Book(1003, "Book3", 101)); + db.upsert(BOOKS_NS, new Book(1004, "BookNoLoc", 102)); + } + + private static void assertAuthorLocation(Book book, int expectedAuthorId, String expectedAuthorName, + String expectedCity) { + assertThat(book.authors.size(), is(1)); + Author author = book.authors.get(0); + assertThat(author.id, is(expectedAuthorId)); + assertThat(author.id, is(book.authorId)); + assertThat(author.name, is(expectedAuthorName)); + assertThat(author.locations.size(), is(1)); + assertThat(author.locations.get(0).id, is(author.locationId)); + assertThat(author.locations.get(0).city, is(expectedCity)); + } + + private static Map byId(List books) { + Map booksById = new HashMap<>(); + for (Book book : books) { + booksById.put(book.id, book); + } + return booksById; + } + + @Setter + @Getter + @NoArgsConstructor + public static class Book { + @Reindex(name = "id", isPrimaryKey = true) + private int id; + + @Reindex(name = "title") + private String title; + + @Reindex(name = "authorId") + private int authorId; + + @Transient + private List authors; + + public Book(int id, String title, int authorId) { + this.id = id; + this.title = title; + this.authorId = authorId; + } + } + + @Setter + @Getter + @NoArgsConstructor + public static class Author { + @Reindex(name = "id", isPrimaryKey = true) + private int id; + + @Reindex(name = "name") + private String name; + + @Reindex(name = "locationId") + private int locationId; + + @Transient + private List locations; + + public Author(int id, String name, int locationId) { + this.id = id; + this.name = name; + this.locationId = locationId; + } + } + + @Setter + @Getter + @NoArgsConstructor + public static class Location { + @Reindex(name = "id", isPrimaryKey = true) + private int id; + + @Reindex(name = "city") + private String city; + + @Reindex(name = "countryId") + private int countryId; + + @Transient + private List countries; + + public Location(int id, String city, int countryId) { + this.id = id; + this.city = city; + this.countryId = countryId; + } + } + + @Setter + @Getter + @NoArgsConstructor + public static class Country { + @Reindex(name = "id", isPrimaryKey = true) + private int id; + + @Reindex(name = "name") + private String name; + + public Country(int id, String name) { + this.id = id; + this.name = name; + } + } + +} From adce4e7c23c8515e60a1db91d82c1ddf36bf745c Mon Sep 17 00:00:00 2001 From: madschemas <155993105+MadSchemas@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:37:10 +0300 Subject: [PATCH 2/3] [upd] Update license --- .../rt/restream/reindexer/connector/BuiltinNestedJoinTest.java | 2 +- .../rt/restream/reindexer/connector/CprotoNestedJoinTest.java | 2 +- .../java/ru/rt/restream/reindexer/connector/NestedJoinTest.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/ru/rt/restream/reindexer/connector/BuiltinNestedJoinTest.java b/src/test/java/ru/rt/restream/reindexer/connector/BuiltinNestedJoinTest.java index ec7d2e4..33fc88f 100644 --- a/src/test/java/ru/rt/restream/reindexer/connector/BuiltinNestedJoinTest.java +++ b/src/test/java/ru/rt/restream/reindexer/connector/BuiltinNestedJoinTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Restream + * Copyright 2020-present Restream * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/ru/rt/restream/reindexer/connector/CprotoNestedJoinTest.java b/src/test/java/ru/rt/restream/reindexer/connector/CprotoNestedJoinTest.java index cdb04ab..c25cc5f 100644 --- a/src/test/java/ru/rt/restream/reindexer/connector/CprotoNestedJoinTest.java +++ b/src/test/java/ru/rt/restream/reindexer/connector/CprotoNestedJoinTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Restream + * Copyright 2020-present Restream * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/ru/rt/restream/reindexer/connector/NestedJoinTest.java b/src/test/java/ru/rt/restream/reindexer/connector/NestedJoinTest.java index 2311e04..8512c4d 100644 --- a/src/test/java/ru/rt/restream/reindexer/connector/NestedJoinTest.java +++ b/src/test/java/ru/rt/restream/reindexer/connector/NestedJoinTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Restream + * Copyright 2020-present Restream * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From c310140ed74a572666b7c9fa812805e45bdc7461 Mon Sep 17 00:00:00 2001 From: madschemas <155993105+MadSchemas@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:54:44 +0300 Subject: [PATCH 3/3] [fix] Simplify subquery serialization --- src/main/java/ru/rt/restream/reindexer/Query.java | 13 ++++++++----- .../reindexer/connector/NestedJoinTest.java | 5 ++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/ru/rt/restream/reindexer/Query.java b/src/main/java/ru/rt/restream/reindexer/Query.java index 7679d35..57e8176 100644 --- a/src/main/java/ru/rt/restream/reindexer/Query.java +++ b/src/main/java/ru/rt/restream/reindexer/Query.java @@ -1336,16 +1336,19 @@ public byte[] bytes() { } private byte[] toSubQueryBytes(int formatVersion) { + if (!joinQueries.isEmpty()) { + throw new IllegalStateException("Join cannot be in subquery"); + } + if (!mergeQueries.isEmpty()) { + throw new IllegalStateException("Merge cannot be in subquery"); + } if (formatVersion == QUERY_FORMAT_V2) { ByteBuffer queryBuffer = new ByteBuffer(getQueryBytes(formatVersion)); queryBuffer.putVarUInt32(QUERY_END); - appendJoinQueries(queryBuffer, new ArrayList<>(), formatVersion); - appendMergeQueries(queryBuffer, new ArrayList<>(), formatVersion); + queryBuffer.putVarUInt32(0); + queryBuffer.putVarUInt32(0); return queryBuffer.bytes(); } - if (!joinQueries.isEmpty() || !mergeQueries.isEmpty()) { - throw new IllegalStateException("Join and merge queries in subquery are not supported by QueryFormatV1"); - } return getQueryBytes(formatVersion); } diff --git a/src/test/java/ru/rt/restream/reindexer/connector/NestedJoinTest.java b/src/test/java/ru/rt/restream/reindexer/connector/NestedJoinTest.java index 8512c4d..5f75298 100644 --- a/src/test/java/ru/rt/restream/reindexer/connector/NestedJoinTest.java +++ b/src/test/java/ru/rt/restream/reindexer/connector/NestedJoinTest.java @@ -24,7 +24,6 @@ import ru.rt.restream.reindexer.annotations.Reindex; import ru.rt.restream.reindexer.annotations.Transient; import ru.rt.restream.reindexer.db.DbBaseTest; -import ru.rt.restream.reindexer.exceptions.ReindexerException; import java.util.HashMap; import java.util.List; @@ -196,7 +195,7 @@ public void testCannotUseJoinInSubquery() { .innerJoin(db.query(LOCATIONS_NS, Location.class) .on("locationId", EQ, "id"), "locations"); - ReindexerException exception = assertThrows(ReindexerException.class, + IllegalStateException exception = assertThrows(IllegalStateException.class, () -> db.query(BOOKS_NS, Book.class) .where("authorId", SET, subQuery) .execute()); @@ -213,7 +212,7 @@ public void testCannotUseMergeInSubquery() { .select("id") .merge(db.query(AUTHORS_NS, Author.class).select("id")); - ReindexerException exception = assertThrows(ReindexerException.class, + IllegalStateException exception = assertThrows(IllegalStateException.class, () -> db.query(BOOKS_NS, Book.class) .where("authorId", SET, subQuery) .execute());