diff --git a/bundles/org.jkiss.utils/src/org/jkiss/utils/CommonUtils.java b/bundles/org.jkiss.utils/src/org/jkiss/utils/CommonUtils.java index 5e80f24f916f308ac34b7de3c3bd83f6bc095334..af0e1346dd4b82f582065f1d0fe87351bbad501f 100644 --- a/bundles/org.jkiss.utils/src/org/jkiss/utils/CommonUtils.java +++ b/bundles/org.jkiss.utils/src/org/jkiss/utils/CommonUtils.java @@ -743,7 +743,7 @@ public class CommonUtils { return String.format("%1$"+length+ "s", string); } - public static boolean startsWithIgnoreCase(@NotNull String str, @NotNull String startPart) { + public static boolean startsWithIgnoreCase(@Nullable String str, @Nullable String startPart) { if (isEmpty(str) || isEmpty(startPart)) { return false; } diff --git a/plugins/org.jkiss.dbeaver.model.sql/src/org/jkiss/dbeaver/model/sql/completion/SQLCompletionAnalyzer.java b/plugins/org.jkiss.dbeaver.model.sql/src/org/jkiss/dbeaver/model/sql/completion/SQLCompletionAnalyzer.java index d8235686cd66fa925940419a0ca658bf3df740a8..6ec3ce90f51e8edf522eb7ae639aece157b35346 100644 --- a/plugins/org.jkiss.dbeaver.model.sql/src/org/jkiss/dbeaver/model/sql/completion/SQLCompletionAnalyzer.java +++ b/plugins/org.jkiss.dbeaver.model.sql/src/org/jkiss/dbeaver/model/sql/completion/SQLCompletionAnalyzer.java @@ -19,6 +19,8 @@ package org.jkiss.dbeaver.model.sql.completion; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.util.TablesNamesFinder; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; @@ -39,16 +41,18 @@ import org.jkiss.dbeaver.model.runtime.DBRRunnableParametrized; import org.jkiss.dbeaver.model.sql.*; import org.jkiss.dbeaver.model.sql.parser.SQLParserPartitions; import org.jkiss.dbeaver.model.sql.parser.SQLWordPartDetector; +import org.jkiss.dbeaver.model.sql.parser.tokens.SQLTokenType; import org.jkiss.dbeaver.model.struct.*; import org.jkiss.dbeaver.model.text.TextUtils; +import org.jkiss.dbeaver.model.text.parser.TPRuleBasedScanner; +import org.jkiss.dbeaver.model.text.parser.TPToken; +import org.jkiss.dbeaver.model.text.parser.TPTokenDefault; import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.Pair; import java.lang.reflect.InvocationTargetException; import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; /** * Completion analyzer @@ -168,7 +172,7 @@ public class SQLCompletionAnalyzer implements DBRRunnableParametrized name = extractTableName(wordPart, true); + if (name != null) { + final String tableName = name.getFirst(); + final String tableAlias = name.getSecond(); if (!hasProposal(proposals, tableName)) { proposals.add( 0, @@ -716,7 +711,7 @@ public class SQLCompletionAnalyzer implements DBRRunnableParametrized nameList = new ArrayList<>(); - SQLDialect sqlDialect = dataSource.getSQLDialect(); - { - // Regex matching MUST be very fast. - // Otherwise UI will freeze during SQL typing. - // So let's make regex as simple as possible. - // TODO: will be replaced by SQL preparse + structure analysis - -// String quote = quoteString == null ? SQLConstants.STR_QUOTE_DOUBLE : -// SQLConstants.STR_QUOTE_DOUBLE.equals(quoteString) || SQLConstants.STR_QUOTE_APOS.equals(quoteString) ? -// quoteString : -// Pattern.quote(quoteString); - String catalogSeparator = sqlDialect.getCatalogSeparator(); - while (token.endsWith(catalogSeparator)) { - token = token.substring(0, token.length() -1); - } + final Pair name = extractTableName(token, false); + if (name != null && CommonUtils.isNotEmpty(name.getFirst())) { + final String[][] quoteStrings = sqlDialect.getIdentifierQuoteStrings(); + final String[] allNames = SQLUtils.splitFullIdentifier(name.getFirst(), catalogSeparator, quoteStrings, false); + return SQLSearchUtils.findObjectByFQN(monitor, sc, request.getContext().getExecutionContext(), Arrays.asList(allNames), !request.isSimpleMode(), request.getWordDetector()); + } - // Use silly pattern with all possible characters - // Valid regex for quote identifiers and FQ names is monstrous and very slow - String tableNamePattern = getTableNamePattern(sqlDialect); - String structNamePattern; - if (CommonUtils.isEmpty(token)) { - String kwList = "from|update|join|into"; - if (request.getQueryType() != SQLCompletionRequest.QueryType.COLUMN) { - kwList = kwList + "|,"; - } - structNamePattern = "(?:" + kwList + ")\\s+" + tableNamePattern; - } else { - structNamePattern = getTableAliasPattern(token, tableNamePattern); - } + return null; + } - Pattern aliasPattern; - try { - aliasPattern = Pattern.compile(structNamePattern, Pattern.CASE_INSENSITIVE); - } catch (PatternSyntaxException e) { - // Bad pattern - seems to be a bad token - return null; - } - // Append trailing space to let alias regex match correctly - String testQuery = SQLUtils.stripComments(request.getContext().getSyntaxManager().getDialect(), activeQuery.getText()) + " "; - Matcher matcher = aliasPattern.matcher(testQuery); - while (matcher.find()) { - if (!nameList.isEmpty() && (firstMatch || matcher.start() > request.getDocumentOffset() - activeQuery.getOffset())) { - // Do not search after cursor + @Nullable + private Pair extractTableName(@Nullable String tableAlias, boolean allowPartialMatch) { + final IDocument document = request.getDocument(); + final TPRuleBasedScanner scanner = request.getScanner(); + final SQLScriptElement activeQuery = request.getActiveQuery(); + + scanner.setRange(document, activeQuery.getOffset(), activeQuery.getLength()); + + /* + When we search for table name knowing its alias, we want to match the following sequence: + [FROM|UPDATE|JOIN|INTO] [AS]? + + If we don't know the alias, the following sequence must be used instead: + [FROM|UPDATE|JOIN|INTO] + + We use "state machine" to process such sequences. The transition table is listed below: + UNMATCHED -> TABLE_NAME ; if found starting token (FROM, UPDATE, JOIN, INTO, etc.). + TABLE_NAME -> ALIAS_AS ; if found string token, and the alias is known. + TABLE_NAME -> MATCHED ; if found string token, and the alias is unknown. + ALIAS_AS -> ALIAS_NAME ; if found 'as' token. + ALIAS_NAME -> MATCHED ; if found string token. + */ + + try { + final int STATE_UNMATCHED = 0; + final int STATE_TABLE_NAME = 1; + final int STATE_ALIAS_AS = 2; + final int STATE_ALIAS_NAME = 3; + final int STATE_MATCHED = 4; + + int state = STATE_UNMATCHED; + String matchedTableName = null; + String matchedTableAlias = null; + + while (true) { + final TPToken tok = scanner.nextToken(); + if (tok.isEOF()) { break; } - nameList.clear(); - int groupCount = matcher.groupCount(); - for (int i = 1; i <= groupCount; i++) { - String group = matcher.group(i); - if (!CommonUtils.isEmpty(group)) { - String[][] quoteStrings = sqlDialect.getIdentifierQuoteStrings(); - - String[] allNames = SQLUtils.splitFullIdentifier(group, catalogSeparator, quoteStrings, false); - Collections.addAll(nameList, allNames); + if (!(tok instanceof TPTokenDefault)) { + continue; + } + final String value = document.get(scanner.getTokenOffset(), scanner.getTokenLength()); + if (state == STATE_UNMATCHED && tok.getData() == SQLTokenType.T_KEYWORD && + (value.equalsIgnoreCase(SQLConstants.KEYWORD_FROM) || + value.equalsIgnoreCase(SQLConstants.KEYWORD_UPDATE) || + value.equalsIgnoreCase(SQLConstants.KEYWORD_JOIN) || + value.equalsIgnoreCase(SQLConstants.KEYWORD_INTO))) { + state = STATE_TABLE_NAME; + continue; + } + if (state == STATE_TABLE_NAME && (tok.getData() == SQLTokenType.T_QUOTED || tok.getData() == SQLTokenType.T_OTHER)) { + matchedTableName = value; + if (CommonUtils.isEmpty(tableAlias)) { + state = STATE_MATCHED; + } else { + state = STATE_ALIAS_AS; + continue; + } + } + if (state == STATE_ALIAS_AS && tok.getData() == SQLTokenType.T_KEYWORD && "AS".equalsIgnoreCase(value)) { + state = STATE_ALIAS_NAME; + continue; + } + if ((state == STATE_ALIAS_AS || state == STATE_ALIAS_NAME) && (tok.getData() == SQLTokenType.T_QUOTED || tok.getData() == SQLTokenType.T_OTHER)) { + matchedTableAlias = value; + state = STATE_MATCHED; + } + if (state == STATE_MATCHED) { + final boolean fullMatch = CommonUtils.isEmpty(tableAlias) || tableAlias.equals(matchedTableAlias); + final boolean partialMatch = fullMatch || (allowPartialMatch && CommonUtils.startsWithIgnoreCase(matchedTableAlias, tableAlias)); + if (!fullMatch && !partialMatch) { + // The presented alias does not fully or partially match the matched token, reset + state = STATE_UNMATCHED; + matchedTableName = null; + matchedTableAlias = null; + } else { + return new Pair<>(matchedTableName, matchedTableAlias); } } } + } catch (BadLocationException e) { + log.debug(e); } - return SQLSearchUtils.findObjectByFQN(monitor, sc, request.getContext().getExecutionContext(), nameList, !request.isSimpleMode(), request.getWordDetector()); - } - - private String getTableAliasPattern(String alias, String tableNamePattern) { - return tableNamePattern + "\\s+(?:as\\s)?" + alias + "[\\s,]+"; - } - - private static String getTableNamePattern(SQLDialect sqlDialect) { - String[][] quoteStrings = sqlDialect.getIdentifierQuoteStrings(); - StringBuilder quotes = new StringBuilder(); - if (quoteStrings != null) { - for (String[] quotePair : quoteStrings) { - if (quotes.indexOf(quotePair[0]) == -1) quotes.append('\\').append(quotePair[0]); - if (quotes.indexOf(quotePair[1]) == -1) quotes.append('\\').append(quotePair[1]); - } - } - // Use silly pattern with all possible characters - // Valid regex for quote identifiers and FQ names is monstrous and very slow - return "([\\p{L}0-9_$ยง#@\\.\\-" + quotes.toString() + "]+)"; + return null; } private void makeProposalsFromChildren(DBPObject parent, @Nullable String startPart, boolean addFirst, Map params) throws DBException { @@ -1115,7 +1130,7 @@ public class SQLCompletionAnalyzer implements DBRRunnableParametrized