From 5d71e9c3840590d11eb17578f9a79c6895ad4e2a Mon Sep 17 00:00:00 2001 From: Kirill Kurdyukov Date: Mon, 27 Jul 2026 13:20:05 +0300 Subject: [PATCH 1/2] feat(hibernate): table/column-aware use_index query hint Extend use_index with use_index::([,...]) so the same table joined multiple times can be pinned to different secondary indexes via VIEW. Match by alias columns in ON/WHERE; most specific full match wins. --- hibernate-dialect/CHANGELOG.md | 3 +- .../dialect/hint/IndexHintApplier.java | 363 ++++++++++++++++++ .../dialect/hint/IndexTypedHint.java | 13 + .../hibernate/dialect/hint/QueryHints.java | 48 ++- .../hint/IndexQueryHintHandlerTest.java | 344 +++++++++++++++++ .../tech/ydb/hibernate/index/BankAccount.java | 70 ++++ .../ydb/hibernate/index/BankDocument.java | 76 ++++ .../index/BankDocumentIndexHintTest.java | 182 +++++++++ .../student/StudentsRepositoryTest.java | 119 ++++++ .../ydb/hibernate/student/entity/Student.java | 3 +- 10 files changed, 1210 insertions(+), 11 deletions(-) create mode 100644 hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/hint/IndexHintApplier.java create mode 100644 hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/hint/IndexTypedHint.java create mode 100644 hibernate-dialect/src/test/java/tech/ydb/hibernate/hint/IndexQueryHintHandlerTest.java create mode 100644 hibernate-dialect/src/test/java/tech/ydb/hibernate/index/BankAccount.java create mode 100644 hibernate-dialect/src/test/java/tech/ydb/hibernate/index/BankDocument.java create mode 100644 hibernate-dialect/src/test/java/tech/ydb/hibernate/index/BankDocumentIndexHintTest.java diff --git a/hibernate-dialect/CHANGELOG.md b/hibernate-dialect/CHANGELOG.md index f0305739..87eae7a0 100644 --- a/hibernate-dialect/CHANGELOG.md +++ b/hibernate-dialect/CHANGELOG.md @@ -1,6 +1,7 @@ ## 1.7.0 ## -- Fixed `HINT_COMMENT` handling: hints separated by `;` are now passed to each handler as a single batch, so multiple short-form `use_index` hints become `view a, b` instead of `view a view b` +- Fixed `HINT_COMMENT` handling: multiple short-form `use_index` hints in one comment become `view a, b` instead of `view a view b` +- Extended `use_index:` hint with table-and-column-aware format `use_index::([,...])` to pin a secondary index per `FROM`/`JOIN` when the same table is joined more than once in one query - Renamed `hibernate-dialect-v6` to `hibernate-dialect`, removed `hibernate-dialect-v7` - Support Hibernate 7 - Support Java 11 for Hibernate 6 profile diff --git a/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/hint/IndexHintApplier.java b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/hint/IndexHintApplier.java new file mode 100644 index 00000000..b2fa38d9 --- /dev/null +++ b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/hint/IndexHintApplier.java @@ -0,0 +1,363 @@ +package tech.ydb.hibernate.dialect.hint; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +class IndexHintApplier { + private final char[] sql; + private final Map> typedHints; + private final List shortIndexes; + private final List joinViews = new ArrayList<>(); + + private int pos; + private int parenLevel; + private TableRef fromTable; + private View fromView; + private boolean whereSeen; + + private IndexHintApplier(String query, Map> typedHints, List shortIndexes) { + this.sql = query.toCharArray(); + this.typedHints = typedHints; + this.shortIndexes = shortIndexes; + } + + static String apply(String query, Map> typedHints, List shortIndexes) { + return new IndexHintApplier(query, typedHints, shortIndexes).apply(); + } + + private String apply() { + while (pos < sql.length) { + if (parenLevel != 0 || !isIdentifierStart(sql[pos])) { + consumeChar(); + continue; + } + + String keyword = readIdentifier(); + if (!isClauseKeyword(keyword)) { + continue; + } + + if (keyword.equalsIgnoreCase("from")) { + fromTable = parseTableRef(); + } else if (keyword.equalsIgnoreCase("join")) { + TableRef joinTable = parseTableRef(); + if (joinTable != null) { + String index = bestIndex(joinTable.table, collectAliasColumnsUntilNextClause(joinTable.alias)); + if (index != null) { + joinViews.add(view(joinTable, index)); + } + } + } else if (keyword.equalsIgnoreCase("where")) { + whereSeen = true; + if (fromTable != null) { + String index = bestIndex(fromTable.table, collectAliasColumnsUntilNextClause(fromTable.alias)); + if (index != null) { + fromView = view(fromTable, index); + } + } + } + } + + applyShortIndexes(); + return render(); + } + + private void applyShortIndexes() { + if (fromView != null || fromTable == null || shortIndexes.isEmpty() || !whereSeen) { + return; + } + fromView = new View(fromTable.afterTable, fromTable.aliasStart, + " view " + String.join(", ", shortIndexes) + " "); + } + + private String render() { + if (fromView == null && joinViews.isEmpty()) { + return new String(sql); + } + + StringBuilder out = new StringBuilder(sql.length + 32); + int copied = 0; + + if (fromView != null) { + out.append(sql, copied, fromView.start - copied); + out.append(fromView.text); + copied = fromView.end; + } + for (View joinView : joinViews) { + out.append(sql, copied, joinView.start - copied); + out.append(joinView.text); + copied = joinView.end; + } + out.append(sql, copied, sql.length - copied); + return out.toString(); + } + + private static View view(TableRef table, String index) { + return new View(table.afterTable, table.aliasStart, " view " + index + " "); + } + + private TableRef parseTableRef() { + skipWhitespace(); + String table = readIdentifier(); + if (table == null) { + return null; + } + + int afterTable = pos; + skipWhitespace(); + String next = peekIdentifier(); + if (next == null || next.equalsIgnoreCase("view")) { + return null; + } + + int aliasStart = pos; + String alias = readIdentifier(); + if (alias != null && alias.equalsIgnoreCase("as")) { + skipWhitespace(); + alias = readIdentifier(); + } + if (alias == null) { + return null; + } + return new TableRef(table, alias, afterTable, aliasStart); + } + + private Set collectAliasColumnsUntilNextClause(String alias) { + Set columns = new HashSet<>(); + while (pos < sql.length) { + if (parenLevel == 0 && startsClauseKeyword()) { + break; + } + if (readAliasColumnInto(alias, columns)) { + continue; + } + consumeChar(); + } + return columns; + } + + private boolean readAliasColumnInto(String alias, Set columns) { + if (pos >= sql.length || !isIdentifierStart(sql[pos])) { + return false; + } + + int saved = pos; + String ident = readIdentifier(); + if (ident == null) { + pos = saved; + return false; + } + if (parenLevel == 0 && isClauseKeyword(ident)) { + pos = saved; + return false; + } + + if (ident.equals(alias) && pos < sql.length && sql[pos] == '.') { + pos++; + skipWhitespace(); + String column = readIdentifier(); + if (column != null) { + columns.add(column); + } + } + return true; + } + + private boolean startsClauseKeyword() { + if (pos >= sql.length || !isIdentifierStart(sql[pos])) { + return false; + } + int saved = pos; + boolean clause = isClauseKeyword(readIdentifier()); + pos = saved; + return clause; + } + + private String bestIndex(String table, Set referencedColumns) { + List hints = typedHints.get(table); + if (hints == null) { + return null; + } + IndexTypedHint best = null; + for (IndexTypedHint hint : hints) { + if (!referencedColumns.containsAll(hint.columns)) { + continue; + } + if (best == null || hint.columns.size() > best.columns.size()) { + best = hint; + } + } + return best == null ? null : best.indexName; + } + + private String readIdentifier() { + if (pos >= sql.length || !isIdentifierStart(sql[pos])) { + return null; + } + int start = pos++; + while (pos < sql.length && isIdentifierPart(sql[pos])) { + pos++; + } + return new String(sql, start, pos - start); + } + + private String peekIdentifier() { + int saved = pos; + String ident = readIdentifier(); + pos = saved; + return ident; + } + + private void skipWhitespace() { + while (pos < sql.length && Character.isWhitespace(sql[pos])) { + pos++; + } + } + + private void consumeChar() { + switch (sql[pos]) { + case '\'': + pos = skipSingleQuotes(pos); + break; + case '"': + pos = skipDoubleQuotes(pos); + break; + case '`': + pos = skipBacktickQuotes(pos); + break; + case '-': + pos = skipLineComment(pos); + break; + case '/': + pos = skipBlockComment(pos); + break; + case '(': + parenLevel++; + pos++; + break; + case ')': + if (parenLevel > 0) { + parenLevel--; + } + pos++; + break; + default: + pos++; + break; + } + } + + private static boolean isClauseKeyword(String ident) { + if (ident == null) { + return false; + } + + switch (ident.length()) { + case 4: + return ident.equalsIgnoreCase("from") || ident.equalsIgnoreCase("join"); + case 5: + return ident.equalsIgnoreCase("where") + || ident.equalsIgnoreCase("order") + || ident.equalsIgnoreCase("group") + || ident.equalsIgnoreCase("limit") + || ident.equalsIgnoreCase("union"); + case 6: + return ident.equalsIgnoreCase("having") || ident.equalsIgnoreCase("except"); + case 9: + return ident.equalsIgnoreCase("intersect"); + default: + return false; + } + } + + private static boolean isIdentifierStart(char ch) { + return Character.isJavaIdentifierStart(ch); + } + + private static boolean isIdentifierPart(char ch) { + return Character.isJavaIdentifierPart(ch); + } + + private int skipSingleQuotes(int offset) { + while (++offset < sql.length) { + if (sql[offset] == '\\') { + ++offset; + } else if (sql[offset] == '\'') { + return offset + 1; + } + } + return sql.length; + } + + private int skipDoubleQuotes(int offset) { + while (++offset < sql.length && sql[offset] != '"') { + } + return Math.min(offset + 1, sql.length); + } + + private int skipBacktickQuotes(int offset) { + while (++offset < sql.length && sql[offset] != '`') { + } + return Math.min(offset + 1, sql.length); + } + + private int skipLineComment(int offset) { + if (offset + 1 < sql.length && sql[offset + 1] == '-') { + offset += 2; + while (offset < sql.length && sql[offset] != '\r' && sql[offset] != '\n') { + offset++; + } + return offset; + } + return offset + 1; + } + + private int skipBlockComment(int offset) { + if (offset + 1 >= sql.length || sql[offset + 1] != '*') { + return offset + 1; + } + int level = 1; + for (offset += 2; offset < sql.length; ++offset) { + if (sql[offset - 1] == '*' && sql[offset] == '/') { + --level; + ++offset; + } else if (sql[offset - 1] == '/' && sql[offset] == '*') { + ++level; + ++offset; + } + if (level == 0) { + return offset; + } + } + return sql.length; + } + + private static final class TableRef { + final String table; + final String alias; + final int afterTable; + final int aliasStart; + + TableRef(String table, String alias, int afterTable, int aliasStart) { + this.table = table; + this.alias = alias; + this.afterTable = afterTable; + this.aliasStart = aliasStart; + } + } + + private static final class View { + final int start; + final int end; + final String text; + + View(int start, int end, String text) { + this.start = start; + this.end = end; + this.text = text; + } + } +} diff --git a/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/hint/IndexTypedHint.java b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/hint/IndexTypedHint.java new file mode 100644 index 00000000..2b7c0fe5 --- /dev/null +++ b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/hint/IndexTypedHint.java @@ -0,0 +1,13 @@ +package tech.ydb.hibernate.dialect.hint; + +import java.util.List; + +class IndexTypedHint { + final String indexName; + final List columns; + + IndexTypedHint(String indexName, List columns) { + this.indexName = indexName; + this.columns = columns; + } +} diff --git a/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/hint/QueryHints.java b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/hint/QueryHints.java index c1f436eb..c44ba701 100644 --- a/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/hint/QueryHints.java +++ b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/hint/QueryHints.java @@ -1,25 +1,55 @@ package tech.ydb.hibernate.dialect.hint; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.util.Map; /** * @author Kirill Kurdyukov */ public final class QueryHints { - private static final Pattern SELECT_FROM_WHERE = Pattern - .compile("^\\s*(select.+?from\\s+\\w+)(.+where.+)$", Pattern.CASE_INSENSITIVE); - private QueryHints() { } - public static String addViewIndexesToQuery(String query, List indexNames) { - Matcher matcher = SELECT_FROM_WHERE.matcher(query); - if (!matcher.matches() || matcher.groupCount() < 2) { + public static String addViewIndexesToQuery(String query, List indexHints) { + List shortIndexes = new ArrayList<>(); + Map> typedHints = new LinkedHashMap<>(); + + for (String body : indexHints) { + int tableColon = body.indexOf(':'); + int columnsStart = body.indexOf('(', tableColon + 1); + int columnsEnd = body.lastIndexOf(')'); + + if (tableColon > 0 && columnsStart > tableColon && columnsEnd > columnsStart) { + String indexName = body.substring(0, tableColon).trim(); + String tableName = body.substring(tableColon + 1, columnsStart).trim(); + List columns = splitColumns(body.substring(columnsStart + 1, columnsEnd)); + if (!indexName.isEmpty() && !tableName.isEmpty() && !columns.isEmpty()) { + List hintsForTable = typedHints + .computeIfAbsent(tableName, k -> new ArrayList<>()); + hintsForTable.add(new IndexTypedHint(indexName, columns)); + continue; + } + } + shortIndexes.add(body); + } + + if (typedHints.isEmpty() && shortIndexes.isEmpty()) { return query; } - return matcher.group(1) + " view " + String.join(", ", indexNames) + matcher.group(2); + return IndexHintApplier.apply(query, typedHints, shortIndexes); + } + + private static List splitColumns(String raw) { + List columns = new ArrayList<>(); + for (String part : raw.split(",")) { + String column = part.trim(); + if (!column.isEmpty()) { + columns.add(column); + } + } + return columns; } public static String addScanToQuery(String query) { diff --git a/hibernate-dialect/src/test/java/tech/ydb/hibernate/hint/IndexQueryHintHandlerTest.java b/hibernate-dialect/src/test/java/tech/ydb/hibernate/hint/IndexQueryHintHandlerTest.java new file mode 100644 index 00000000..a9a6c461 --- /dev/null +++ b/hibernate-dialect/src/test/java/tech/ydb/hibernate/hint/IndexQueryHintHandlerTest.java @@ -0,0 +1,344 @@ +package tech.ydb.hibernate.hint; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import tech.ydb.hibernate.dialect.hint.QueryHints; + +public class IndexQueryHintHandlerTest { + + private static String apply(String query, String... hints) { + java.util.List bodies = new java.util.ArrayList<>(); + for (String hint : hints) { + if (hint.startsWith("use_index:")) { + bodies.add(hint.substring("use_index:".length())); + } + } + return QueryHints.addViewIndexesToQuery(query, bodies); + } + + + @Nested + class ShortForm { + + @Test + void rewritesFromTable() { + String query = "select g1_0.GroupId from Groups g1_0 where g1_0.GroupName='M3439'"; + String result = apply(query, "use_index:group_name_index"); + + assertEquals( + "select g1_0.GroupId from Groups view group_name_index g1_0 where g1_0.GroupName='M3439'", + result + ); + } + + @Test + void multipleShortHintsAreJoinedWithComma() { + String query = "select g1_0.GroupId from Groups g1_0 where g1_0.GroupName='M3439'"; + String result = apply(query, "use_index:idx_one", "use_index:idx_two"); + + assertEquals( + "select g1_0.GroupId from Groups view idx_one, idx_two g1_0 where g1_0.GroupName='M3439'", + result + ); + } + + @Test + void noWhereClauseLeavesQueryAlone() { + String query = "select g1_0.GroupId from Groups g1_0"; + String result = apply(query, "use_index:group_name_index"); + + assertEquals(query, result); + } + + @Test + void hintsAreIgnoredWhenPrefixDoesNotMatch() { + String query = "select g1_0.GroupId from Groups g1_0 where g1_0.GroupName='M3439'"; + String result = apply(query, "use_scan", "add_pragma:Foo"); + + assertEquals(query, result); + } + } + + @Nested + class TypedFormSingleColumn { + + @Test + void rewritesSingleJoin() { + String query = "select * from orders o1_0 " + + "left join customers c1_0 on c1_0.code=o1_0.acc_dt_code " + + "where o1_0.id='X'"; + String result = apply(query, "use_index:customers_code_idx:customers(code)"); + + assertEquals( + "select * from orders o1_0 " + + "left join customers view customers_code_idx c1_0 on c1_0.code=o1_0.acc_dt_code " + + "where o1_0.id='X'", + result + ); + } + + @Test + void rewritesTwoJoinsOfSameTableOnDifferentColumns() { + String query = "select * from orders o1_0 " + + "left join customers c1_0 on c1_0.code=source.code " + + "left join customers c3_0 on c3_0.parent=source.parent " + + "where o1_0.id='X'"; + String result = apply(query, "use_index:customers_code_idx:customers(code)", "use_index:customers_parent_idx:customers(parent)"); + + assertEquals( + "select * from orders o1_0 " + + "left join customers view customers_code_idx c1_0 on c1_0.code=source.code " + + "left join customers view customers_parent_idx c3_0 on c3_0.parent=source.parent " + + "where o1_0.id='X'", + result + ); + } + + @Test + void skipsJoinsWhereColumnIsAbsent() { + String query = "select * from orders o1_0 " + + "left join customers c1_0 on c1_0.id=o1_0.acc_dt_id " + + "where o1_0.id='X'"; + String result = apply(query, "use_index:customers_code_idx:customers(code)"); + + assertEquals(query, result); + } + + @Test + void leavesUnrelatedTablesUntouched() { + String query = "select * from orders o1_0 " + + "left join customers c1_0 on c1_0.code=source.code " + + "left join regions r1_0 on r1_0.code=source.code " + + "where o1_0.id='X'"; + String result = apply(query, "use_index:customers_code_idx:customers(code)"); + + assertEquals( + "select * from orders o1_0 " + + "left join customers view customers_code_idx c1_0 on c1_0.code=source.code " + + "left join regions r1_0 on r1_0.code=source.code " + + "where o1_0.id='X'", + result + ); + } + + @Test + void rewritesFromTableWhenColumnAppearsInWhere() { + String query = "select g1_0.GroupId from Groups g1_0 where g1_0.GroupName='M3439'"; + String result = apply(query, "use_index:group_name_index:Groups(GroupName)"); + + assertEquals( + "select g1_0.GroupId from Groups view group_name_index g1_0 where g1_0.GroupName='M3439'", + result + ); + } + + @Test + void leavesFromTableAloneWhenColumnNotInWhere() { + String query = "select g1_0.GroupId from Groups g1_0 where g1_0.GroupId=1"; + String result = apply(query, "use_index:group_name_index:Groups(GroupName)"); + + assertEquals(query, result); + } + } + + @Nested + class TypedFormCompositeColumns { + + + @Test + void fullColumnMatch_appliesIndex() { + String query = "select * from orders o1_0 " + + "left join customers c1_0 on c1_0.code=source.code and c1_0.parent=source.parent " + + "where o1_0.id='X'"; + String result = apply(query, "use_index:customers_combo_idx:customers(code,parent)"); + + assertEquals( + "select * from orders o1_0 " + + "left join customers view customers_combo_idx c1_0 on c1_0.code=source.code and c1_0.parent=source.parent " + + "where o1_0.id='X'", + result + ); + } + + + @Test + void partialColumnMatch_doesNotApply() { + String query = "select * from orders o1_0 " + + "left join customers c1_0 on c1_0.code=source.code " + + "left join customers c3_0 on c3_0.parent=source.parent " + + "where o1_0.id='X'"; + String result = apply(query, "use_index:customers_combo_idx:customers(code,parent)"); + + assertEquals(query, result); + } + + + @Test + void bestMatchWins_mostSpecificIndexPicked() { + String query = "select * from orders o1_0 " + + "left join customers c1_0 on c1_0.code=source.code and c1_0.parent=source.parent " + + "where o1_0.id='X'"; + String result = apply(query, "use_index:customers_code_idx:customers(code)", "use_index:customers_combo_idx:customers(code,parent)"); + + assertEquals( + "select * from orders o1_0 " + + "left join customers view customers_combo_idx c1_0 on c1_0.code=source.code and c1_0.parent=source.parent " + + "where o1_0.id='X'", + result + ); + } + + + @Test + void partialCompositeFallsBackToFullSingleColumn() { + String query = "select * from orders o1_0 " + + "left join customers c1_0 on c1_0.code=source.code " + + "where o1_0.id='X'"; + String result = apply(query, "use_index:customers_combo_idx:customers(code,parent)", "use_index:customers_code_idx:customers(code)"); + + assertEquals( + "select * from orders o1_0 " + + "left join customers view customers_code_idx c1_0 on c1_0.code=source.code " + + "where o1_0.id='X'", + result + ); + } + } + + @Nested + class IdentifierBoundaries { + + + @Test + void columnNameIsNotASubstringMatch() { + String query = "select * from orders o1_0 " + + "left join customers c1_0 on c1_0.acc_dt_code=source.x " + + "left join customers c2_0 on c2_0.code1=source.y " + + "where o1_0.id='X'"; + String result = apply(query, "use_index:customers_code_idx:customers(code)"); + + assertEquals(query, result); + } + + + @Test + void aliasIsNotASubstringMatch() { + String query = "select * from orders o1_0 " + + "left join customers c1_0 on xo1_0.code=source.code " + + "where o1_0.id='X'"; + String result = apply(query, "use_index:customers_code_idx:customers(code)"); + + assertEquals(query, result); + } + } + + @Nested + class Interactions { + + @Test + void shortFormSkippedWhenTypedAlreadyAddedView() { + String query = "select g1_0.GroupId from Groups g1_0 where g1_0.GroupName='M3439'"; + String result = apply(query, "use_index:group_name_index:Groups(GroupName)", "use_index:other_index"); + + assertEquals( + "select g1_0.GroupId from Groups view group_name_index g1_0 where g1_0.GroupName='M3439'", + result + ); + } + + @Test + void largeRealisticQueryWithMultipleAliasesOfSameTable() { + String query = "select o1_0.id,c1_0.id,c3_0.id,r1_0.id " + + "from orders o1_0 " + + "left join customers c1_0 on c1_0.id=o1_0.acc_dt_id " + + "left join customers c3_0 on c3_0.id=o1_0.acc_kt_id " + + "left join regions r1_0 on r1_0.id=o1_0.branch_id " + + "where o1_0.id='abc'"; + String result = apply(query, "use_index:customers_id_idx:customers(id)", "use_index:regions_id_idx:regions(id)"); + + assertEquals( + "select o1_0.id,c1_0.id,c3_0.id,r1_0.id " + + "from orders o1_0 " + + "left join customers view customers_id_idx c1_0 on c1_0.id=o1_0.acc_dt_id " + + "left join customers view customers_id_idx c3_0 on c3_0.id=o1_0.acc_kt_id " + + "left join regions view regions_id_idx r1_0 on r1_0.id=o1_0.branch_id " + + "where o1_0.id='abc'", + result + ); + } + + @Test + void malformedTypedHintIsIgnored() { + String query = "select g1_0.GroupId from Groups g1_0 where g1_0.GroupName='M3439'"; + String result = apply(query, "use_index:group_name_index:Groups(GroupName"); + + assertTrue(result.contains("from Groups")); + } + } + + + + @Nested + class TopLevelClauseBoundaries { + + @Test + void joinKeywordInsideStringLiteral_doesNotCreateSpuriousJoin() { + String query = "select * from orders o1_0 " + + "left join customers c1_0 on c1_0.code=o1_0.x " + + "where o1_0.note='text with left join customers fake on fake.code=x'"; + String result = apply(query, "use_index:customers_code_idx:customers(code)"); + + assertEquals( + "select * from orders o1_0 " + + "left join customers view customers_code_idx c1_0 on c1_0.code=o1_0.x " + + "where o1_0.note='text with left join customers fake on fake.code=x'", + result + ); + } + + @Test + void fromInsideSubquery_isNotRewritten() { + String query = "select * from orders o1_0 " + + "left join (select x.id from customers x where x.parent='p') c1_0 on c1_0.id=o1_0.id " + + "left join customers c2_0 on c2_0.code=o1_0.code " + + "where o1_0.id='X'"; + String result = apply(query, "use_index:customers_code_idx:customers(code)", "use_index:customers_parent_idx:customers(parent)"); + + assertTrue(result.contains("left join customers view customers_code_idx c2_0 on c2_0.code")); + assertFalse(result.contains("customers view customers_code_idx c1_0")); + assertFalse(result.contains("customers view customers_parent_idx")); + assertFalse(result.contains("from customers view")); + } + + @Test + void joinInsideBlockComment_doesNotSplitSegments() { + String query = "select * from orders o1_0 /* join decoy */ " + + "left join customers c1_0 on c1_0.code=o1_0.x where o1_0.id='X'"; + String result = apply(query, "use_index:customers_code_idx:customers(code)"); + + assertEquals( + "select * from orders o1_0 /* join decoy */ " + + "left join customers view customers_code_idx c1_0 on c1_0.code=o1_0.x where o1_0.id='X'", + result + ); + } + + @Test + void viewIsInsertedBeforeAsAlias() { + String query = "select a1.b as a1_b, a2.b as a2_b " + + "from a as a1 join a as a2 on a1.d=a2.c where a1.d=1"; + String result = apply(query, + "use_index:Index1:a(d)", + "use_index:Index2:a(c)"); + + assertEquals( + "select a1.b as a1_b, a2.b as a2_b " + + "from a view Index1 as a1 join a view Index2 as a2 on a1.d=a2.c where a1.d=1", + result + ); + } + } +} diff --git a/hibernate-dialect/src/test/java/tech/ydb/hibernate/index/BankAccount.java b/hibernate-dialect/src/test/java/tech/ydb/hibernate/index/BankAccount.java new file mode 100644 index 00000000..8437d8a6 --- /dev/null +++ b/hibernate-dialect/src/test/java/tech/ydb/hibernate/index/BankAccount.java @@ -0,0 +1,70 @@ +package tech.ydb.hibernate.index; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; + +/** + * Account table that is joined several times under different aliases and different columns, + * reproducing the real "one table, many joins" scenario the index hints are built for. + *

