Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ public final class MysqlSqlCompletionRelationScopeResolver {
MySqlLexer.IGNORE,
MySqlLexer.FORCE);

private static final Set<Integer> FROM_CLAUSE_TAIL_TOKENS = Set.of(
MySqlLexer.WHERE,
MySqlLexer.GROUP,
MySqlLexer.HAVING,
MySqlLexer.WINDOW,
MySqlLexer.ORDER,
MySqlLexer.LIMIT,
MySqlLexer.UNION,
MySqlLexer.INTO);

private MysqlSqlCompletionRelationScopeResolver() {
}

Expand Down Expand Up @@ -81,7 +91,8 @@ public static MysqlSqlCompletionRelationScope resolve(SqlCompletionStatementWind
relations.addAll(MysqlSqlCompletionLocalResultSetResolver.resolveDerivedTables(tokens, relationScanCursor));
}
MysqlSqlCompletionLocalResultSetResolver.resolveCurrentProjection(tokens, cursor).ifPresent(relations::add);
for (int index = 0; index < tokens.size(); index++) {
boolean[] fromRelationSeparators = fromRelationSeparators(tokens);
for (int index = statementStartIndex(tokens, cursor); index < tokens.size(); index++) {
Token token = tokens.get(index);
if (activeSelect != null && token.getStartIndex() >= activeSelect.endOffset()) {
break;
Expand All @@ -98,7 +109,7 @@ public static MysqlSqlCompletionRelationScope resolve(SqlCompletionStatementWind
}
continue;
}
if (!isRelationIntroducer(tokens, index)) {
if (!isRelationIntroducer(tokens, index, fromRelationSeparators)) {
continue;
}
RelationCandidate relationCandidate = relationAfter(tokens, index, true);
Expand Down Expand Up @@ -290,23 +301,97 @@ private static int skipIndexHintType(List<Token> tokens, int index) {
return index;
}

private static boolean isRelationIntroducer(List<Token> tokens, int index) {
private static boolean isRelationIntroducer(List<Token> tokens,
int index,
boolean[] fromRelationSeparators) {
Token token = tokens.get(index);
return switch (token.getType()) {
case MySqlLexer.FROM,
MySqlLexer.JOIN,
MySqlLexer.STRAIGHT_JOIN,
MySqlLexer.INTO -> true;
case MySqlLexer.UPDATE -> !isOnDuplicateKeyUpdate(tokens, index);
case MySqlLexer.COMMA -> fromRelationSeparators[index];
case MySqlLexer.UPDATE -> !isOnDuplicateKeyUpdate(tokens, index)
&& !isLockingClauseUpdate(tokens, index);
default -> false;
};
}

private static boolean[] fromRelationSeparators(List<Token> tokens) {
boolean[] result = new boolean[tokens.size()];
boolean[] insideFromClause = new boolean[tokens.size() + 1];
int depth = 0;
for (int index = 0; index < tokens.size(); index++) {
int tokenType = tokens.get(index).getType();
if (tokenType == MySqlLexer.RR_BRACKET) {
insideFromClause[depth] = false;
depth = Math.max(0, depth - 1);
continue;
}
if (tokenType == MySqlLexer.SELECT || tokenType == MySqlLexer.SEMI) {
insideFromClause[depth] = false;
} else if (tokenType == MySqlLexer.FROM) {
insideFromClause[depth] = true;
} else if (insideFromClause[depth]
&& (FROM_CLAUSE_TAIL_TOKENS.contains(tokenType)
|| isOnDuplicateKeyUpdateStart(tokens, index)
|| isLockingClauseStart(tokens, index))) {
insideFromClause[depth] = false;
} else if (tokenType == MySqlLexer.COMMA) {
result[index] = insideFromClause[depth];
}
if (tokenType == MySqlLexer.LR_BRACKET) {
depth++;
insideFromClause[depth] = false;
}
}
return result;
}

private static boolean isOnDuplicateKeyUpdate(List<Token> tokens, int index) {
int previousIndex = MysqlSqlCompletionTokenUtil.previousDefaultIndex(tokens, index - 1);
return previousIndex >= 0 && tokens.get(previousIndex).getType() == MySqlLexer.KEY
&& MysqlSqlCompletionTokenUtil.hasTokenBefore(tokens, tokens.get(index).getStartIndex(),
MySqlLexer.DUPLICATE);
return hasTokenTypes(tokens, index - 3,
MySqlLexer.ON, MySqlLexer.DUPLICATE, MySqlLexer.KEY, MySqlLexer.UPDATE);
}

private static boolean isOnDuplicateKeyUpdateStart(List<Token> tokens, int index) {
return hasTokenTypes(tokens, index,
MySqlLexer.ON, MySqlLexer.DUPLICATE, MySqlLexer.KEY, MySqlLexer.UPDATE);
}

private static boolean isLockingClauseStart(List<Token> tokens, int index) {
return hasTokenTypes(tokens, index, MySqlLexer.FOR, MySqlLexer.UPDATE)
|| hasTokenTypes(tokens, index, MySqlLexer.FOR, MySqlLexer.SHARE)
|| hasTokenTypes(tokens, index, MySqlLexer.LOCK, MySqlLexer.IN, MySqlLexer.SHARE);
}

private static boolean isLockingClauseUpdate(List<Token> tokens, int index) {
return hasTokenTypes(tokens, index - 1, MySqlLexer.FOR, MySqlLexer.UPDATE);
}

private static boolean hasTokenTypes(List<Token> tokens, int start, int... tokenTypes) {
if (start < 0 || start + tokenTypes.length > tokens.size()) {
return false;
}
for (int offset = 0; offset < tokenTypes.length; offset++) {
if (tokens.get(start + offset).getType() != tokenTypes[offset]) {
return false;
}
}
return true;
}

private static int statementStartIndex(List<Token> tokens, int cursor) {
int result = 0;
for (int index = 0; index < tokens.size(); index++) {
Token token = tokens.get(index);
if (token.getStartIndex() >= cursor) {
break;
}
if (token.getType() == MySqlLexer.SEMI) {
result = index + 1;
}
}
return result;
}

private static MysqlSqlCompletionRelationScope resolveDdlFocusedScope(List<Token> tokens, int cursor) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,26 @@ void completesColumnsAfterAliasDotUsingResolvedTableScope() {
Assertions.assertEquals("u", candidate.getTableAlias());
}

@Test
void completesOnlySecondJoinRelationColumnsAfterAliasDot() {
String sql = "select * from users a join orders b on a.id = b.id where b.";

SqlCompletionResponse result = complete(sql, sql.length());

assertSuccessReplacement(result, sql.length(), sql.length());
assertOnlyOrdersAliasBColumns(result);
}

@Test
void completesOnlySecondCommaSeparatedRelationColumnsAfterAliasDot() {
String sql = "select * from users a, orders b where b.";

SqlCompletionResponse result = complete(sql, sql.length());

assertSuccessReplacement(result, sql.length(), sql.length());
assertOnlyOrdersAliasBColumns(result);
}

@Test
void completesRelationAliasBeforeDotInWherePredicate() {
String sql = """
Expand Down Expand Up @@ -2455,6 +2475,20 @@ private void assertNoCandidate(SqlCompletionResponse result,
() -> "unexpected candidate " + type + ":" + label);
}

private void assertOnlyOrdersAliasBColumns(SqlCompletionResponse result) {
Assertions.assertEquals(SqlCompletionCandidateTypeEnum.COLUMN.name(), metadataProvider.lastRequest.type());
Assertions.assertEquals("orders", metadataProvider.lastRequest.scope().table());
Assertions.assertEquals("", metadataProvider.lastRequest.prefix());
Assertions.assertTrue(result.getCandidates().stream()
.allMatch(candidate -> candidate.getType() == SqlCompletionCandidateTypeEnum.COLUMN
&& "orders".equals(candidate.getTableName())
&& "b".equals(candidate.getTableAlias())));
Assertions.assertTrue(result.getCandidates().stream()
.anyMatch(candidate -> "amount".equals(candidate.getLabel())));
Assertions.assertFalse(result.getCandidates().stream()
.anyMatch(candidate -> "name".equals(candidate.getLabel())));
}

private void assertFunctionCandidate(String sql, String label, String detail, String dataType) {
SqlCompletionResponse result = complete(sql, sql.length());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,87 @@ void keepsJoinAliasesSeparated() {
relation(null, null, "orders", "o"));
}

@Test
void resolvesCommaRelationAfterJoinPredicate() {
MysqlSqlCompletionRelationScope scope = resolveAtCaret(
"select * from users u join orders o on u.id = o.user_id, payments p where p.{caret}");

assertRelations(scope,
relation(null, null, "users", "u"),
relation(null, null, "orders", "o"),
relation(null, null, "payments", "p"));
}

@Test
void doesNotReuseFromFromSiblingSubqueryForProjectionComma() {
MysqlSqlCompletionRelationScope scope = resolveAtCaret(
"select (select id from users u), (select x, y, z{caret} from orders o)");

assertRelations(scope, relation(null, null, "orders", "o"));
Assertions.assertTrue(scope.resolveOwner("y").isEmpty());
}

@Test
void doesNotReuseFromFromPreviousCteBodyForProjectionComma() {
MysqlSqlCompletionRelationScope scope = resolveAtCaret(
"with first_cte as (select id from users u), "
+ "second_cte as (select x, y, z{caret} from orders o) select * from second_cte");

assertRelations(scope, relation(null, null, "orders", "o"));
Assertions.assertTrue(scope.resolveOwner("y").isEmpty());
}

@Test
void doesNotReuseFromFromPreviousStatementForProjectionComma() {
MysqlSqlCompletionRelationScope scope = resolveAtCaret(
"select id from users u; select x, y, z{caret} from orders o");

assertRelations(scope, relation(null, null, "orders", "o"));
Assertions.assertTrue(scope.resolveOwner("y").isEmpty());
}

@Test
void doesNotTreatOnDuplicateKeyUpdateCommaAsRelation() {
MysqlSqlCompletionRelationScope scope = resolveAtCaret(
"insert into dst (id, v) select s.id, s.v from src s "
+ "on duplicate key update id = values(id), v = s.{caret}");

assertRelations(scope,
relation(null, null, "dst", null),
relation(null, null, "src", "s"));
Assertions.assertTrue(scope.resolveOwner("v").isEmpty());
}

@Test
void doesNotTreatForUpdateCommaAsRelation() {
MysqlSqlCompletionRelationScope scope = resolveAtCaret(
"select * from users u join orders o on u.id = o.user_id for update of u, o.{caret}");

assertRelations(scope,
relation(null, null, "users", "u"),
relation(null, null, "orders", "o"));
}

@Test
void doesNotTreatForShareCommaAsRelation() {
MysqlSqlCompletionRelationScope scope = resolveAtCaret(
"select * from users u join orders o on u.id = o.user_id for share of u, o.{caret}");

assertRelations(scope,
relation(null, null, "users", "u"),
relation(null, null, "orders", "o"));
}

@Test
void doesNotTreatNamedWindowAsRelation() {
MysqlSqlCompletionRelationScope scope = resolveAtCaret(
"select * from users u window w1 as (order by u.id), w2 as (order by u.id) "
+ "order by u.{caret}");

assertRelations(scope, relation(null, null, "users", "u"));
Assertions.assertTrue(scope.resolveOwner("w2").isEmpty());
}

@Test
void doesNotTreatIndexHintAsAlias() {
MysqlSqlCompletionRelationScope scope = resolveAtCaret(
Expand Down
Loading