/* * DBeaver - Universal Database Manager * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) * * 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 org.jkiss.dbeaver.ui.editors.sql.syntax; import org.eclipse.jface.text.contentassist.ContextInformation; import org.eclipse.swt.graphics.Image; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.core.DBeaverCore; import org.jkiss.dbeaver.model.*; import org.jkiss.dbeaver.model.impl.DBObjectNameCaseTransformer; import org.jkiss.dbeaver.model.navigator.DBNNode; import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.sql.*; import org.jkiss.dbeaver.model.struct.*; import org.jkiss.dbeaver.ui.DBeaverIcons; import org.jkiss.dbeaver.ui.TextUtils; import org.jkiss.dbeaver.ui.editors.sql.SQLEditorBase; import org.jkiss.dbeaver.ui.editors.sql.SQLPreferenceConstants; import org.jkiss.utils.CommonUtils; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; /** * Completion utils */ class SQLCompletionAnalyzer { private static final Log log = Log.getLog(SQLCompletionAnalyzer.class); static class CompletionRequest { final SQLEditorBase editor; final boolean simpleMode; int documentOffset; String activeQuery = null; SQLWordPartDetector wordDetector; String wordPart; SQLCompletionProcessor.QueryType queryType; final List<SQLCompletionProposal> proposals = new ArrayList<>(); CompletionRequest(SQLEditorBase editor, int documentOffset, boolean simpleMode) { this.editor = editor; this.documentOffset = documentOffset; this.simpleMode = simpleMode; } } private final DBRProgressMonitor monitor; private final CompletionRequest request; SQLCompletionAnalyzer(DBRProgressMonitor monitor, CompletionRequest request) { this.monitor = monitor; this.request = request; } void runAnalyzer() { DBPDataSource dataSource = request.editor.getDataSource(); if (dataSource == null) { return; } if (request.queryType != null) { // Try to determine which object is queried (if wordPart is not empty) // or get list of root database objects if (request.wordPart.length() == 0) { // Get root objects DBPObject rootObject = null; if (request.queryType == SQLCompletionProcessor.QueryType.COLUMN && dataSource instanceof DBSObjectContainer) { // Try to detect current table rootObject = getTableFromAlias((DBSObjectContainer)dataSource, null); } else if (dataSource instanceof DBSObjectContainer) { // Try to get from active object DBSObject selectedObject = DBUtils.getActiveInstanceObject(dataSource); if (selectedObject != null) { makeProposalsFromChildren(selectedObject, null); rootObject = DBUtils.getPublicObject(selectedObject.getParentObject()); } else { rootObject = dataSource; } } if (rootObject != null) { makeProposalsFromChildren(rootObject, null); } } else { DBSObject rootObject = null; if (request.queryType == SQLCompletionProcessor.QueryType.COLUMN && dataSource instanceof DBSObjectContainer) { // Part of column name // Try to get from active object DBSObjectContainer sc = (DBSObjectContainer) dataSource; DBSObject selectedObject = DBUtils.getActiveInstanceObject(dataSource); if (selectedObject instanceof DBSObjectContainer) { sc = (DBSObjectContainer)selectedObject; } int divPos = request.wordPart.indexOf(request.editor.getSyntaxManager().getStructSeparator()); String tableAlias = divPos == -1 ? null : request.wordPart.substring(0, divPos); rootObject = getTableFromAlias(sc, tableAlias); } if (rootObject != null) { makeProposalsFromChildren(rootObject, request.wordPart); } else { // Get root object or objects from active database (if any) makeDataSourceProposals(); } } } else { // Get list of sub-objects (filtered by wordPart) makeDataSourceProposals(); } } private void makeDataSourceProposals() { DBPDataSource dataSource = request.editor.getDataSource(); final DBSObjectContainer rootContainer = DBUtils.getAdapter(DBSObjectContainer.class, dataSource); if (rootContainer == null) { return; } DBSObjectContainer sc = rootContainer; DBSObject childObject = sc; List<String> tokens = request.wordDetector.splitWordPart(); // Detect selected object (container). // There could be multiple selected objects on different hierarchy levels (e.g. PG) DBSObjectContainer selectedContainers[]; { DBSObject[] selectedObjects = DBUtils.getSelectedObjects(dataSource); selectedContainers = new DBSObjectContainer[selectedObjects.length]; for (int i = 0; i < selectedObjects.length; i++) { selectedContainers[i] = DBUtils.getAdapter(DBSObjectContainer.class, selectedObjects[i]); } } String lastToken = null; for (int i = 0; i < tokens.size(); i++) { final String token = tokens.get(i); if (i == tokens.size() - 1 && !request.wordDetector.getWordPart().endsWith(".")) { lastToken = token; break; } if (sc == null) { break; } // Get next structure container try { final String objectName = request.wordDetector.isQuoted(token) ? request.wordDetector.removeQuotes(token) : DBObjectNameCaseTransformer.transformName(dataSource, token); childObject = objectName == null ? null : sc.getChild(monitor, objectName); if (childObject == null && i == 0 && objectName != null) { for (int k = 0; k < selectedContainers.length; k++) { if (selectedContainers[k] != null) { // Probably it is from selected object, let's try it childObject = selectedContainers[k].getChild(monitor, objectName); if (childObject != null) { sc = selectedContainers[k]; break; } } } } if (childObject == null) { if (i == 0) { // Assume it's a table alias ? childObject = getTableFromAlias(sc, token); if (childObject == null && !request.simpleMode) { // Search using structure assistant DBSStructureAssistant structureAssistant = DBUtils.getAdapter(DBSStructureAssistant.class, sc); if (structureAssistant != null) { Collection<DBSObjectReference> references = structureAssistant.findObjectsByMask( monitor, null, structureAssistant.getAutoCompleteObjectTypes(), request.wordDetector.removeQuotes(token), request.wordDetector.isQuoted(token), false, 2); if (!references.isEmpty()) { childObject = references.iterator().next().resolveObject(monitor); } } } } else { // Path element not found. Damn - can't do anything. return; } } if (childObject instanceof DBSObjectContainer) { sc = (DBSObjectContainer) childObject; } else { sc = null; } } catch (DBException e) { log.error(e); return; } } if (childObject == null) { return; } if (lastToken == null) { // Get all children objects as proposals makeProposalsFromChildren(childObject, null); } else { // Get matched children makeProposalsFromChildren(childObject, lastToken); if (tokens.size() == 1) { // Get children from selected object } if (request.proposals.isEmpty() || tokens.size() == 1) { // Try in active object for (int k = 0; k < selectedContainers.length; k++) { if (selectedContainers[k] != null && selectedContainers[k] != childObject) { makeProposalsFromChildren(selectedContainers[k], lastToken); } } if (!request.simpleMode) { // At last - try to find child tables by pattern DBSStructureAssistant structureAssistant = null; for (DBSObject object = childObject; object != null; object = object.getParentObject()) { structureAssistant = DBUtils.getAdapter(DBSStructureAssistant.class, object); if (structureAssistant != null) { break; } } if (structureAssistant != null) { makeProposalsFromAssistant(structureAssistant, sc, lastToken); } } } } } @Nullable private DBSObject getTableFromAlias(DBSObjectContainer sc, @Nullable String token) { final DBPDataSource dataSource = request.editor.getDataSource(); if (!(dataSource instanceof SQLDataSource)) { return null; } if (request.activeQuery == null) { final SQLScriptElement queryAtPos = request.editor.extractQueryAtPos(request.documentOffset); if (queryAtPos != null) { request.activeQuery = queryAtPos.getText() + " "; } } if (request.activeQuery == null) { return null; } if (token == null) { token = ""; } final List<String> nameList = new ArrayList<>(); SQLDialect sqlDialect = ((SQLDataSource) dataSource).getSQLDialect(); String quoteString = sqlDialect.getIdentifierQuoteString(); { // 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); } //String separatorPattern = ".".equals(catalogSeparator) ? "\\." : Pattern.quote(catalogSeparator); String tableNamePattern = "([\\w_$\\.\\-\"`]+)"; String structNamePattern; if (CommonUtils.isEmpty(token)) { structNamePattern = "(?:from|update|join|into)\\s*" + tableNamePattern; } else { structNamePattern = tableNamePattern + "\\s+(?:as\\s)?" + token + "[\\s,]+"; } Pattern aliasPattern; try { aliasPattern = Pattern.compile(structNamePattern, Pattern.CASE_INSENSITIVE); } catch (PatternSyntaxException e) { // Bad pattern - seems to be a bad token return null; } String testQuery = SQLUtils.stripComments(request.editor.getSyntaxManager().getDialect(), request.activeQuery); Matcher matcher = aliasPattern.matcher(testQuery); if (matcher.find()) { int groupCount = matcher.groupCount(); for (int i = 1; i <= groupCount; i++) { String group = matcher.group(i); if (!CommonUtils.isEmpty(group)) { String[] allNames = group.split(Pattern.quote(catalogSeparator)); Collections.addAll(nameList, allNames); } } } } if (nameList.isEmpty()) { return null; } // Fix names (convert case or remove quotes) for (int i = 0; i < nameList.size(); i++) { String name = nameList.get(i); if (quoteString != null && name.startsWith(quoteString) && name.endsWith(quoteString)) { name = name.substring(1, name.length() - 1); } else { name = DBObjectNameCaseTransformer.transformName(sc.getDataSource(), name); } nameList.set(i, name); } try { DBSObject childObject = null; while (childObject == null) { childObject = DBUtils.findNestedObject(monitor, sc, nameList); if (childObject == null) { DBSObjectContainer parentSc = DBUtils.getParentAdapter(DBSObjectContainer.class, sc); if (parentSc == null) { break; } sc = parentSc; } } if (childObject == null && nameList.size() <= 1) { if (!request.simpleMode) { // No such object found - may be it's start of table name DBSStructureAssistant structureAssistant = DBUtils.getAdapter(DBSStructureAssistant.class, sc); if (structureAssistant != null) { String objectNameMask = nameList.get(0); Collection<DBSObjectReference> tables = structureAssistant.findObjectsByMask( monitor, sc, structureAssistant.getAutoCompleteObjectTypes(), request.wordDetector.removeQuotes(objectNameMask), request.wordDetector.isQuoted(objectNameMask), false, 2); if (!tables.isEmpty()) { return tables.iterator().next().resolveObject(monitor); } } } return null; } else { return childObject; } } catch (DBException e) { log.error(e); return null; } } private void makeProposalsFromChildren(DBPObject parent, @Nullable String startPart) { if (startPart != null) { startPart = request.wordDetector.removeQuotes(startPart).toUpperCase(Locale.ENGLISH); int divPos = startPart.lastIndexOf(request.editor.getSyntaxManager().getStructSeparator()); if (divPos != -1) { startPart = startPart.substring(divPos + 1); } } try { Collection<? extends DBSObject> children = null; if (parent instanceof DBSObjectContainer) { children = ((DBSObjectContainer)parent).getChildren(monitor); } else if (parent instanceof DBSEntity) { children = ((DBSEntity)parent).getAttributes(monitor); } if (children != null && !children.isEmpty()) { //boolean isJoin = SQLConstants.KEYWORD_JOIN.equals(request.wordDetector.getPrevKeyWord()); List<DBSObject> matchedObjects = new ArrayList<>(); final Map<String, Integer> scoredMatches = new HashMap<>(); boolean simpleMode = request.simpleMode; boolean allObjects = !simpleMode && SQLCompletionProcessor.ALL_COLUMNS_PATTERN.equals(startPart); String objPrefix = null; if (allObjects) { if (!CommonUtils.isEmpty(request.wordDetector.getPrevWords())) { String prevWord = request.wordDetector.getPrevWords().get(0); if (prevWord.length() > 0 && prevWord.charAt(prevWord.length() - 1) == request.editor.getSyntaxManager().getStructSeparator()) { objPrefix = prevWord; } } } StringBuilder combinedMatch = new StringBuilder(); for (DBSObject child : children) { if (DBUtils.isHiddenObject(child)) { // Skip hidden continue; } if (allObjects) { if (combinedMatch.length() > 0) { combinedMatch.append(", "); if (objPrefix != null) combinedMatch.append(objPrefix); } combinedMatch.append(DBUtils.getQuotedIdentifier(child)); } else if (simpleMode) { if (startPart == null || child.getName().toUpperCase(Locale.ENGLISH).startsWith(startPart)) { matchedObjects.add(child); } } else { int score = CommonUtils.isEmpty(startPart) ? 1 : TextUtils.fuzzyScore(child.getName(), startPart); if (score > 0) { matchedObjects.add(child); scoredMatches.put(child.getName(), score); } } } if (combinedMatch.length() > 0) { String replaceString = combinedMatch.toString(); request.proposals.add(createCompletionProposal( request, replaceString, replaceString, DBPKeywordType.OTHER, "All objects")); } else if (!matchedObjects.isEmpty()) { if (startPart != null) { if (simpleMode) { Collections.sort(matchedObjects, new Comparator<DBSObject>() { @Override public int compare(DBSObject o1, DBSObject o2) { return o1.getName().compareTo(o2.getName()); } }); } else { Collections.sort(matchedObjects, new Comparator<DBSObject>() { @Override public int compare(DBSObject o1, DBSObject o2) { int score1 = scoredMatches.get(o1.getName()); int score2 = scoredMatches.get(o2.getName()); if (score1 == score2) { if (o1 instanceof DBSAttributeBase) { return ((DBSAttributeBase) o1).getOrdinalPosition() - ((DBSAttributeBase) o2).getOrdinalPosition(); } return o1.getName().compareTo(o2.getName()); } return score2 - score1; } }); } } for (DBSObject child : matchedObjects) { request.proposals.add(makeProposalsFromObject(child)); } } } } catch (DBException e) { log.error(e); } } private void makeProposalsFromAssistant( DBSStructureAssistant assistant, @Nullable DBSObjectContainer rootSC, String objectName) { try { Collection<DBSObjectReference> references = assistant.findObjectsByMask( monitor, rootSC, assistant.getAutoCompleteObjectTypes(), request.wordDetector.removeQuotes(objectName) + "%", request.wordDetector.isQuoted(objectName), false, 100); for (DBSObjectReference reference : references) { request.proposals.add(makeProposalsFromObject(reference, reference.getObjectType().getImage())); } } catch (DBException e) { log.error(e); } } private SQLCompletionProposal makeProposalsFromObject(DBSObject object) { DBNNode node = DBeaverCore.getInstance().getNavigatorModel().getNodeByObject(monitor, object, false); return makeProposalsFromObject(object, node == null ? null : node.getNodeIconDefault()); } private SQLCompletionProposal makeProposalsFromObject( DBPNamedObject object, @Nullable DBPImage objectIcon) { String objectName = DBUtils.getObjectFullName(object, DBPEvaluationContext.DML); boolean isSingleObject = true; String replaceString = null; DBPDataSource dataSource = request.editor.getDataSource(); if (dataSource != null) { // If we replace short name with referenced object // and current active schema (catalog) is not this object's container then // replace with full qualified name if (!request.editor.getActivePreferenceStore().getBoolean(SQLPreferenceConstants.PROPOSAL_SHORT_NAME) && object instanceof DBSObjectReference) { if (request.wordDetector.getFullWord().indexOf(request.editor.getSyntaxManager().getStructSeparator()) == -1) { DBSObjectReference structObject = (DBSObjectReference) object; if (structObject.getContainer() != null) { DBSObject selectedObject = DBUtils.getActiveInstanceObject(dataSource); if (selectedObject != structObject.getContainer()) { replaceString = DBUtils.getFullQualifiedName( dataSource, structObject.getContainer(), object); isSingleObject = false; } } } } if (replaceString == null) { replaceString = DBUtils.getQuotedIdentifier(dataSource, object.getName()); } } else { replaceString = DBUtils.getObjectShortName(object); } return createCompletionProposal( request, replaceString, objectName, DBPKeywordType.OTHER, objectIcon, isSingleObject, object); } /* * Turns the vector into an Array of ICompletionProposal objects */ static SQLCompletionProposal createCompletionProposal( CompletionRequest request, String replaceString, String displayString, DBPKeywordType proposalType, @Nullable DBPImage image, boolean isObject, @Nullable DBPNamedObject object) { DBPPreferenceStore store = request.editor.getActivePreferenceStore(); DBPDataSource dataSource = request.editor.getDataSource(); if (dataSource != null) { if (isObject) { // Escape replace string if required replaceString = DBUtils.getQuotedIdentifier(dataSource, replaceString); } } // If we have quoted string then ignore pref settings boolean quotedString = request.wordDetector.isQuoted(replaceString); if (!quotedString) { final int proposalCase = store.getInt(SQLPreferenceConstants.PROPOSAL_INSERT_CASE); switch (proposalCase) { case SQLPreferenceConstants.PROPOSAL_CASE_UPPER: replaceString = replaceString.toUpperCase(); break; case SQLPreferenceConstants.PROPOSAL_CASE_LOWER: replaceString = replaceString.toLowerCase(); break; default: // Do not convert case if we got it directly from object if (!isObject) { DBPIdentifierCase convertCase = dataSource instanceof SQLDataSource ? ((SQLDataSource) dataSource).getSQLDialect().storesUnquotedCase() : DBPIdentifierCase.MIXED; replaceString = convertCase.transform(replaceString); } break; } } Image img = image == null ? null : DBeaverIcons.getImage(image); return new SQLCompletionProposal( request, displayString, replaceString, // replacementString replaceString.length(), //cursorPosition the position of the cursor following the insert // relative to replacementOffset img, //image to display new ContextInformation(img, displayString, displayString), //the context information associated with this proposal proposalType, null, object); } protected static SQLCompletionProposal createCompletionProposal( CompletionRequest request, String replaceString, String displayString, DBPKeywordType proposalType, String description) { return new SQLCompletionProposal( request, displayString, replaceString, // replacementString replaceString.length(), //cursorPosition the position of the cursor following the insert null, //image to display new ContextInformation(null, displayString, displayString), //the context information associated with this proposal proposalType, description, null); } }