+ * Three secondary indexes let a test pin a different {@code VIEW} per join. + */ +@Entity +@Table( + name = "bank_account", + indexes = { + @Index(name = "bank_account_code_idx", columnList = "code"), + @Index(name = "bank_account_parent_idx", columnList = "parent"), + @Index(name = "bank_account_combo_idx", columnList = "code, parent") + } +) +public class BankAccount { + + @Id + @Column(name = "id") + private int id; + + @Column(name = "code") + private String code; + + @Column(name = "parent") + private String parent; + + @Column(name = "name") + private String name; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getParent() { + return parent; + } + + public void setParent(String parent) { + this.parent = parent; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-dialect/src/test/java/tech/ydb/hibernate/index/BankDocument.java b/hibernate-dialect/src/test/java/tech/ydb/hibernate/index/BankDocument.java new file mode 100644 index 00000000..8853a4a6 --- /dev/null +++ b/hibernate-dialect/src/test/java/tech/ydb/hibernate/index/BankDocument.java @@ -0,0 +1,76 @@ +package tech.ydb.hibernate.index; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +/** + * A document that references {@link BankAccount} three times, each association joining the + * same table on different column(s). Hibernate generates joins such as: + *

+ *   join bank_account a1_0 on a1_0.code=d1_0.acc_dt_code                              (accDt)
+ *   join bank_account a2_0 on a2_0.parent=d1_0.acc_kt_parent                          (accKt)
+ *   join bank_account a3_0 on a3_0.code=d1_0.acc_combo_code and a3_0.parent=...        (accCombo)
+ * 
+ * which is exactly what the column-aware {@code use_index} hint needs to distinguish. + */ +@Entity +@Table(name = "bank_document") +public class BankDocument { + + @Id + @Column(name = "id") + private int id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "acc_dt_code", referencedColumnName = "code") + private BankAccount accDt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "acc_kt_parent", referencedColumnName = "parent") + private BankAccount accKt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumns({ + @JoinColumn(name = "acc_combo_code", referencedColumnName = "code"), + @JoinColumn(name = "acc_combo_parent", referencedColumnName = "parent") + }) + private BankAccount accCombo; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public BankAccount getAccDt() { + return accDt; + } + + public void setAccDt(BankAccount accDt) { + this.accDt = accDt; + } + + public BankAccount getAccKt() { + return accKt; + } + + public void setAccKt(BankAccount accKt) { + this.accKt = accKt; + } + + public BankAccount getAccCombo() { + return accCombo; + } + + public void setAccCombo(BankAccount accCombo) { + this.accCombo = accCombo; + } +} diff --git a/hibernate-dialect/src/test/java/tech/ydb/hibernate/index/BankDocumentIndexHintTest.java b/hibernate-dialect/src/test/java/tech/ydb/hibernate/index/BankDocumentIndexHintTest.java new file mode 100644 index 00000000..499fcb69 --- /dev/null +++ b/hibernate-dialect/src/test/java/tech/ydb/hibernate/index/BankDocumentIndexHintTest.java @@ -0,0 +1,182 @@ +package tech.ydb.hibernate.index; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +import org.hibernate.SessionFactory; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.jpa.HibernateHints; +import org.hibernate.query.Query; +import org.hibernate.resource.jdbc.spi.StatementInspector; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import tech.ydb.hibernate.TestUtils; +import tech.ydb.test.junit5.YdbHelperExtension; + +/** + * @author Kirill Kurdyukov + */ +public class BankDocumentIndexHintTest { + + @RegisterExtension + private static final YdbHelperExtension ydb = new YdbHelperExtension(); + + private static SessionFactory sessionFactory; + + @BeforeAll + static void beforeAll() { + Configuration configuration = TestUtils.basedConfiguration() + .setProperty(AvailableSettings.URL, TestUtils.jdbcUrl(ydb)) + .setProperty(AvailableSettings.STATEMENT_INSPECTOR, CapturingStatementInspector.class.getName()) + .addAnnotatedClass(BankAccount.class) + .addAnnotatedClass(BankDocument.class); + + sessionFactory = configuration.buildSessionFactory(); + + sessionFactory.inTransaction(session -> { + BankAccount a1 = newAccount(1, "ACC-CODE-1", "PARENT-1", "first"); + BankAccount a2 = newAccount(2, "ACC-CODE-2", "PARENT-2", "second"); + session.persist(a1); + session.persist(a2); + + BankDocument document = new BankDocument(); + document.setId(1); + document.setAccDt(a1); + document.setAccKt(a2); + document.setAccCombo(a1); + session.persist(document); + }); + } + + @BeforeEach + void clearCapturedSql() { + CapturingStatementInspector.clear(); + } + + + @Test + void sameTableJoinedTwice_byDifferentColumns_getsTwoDifferentIndexes() { + String joins = "join fetch d.accDt join fetch d.accKt"; + String hintCode = "use_index:bank_account_code_idx:bank_account(code)"; + String hintParent = "use_index:bank_account_parent_idx:bank_account(parent)"; + + runDocumentQuery(joins, query -> query.addQueryHint(hintCode).addQueryHint(hintParent)); + assertDocumentSql(sql -> { + assertTrue(sql.contains("bank_account view bank_account_code_idx"), + () -> "expected code index on the code join, got:\n" + sql); + assertTrue(sql.contains("bank_account view bank_account_parent_idx"), + () -> "expected parent index on the parent join, got:\n" + sql); + }); + + clearCapturedSql(); + runDocumentQuery(joins, query -> query.setHint(HibernateHints.HINT_COMMENT, hintCode + ";" + hintParent)); + assertDocumentSql(sql -> { + assertTrue(sql.contains("bank_account view bank_account_code_idx"), + () -> "expected code index via HINT_COMMENT, got:\n" + sql); + assertTrue(sql.contains("bank_account view bank_account_parent_idx"), + () -> "expected parent index via HINT_COMMENT, got:\n" + sql); + }); + } + + + @Test + void compositeJoin_picksIndexWithMostColumnMatches() { + String joins = "join fetch d.accCombo"; + String hintCode = "use_index:bank_account_code_idx:bank_account(code)"; + String hintCombo = "use_index:bank_account_combo_idx:bank_account(code,parent)"; + + runDocumentQuery(joins, query -> query.addQueryHint(hintCode).addQueryHint(hintCombo)); + assertDocumentSql(sql -> { + assertTrue(sql.contains("bank_account view bank_account_combo_idx"), + () -> "expected the composite index to win, got:\n" + sql); + assertFalse(sql.contains("bank_account view bank_account_code_idx"), + () -> "single-column index must not win over the composite one, got:\n" + sql); + }); + + clearCapturedSql(); + runDocumentQuery(joins, query -> query.setHint(HibernateHints.HINT_COMMENT, hintCode + ";" + hintCombo)); + assertDocumentSql(sql -> { + assertTrue(sql.contains("bank_account view bank_account_combo_idx"), + () -> "expected the composite index to win via HINT_COMMENT, got:\n" + sql); + assertFalse(sql.contains("bank_account view bank_account_code_idx"), + () -> "single-column index must not win via HINT_COMMENT, got:\n" + sql); + }); + } + + + @Test + void compositeHint_withOneColumnMissing_isNotApplied() { + String joins = "join fetch d.accDt"; + String hintCombo = "use_index:bank_account_combo_idx:bank_account(code,parent)"; + + runDocumentQuery(joins, query -> query.addQueryHint(hintCombo)); + assertDocumentSql(sql -> assertFalse(sql.contains("bank_account view"), + () -> "partial composite hint must not inject a view, got:\n" + sql)); + + clearCapturedSql(); + runDocumentQuery(joins, query -> query.setHint(HibernateHints.HINT_COMMENT, hintCombo)); + assertDocumentSql(sql -> assertFalse(sql.contains("bank_account view"), + () -> "partial composite hint must not inject a view via HINT_COMMENT, got:\n" + sql)); + } + + private void runDocumentQuery(String joinClause, Consumer> configure) { + sessionFactory.inTransaction(session -> { + Query query = session.createQuery( + "select d from BankDocument d " + joinClause + " where d.id = 1", + BankDocument.class + ); + configure.accept(query); + BankDocument document = query.getSingleResult(); + assertEquals(1, document.getId()); + }); + } + + private void assertDocumentSql(Consumer assertion) { + Optional sql = CapturingStatementInspector.lastSqlContaining("from bank_document"); + assertTrue(sql.isPresent(), "no SELECT against bank_document was captured"); + assertion.accept(sql.get()); + } + + private static BankAccount newAccount(int id, String code, String parent, String name) { + BankAccount account = new BankAccount(); + account.setId(id); + account.setCode(code); + account.setParent(parent); + account.setName(name); + return account; + } + + + public static final class CapturingStatementInspector implements StatementInspector { + + private static final List CAPTURED = new ArrayList<>(); + + public static synchronized void clear() { + CAPTURED.clear(); + } + + static synchronized Optional lastSqlContaining(String needle) { + for (int i = CAPTURED.size() - 1; i >= 0; i--) { + if (CAPTURED.get(i).contains(needle)) { + return Optional.of(CAPTURED.get(i)); + } + } + return Optional.empty(); + } + + @Override + public synchronized String inspect(String sql) { + CAPTURED.add(sql); + return sql; + } + } +} diff --git a/hibernate-dialect/src/test/java/tech/ydb/hibernate/student/StudentsRepositoryTest.java b/hibernate-dialect/src/test/java/tech/ydb/hibernate/student/StudentsRepositoryTest.java index c5524280..3312af3a 100644 --- a/hibernate-dialect/src/test/java/tech/ydb/hibernate/student/StudentsRepositoryTest.java +++ b/hibernate-dialect/src/test/java/tech/ydb/hibernate/student/StudentsRepositoryTest.java @@ -205,6 +205,125 @@ void groupByGroupName_ViewIndex() { ); } + @Test + void studentsByGroupName_TypedViewIndex_JoinTable() { + /* + select + g1_0.GroupId, + g1_0.GroupName, + s1_0.GroupId, + s1_0.StudentId, + s1_0.StudentName + from + Groups view group_name_index g1_0 + join + Students view students_group_index s1_0 + on g1_0.GroupId=s1_0.GroupId + where + g1_0.GroupName='M3439' + */ + inTransaction( + session -> { + List students = session + .createQuery("FROM Group g JOIN FETCH g.students WHERE g.name = 'M3439'", Group.class) + .addQueryHint("use_index:group_name_index:Groups(GroupName)") + .addQueryHint("use_index:students_group_index:Students(GroupId)") + .getSingleResult().getStudents(); + + assertEquals(2, students.size()); + assertEquals("Петров П.П.", students.get(0).getName()); + assertEquals("Сидоров С.С.", students.get(1).getName()); + } + ); + + inTransaction( + session -> { + List students = session + .createQuery("FROM Group g JOIN FETCH g.students WHERE g.name = 'M3439'", Group.class) + .setHint( + HibernateHints.HINT_COMMENT, + "use_index:group_name_index:Groups(GroupName);" + + "use_index:students_group_index:Students(GroupId)" + ) + .getSingleResult().getStudents(); + + assertEquals(2, students.size()); + assertEquals("Петров П.П.", students.get(0).getName()); + assertEquals("Сидоров С.С.", students.get(1).getName()); + } + ); + } + + @Test + void typedViewIndex_columnDoesNotMatch_queryStillWorks() { + // Hint targets Students.StudentName, but the JOIN's ON clause references only GroupId. + // The handler must NOT inject a view in this case; the query should still execute. + inTransaction( + session -> { + List students = session + .createQuery("FROM Group g JOIN FETCH g.students WHERE g.name = 'M3439'", Group.class) + .addQueryHint("use_index:students_group_index:Students(StudentName)") + .getSingleResult().getStudents(); + + assertEquals(2, students.size()); + assertEquals("Петров П.П.", students.get(0).getName()); + assertEquals("Сидоров С.С.", students.get(1).getName()); + } + ); + } + + @Test + void typedViewIndex_partialCompositeMatch_doesNotApply() { + // The composite hint requires BOTH StudentName and GroupId in the JOIN's ON clause; + // only GroupId is referenced, so the index must NOT be applied — the query still runs. + inTransaction( + session -> { + List students = session + .createQuery("FROM Group g JOIN FETCH g.students WHERE g.name = 'M3439'", Group.class) + .addQueryHint("use_index:students_group_index:Students(GroupId,StudentName)") + .getSingleResult().getStudents(); + + assertEquals(2, students.size()); + assertEquals("Петров П.П.", students.get(0).getName()); + assertEquals("Сидоров С.С.", students.get(1).getName()); + } + ); + } + + @Test + void groupByGroupName_TypedViewIndex_TableColumn() { + /* + select + g1_0.GroupId, + g1_0.GroupName + from + Groups view group_name_index g1_0 + where + g1_0.GroupName='M3439' + */ + inTransaction( + session -> { + Group group = session + .createQuery("FROM Group g WHERE g.name = 'M3439'", Group.class) + .addQueryHint("use_index:group_name_index:Groups(GroupName)") // Hibernate + .getSingleResult(); + + assertEquals("M3439", group.getName()); + } + ); + + inTransaction( + session -> { + Group group = session + .createQuery("FROM Group g WHERE g.name = 'M3439'", Group.class) + .setHint(HibernateHints.HINT_COMMENT, "use_index:group_name_index:Groups(GroupName)") // JPA + .getSingleResult(); + + assertEquals("M3439", group.getName()); + } + ); + } + @Test void studentsByGroupName_Eager_OneToManyTest() { inTransaction( diff --git a/hibernate-dialect/src/test/java/tech/ydb/hibernate/student/entity/Student.java b/hibernate-dialect/src/test/java/tech/ydb/hibernate/student/entity/Student.java index 6b65bf09..77c96ab6 100644 --- a/hibernate-dialect/src/test/java/tech/ydb/hibernate/student/entity/Student.java +++ b/hibernate-dialect/src/test/java/tech/ydb/hibernate/student/entity/Student.java @@ -4,6 +4,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.Id; +import jakarta.persistence.Index; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToMany; import jakarta.persistence.ManyToOne; @@ -17,7 +18,7 @@ */ @Data @Entity -@Table(name = "Students") +@Table(name = "Students", indexes = @Index(name = "students_group_index", columnList = "GroupId")) public class Student { @Id From 29257196ad9a0a3497b66cc53042b0ddd6d3692d Mon Sep 17 00:00:00 2001 From: Kirill Kurdyukov Date: Mon, 27 Jul 2026 13:51:56 +0300 Subject: [PATCH 2/2] finally refactoring --- hibernate-dialect/CHANGELOG.md | 2 +- .../dialect/hint/IndexHintApplier.java | 87 +++++-------------- .../hint/IndexQueryHintHandlerTest.java | 37 ++++---- .../student/StudentsRepositoryTest.java | 10 +-- 4 files changed, 50 insertions(+), 86 deletions(-) diff --git a/hibernate-dialect/CHANGELOG.md b/hibernate-dialect/CHANGELOG.md index 87eae7a0..2d268b74 100644 --- a/hibernate-dialect/CHANGELOG.md +++ b/hibernate-dialect/CHANGELOG.md @@ -1,7 +1,7 @@ ## 1.7.0 ## - Fixed `HINT_COMMENT` handling: multiple short-form `use_index` hints in one comment become `view a, b` instead of `view a view b` -- Extended `use_index:` hint with table-and-column-aware format `use_index::([,...])` to pin a secondary index per `FROM`/`JOIN` when the same table is joined more than once in one query +- Extended `use_index:` hint with table-and-column-aware format `use_index::([,...])` to pin a secondary index per `JOIN` when the same table is joined more than once in one query - Renamed `hibernate-dialect-v6` to `hibernate-dialect`, removed `hibernate-dialect-v7` - Support Hibernate 7 - Support Java 11 for Hibernate 6 profile diff --git a/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/hint/IndexHintApplier.java b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/hint/IndexHintApplier.java index b2fa38d9..1586fa89 100644 --- a/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/hint/IndexHintApplier.java +++ b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/hint/IndexHintApplier.java @@ -1,6 +1,5 @@ package tech.ydb.hibernate.dialect.hint; -import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -10,18 +9,18 @@ class IndexHintApplier { private final char[] sql; private final Map> typedHints; private final List shortIndexes; - private final List joinViews = new ArrayList<>(); + private final StringBuilder out; private int pos; + private int emitted; private int parenLevel; - private TableRef fromTable; - private View fromView; - private boolean whereSeen; + private boolean changed; private IndexHintApplier(String query, Map> typedHints, List shortIndexes) { this.sql = query.toCharArray(); this.typedHints = typedHints; this.shortIndexes = shortIndexes; + this.out = new StringBuilder(); } static String apply(String query, Map> typedHints, List shortIndexes) { @@ -41,62 +40,35 @@ private String apply() { } if (keyword.equalsIgnoreCase("from")) { - fromTable = parseTableRef(); + if (!shortIndexes.isEmpty()) { + TableRef fromTable = parseTableRef(); + if (fromTable != null) { + emitView(fromTable, String.join(", ", shortIndexes)); + } + } } else if (keyword.equalsIgnoreCase("join")) { TableRef joinTable = parseTableRef(); - if (joinTable != null) { + if (joinTable != null && joinTable.alias != null) { String index = bestIndex(joinTable.table, collectAliasColumnsUntilNextClause(joinTable.alias)); if (index != null) { - joinViews.add(view(joinTable, index)); - } - } - } else if (keyword.equalsIgnoreCase("where")) { - whereSeen = true; - if (fromTable != null) { - String index = bestIndex(fromTable.table, collectAliasColumnsUntilNextClause(fromTable.alias)); - if (index != null) { - fromView = view(fromTable, index); + emitView(joinTable, index); } } } } - applyShortIndexes(); - return render(); - } - - private void applyShortIndexes() { - if (fromView != null || fromTable == null || shortIndexes.isEmpty() || !whereSeen) { - return; - } - fromView = new View(fromTable.afterTable, fromTable.aliasStart, - " view " + String.join(", ", shortIndexes) + " "); - } - - private String render() { - if (fromView == null && joinViews.isEmpty()) { + if (!changed) { return new String(sql); } - - StringBuilder out = new StringBuilder(sql.length + 32); - int copied = 0; - - if (fromView != null) { - out.append(sql, copied, fromView.start - copied); - out.append(fromView.text); - copied = fromView.end; - } - for (View joinView : joinViews) { - out.append(sql, copied, joinView.start - copied); - out.append(joinView.text); - copied = joinView.end; - } - out.append(sql, copied, sql.length - copied); + out.append(sql, emitted, sql.length - emitted); return out.toString(); } - private static View view(TableRef table, String index) { - return new View(table.afterTable, table.aliasStart, " view " + index + " "); + private void emitView(TableRef table, String indexes) { + out.append(sql, emitted, table.afterTable - emitted); + out.append(" view ").append(indexes).append(' '); + emitted = table.aliasStart; + changed = true; } private TableRef parseTableRef() { @@ -109,9 +81,12 @@ private TableRef parseTableRef() { int afterTable = pos; skipWhitespace(); String next = peekIdentifier(); - if (next == null || next.equalsIgnoreCase("view")) { + if (next != null && next.equalsIgnoreCase("view")) { return null; } + if (next == null || isClauseKeyword(next) || next.equalsIgnoreCase("on")) { + return new TableRef(table, null, afterTable, pos); + } int aliasStart = pos; String alias = readIdentifier(); @@ -119,9 +94,7 @@ private TableRef parseTableRef() { skipWhitespace(); alias = readIdentifier(); } - if (alias == null) { - return null; - } + return new TableRef(table, alias, afterTable, aliasStart); } @@ -348,16 +321,4 @@ private static final class TableRef { this.aliasStart = aliasStart; } } - - private static final class View { - final int start; - final int end; - final String text; - - View(int start, int end, String text) { - this.start = start; - this.end = end; - this.text = text; - } - } } diff --git a/hibernate-dialect/src/test/java/tech/ydb/hibernate/hint/IndexQueryHintHandlerTest.java b/hibernate-dialect/src/test/java/tech/ydb/hibernate/hint/IndexQueryHintHandlerTest.java index a9a6c461..7cd0a424 100644 --- a/hibernate-dialect/src/test/java/tech/ydb/hibernate/hint/IndexQueryHintHandlerTest.java +++ b/hibernate-dialect/src/test/java/tech/ydb/hibernate/hint/IndexQueryHintHandlerTest.java @@ -46,11 +46,25 @@ void multipleShortHintsAreJoinedWithComma() { } @Test - void noWhereClauseLeavesQueryAlone() { + void noWhereClauseStillAppliesShortHint() { String query = "select g1_0.GroupId from Groups g1_0"; String result = apply(query, "use_index:group_name_index"); - assertEquals(query, result); + assertEquals( + "select g1_0.GroupId from Groups view group_name_index g1_0", + result + ); + } + + @Test + void shortHintAppliesWithoutAlias() { + String query = "select GroupId from Groups where GroupName='M3439'"; + String result = apply(query, "use_index:group_name_index"); + + assertEquals( + "select GroupId from Groups view group_name_index where GroupName='M3439'", + result + ); } @Test @@ -125,21 +139,10 @@ void leavesUnrelatedTablesUntouched() { } @Test - void rewritesFromTableWhenColumnAppearsInWhere() { + void typedFromHintDoesNotRewriteFromTable() { String query = "select g1_0.GroupId from Groups g1_0 where g1_0.GroupName='M3439'"; String result = apply(query, "use_index:group_name_index:Groups(GroupName)"); - assertEquals( - "select g1_0.GroupId from Groups view group_name_index g1_0 where g1_0.GroupName='M3439'", - result - ); - } - - @Test - void leavesFromTableAloneWhenColumnNotInWhere() { - String query = "select g1_0.GroupId from Groups g1_0 where g1_0.GroupId=1"; - String result = apply(query, "use_index:group_name_index:Groups(GroupName)"); - assertEquals(query, result); } } @@ -239,12 +242,12 @@ void aliasIsNotASubstringMatch() { class Interactions { @Test - void shortFormSkippedWhenTypedAlreadyAddedView() { + void typedFromHintDoesNotBlockShortForm() { String query = "select g1_0.GroupId from Groups g1_0 where g1_0.GroupName='M3439'"; String result = apply(query, "use_index:group_name_index:Groups(GroupName)", "use_index:other_index"); assertEquals( - "select g1_0.GroupId from Groups view group_name_index g1_0 where g1_0.GroupName='M3439'", + "select g1_0.GroupId from Groups view other_index g1_0 where g1_0.GroupName='M3439'", result ); } @@ -331,7 +334,7 @@ void viewIsInsertedBeforeAsAlias() { String query = "select a1.b as a1_b, a2.b as a2_b " + "from a as a1 join a as a2 on a1.d=a2.c where a1.d=1"; String result = apply(query, - "use_index:Index1:a(d)", + "use_index:Index1", "use_index:Index2:a(c)"); assertEquals( diff --git a/hibernate-dialect/src/test/java/tech/ydb/hibernate/student/StudentsRepositoryTest.java b/hibernate-dialect/src/test/java/tech/ydb/hibernate/student/StudentsRepositoryTest.java index 3312af3a..57b1b30c 100644 --- a/hibernate-dialect/src/test/java/tech/ydb/hibernate/student/StudentsRepositoryTest.java +++ b/hibernate-dialect/src/test/java/tech/ydb/hibernate/student/StudentsRepositoryTest.java @@ -226,7 +226,7 @@ void studentsByGroupName_TypedViewIndex_JoinTable() { session -> { List students = session .createQuery("FROM Group g JOIN FETCH g.students WHERE g.name = 'M3439'", Group.class) - .addQueryHint("use_index:group_name_index:Groups(GroupName)") + .addQueryHint("use_index:group_name_index") .addQueryHint("use_index:students_group_index:Students(GroupId)") .getSingleResult().getStudents(); @@ -242,7 +242,7 @@ void studentsByGroupName_TypedViewIndex_JoinTable() { .createQuery("FROM Group g JOIN FETCH g.students WHERE g.name = 'M3439'", Group.class) .setHint( HibernateHints.HINT_COMMENT, - "use_index:group_name_index:Groups(GroupName);" + + "use_index:group_name_index;" + "use_index:students_group_index:Students(GroupId)" ) .getSingleResult().getStudents(); @@ -291,7 +291,7 @@ void typedViewIndex_partialCompositeMatch_doesNotApply() { } @Test - void groupByGroupName_TypedViewIndex_TableColumn() { + void groupByGroupName_ShortViewIndex() { /* select g1_0.GroupId, @@ -305,7 +305,7 @@ void groupByGroupName_TypedViewIndex_TableColumn() { session -> { Group group = session .createQuery("FROM Group g WHERE g.name = 'M3439'", Group.class) - .addQueryHint("use_index:group_name_index:Groups(GroupName)") // Hibernate + .addQueryHint("use_index:group_name_index") // Hibernate .getSingleResult(); assertEquals("M3439", group.getName()); @@ -316,7 +316,7 @@ void groupByGroupName_TypedViewIndex_TableColumn() { session -> { Group group = session .createQuery("FROM Group g WHERE g.name = 'M3439'", Group.class) - .setHint(HibernateHints.HINT_COMMENT, "use_index:group_name_index:Groups(GroupName)") // JPA + .setHint(HibernateHints.HINT_COMMENT, "use_index:group_name_index") // JPA .getSingleResult(); assertEquals("M3439", group.getName());