From e8c0271dc27b1de025c0c22418d4d12fda61a00c Mon Sep 17 00:00:00 2001 From: Eric Bresie Date: Fri, 23 Jul 2021 12:35:03 -0500 Subject: [PATCH 1/2] [NETBEANS-189] Updates for Sql autocomplete --- .../db/sql/analyzer/SQLStatementAnalyzer.java | 5 +- .../completion/SQLCompletionProvider.java | 86 ++++--- .../editor/completion/SQLCompletionQuery.java | 222 +++++++++++------- 3 files changed, 202 insertions(+), 111 deletions(-) diff --git a/ide/db.sql.editor/src/org/netbeans/modules/db/sql/analyzer/SQLStatementAnalyzer.java b/ide/db.sql.editor/src/org/netbeans/modules/db/sql/analyzer/SQLStatementAnalyzer.java index 299e2a39140e..155cc61f8100 100644 --- a/ide/db.sql.editor/src/org/netbeans/modules/db/sql/analyzer/SQLStatementAnalyzer.java +++ b/ide/db.sql.editor/src/org/netbeans/modules/db/sql/analyzer/SQLStatementAnalyzer.java @@ -34,7 +34,6 @@ import org.netbeans.modules.db.sql.analyzer.SQLStatement.Context; import org.netbeans.modules.db.sql.editor.StringUtils; import org.netbeans.modules.db.sql.lexer.SQLTokenId; - /** * * @author Jiri Rechtacek, Jiri Skrivanek @@ -239,6 +238,10 @@ protected QualIdent parseIdentifier() { } protected String getUnquotedIdentifier() { + // quoter unavailable so returnas is + if (quoter == null) { + return seq.token().text().toString(); + } return quoter.unquote(seq.token().text().toString()); } diff --git a/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionProvider.java b/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionProvider.java index 8d3aa3d8a6a9..78fbb144a626 100644 --- a/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionProvider.java +++ b/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionProvider.java @@ -22,7 +22,6 @@ import javax.swing.text.Document; import javax.swing.text.JTextComponent; import org.netbeans.api.db.explorer.DatabaseConnection; -import org.netbeans.api.editor.completion.Completion; import org.netbeans.api.lexer.Language; import org.netbeans.api.lexer.TokenHierarchy; import org.netbeans.api.lexer.TokenSequence; @@ -41,39 +40,61 @@ * @author Andrei Badea */ public class SQLCompletionProvider implements CompletionProvider { - + @Override public CompletionTask createTask(int queryType, JTextComponent component) { - if (queryType == CompletionProvider.COMPLETION_QUERY_TYPE || queryType == CompletionProvider.COMPLETION_ALL_QUERY_TYPE) { + if (queryType == CompletionProvider.COMPLETION_QUERY_TYPE || + queryType == CompletionProvider.COMPLETION_ALL_QUERY_TYPE) { + + /* to support DB related completion tasks (i.e. auto populating table + names or db columns for given schema) check for connection */ DatabaseConnection dbconn = findDBConn(component); - if (dbconn == null) { - // XXX perhaps should have an item in the completion instead? - Completion.get().hideAll(); - SQLExecutionBaseAction.notifyNoDatabaseConnection(); - return null; - } + return new AsyncCompletionTask(new SQLCompletionQuery(dbconn), component); } + + // not a completion query type so return nothing return null; } + /** + * getAutoQueryTypes is invoked to check whether a popup with suggestions + * should be shown without the user explicitly asking for it. + * + * If either #getAutoQueryTypes return a non-zero value or the user + * explicitly asks for completion, #createTask is invoked with the + * requested type. In case of SQL see + * org.netbeans.modules.db.sql.editor.completion.SQLCompletionQuery. + * + * @param component + * @param typedText + * @return + */ public int getAutoQueryTypes(JTextComponent component, String typedText) { + // XXX: Check if "enable/disable" autocomplete is setting. See NETBEANS-188 + // If "." has not been typed then acceptable to start checking for options. if (!".".equals(typedText)) { // NOI18N return 0; } + // check typed text if dot is present at the selected offset if (!isDotAtOffset(component, component.getSelectionStart() - 1)) { return 0; } + + // check if there is a DB connection DatabaseConnection dbconn = findDBConn(component); if (dbconn == null) { String message = NbBundle.getMessage(SQLCompletionProvider.class, "MSG_NoDatabaseConnection"); StatusDisplayer.getDefault().setStatusText(message); - return 0; + SQLExecutionBaseAction.notifyNoDatabaseConnection(); } - if (dbconn.getJDBCConnection() == null) { + + // check and notify if DB connection is inactive + if (dbconn != null && dbconn.getJDBCConnection() == null) { String message = NbBundle.getMessage(SQLCompletionProvider.class, "MSG_NotConnected"); StatusDisplayer.getDefault().setStatusText(message); - return 0; - } + SQLExecutionBaseAction.notifyNoDatabaseConnection(); + // XXX: Maybe add content specific "fixs" + } return COMPLETION_QUERY_TYPE; } @@ -101,24 +122,35 @@ private static Lookup findContext(JTextComponent component) { return null; } + /** + * For given component object's doc model, determine if given offset + * has applicable dot delimiter token + * @param component + * @param offset + * @return + */ private static boolean isDotAtOffset(JTextComponent component, final int offset) { final Document doc = component.getDocument(); final boolean[] result = { false }; - doc.render(new Runnable() { - public void run() { - TokenSequence seq = getSQLTokenSequence(doc); - if (seq == null) { - return; - } - seq.move(offset); - if (!seq.moveNext() && !seq.movePrevious()) { - return; - } - if (seq.offset() != offset) { - return; - } - result[0] = (seq.token().id() == SQLTokenId.DOT); + // trigger internal model rendering based on SQL tokens + doc.render(() -> { + TokenSequence seq = getSQLTokenSequence(doc); + // no SQL tokens found; done checking + if (seq == null) { + return; + } + // in the sequence move to provided offset location + seq.move(offset); + // no next or previous so done looking for tokens; done checking + if (!seq.moveNext() && !seq.movePrevious()) { + return; + } + // if offset not consistent with sequence offset done checking + if (seq.offset() != offset) { + return; } + // confirm token ID is applicable SQL "dot" token + result[0] = (seq.token().id() == SQLTokenId.DOT); }); return result[0]; } diff --git a/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionQuery.java b/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionQuery.java index 57fd2a334696..51a47122dde4 100644 --- a/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionQuery.java +++ b/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionQuery.java @@ -57,6 +57,7 @@ import org.netbeans.modules.db.sql.analyzer.SQLStatementKind; import org.netbeans.modules.db.sql.analyzer.UpdateStatement; import org.netbeans.modules.db.sql.editor.api.completion.SQLCompletionResultSet; +import org.netbeans.modules.db.sql.editor.ui.actions.SQLExecutionBaseAction; import org.netbeans.modules.db.sql.lexer.SQLTokenId; import org.netbeans.spi.editor.completion.CompletionResultSet; import org.netbeans.spi.editor.completion.support.AsyncCompletionQuery; @@ -120,23 +121,42 @@ public void query(SQLCompletionResultSet resultSet, SQLCompletionEnv newEnv) { private void doQuery(final SQLCompletionEnv newEnv) { try { - DBConnMetadataModelManager.get(dbconn).runReadAction(new Action() { - @Override - public void run(Metadata metadata) { - Connection conn = dbconn.getJDBCConnection(); - if (conn == null) { - return; - } - Quoter quoter = null; - try { - DatabaseMetaData dmd = conn.getMetaData(); - quoter = SQLIdentifiers.createQuoter(dmd); - } catch (SQLException e) { - throw new MetadataException(e); + // DB Connection available + if (dbconn != null) { + // DB connection present so + DBConnMetadataModelManager.get(dbconn).runReadAction(new Action() { + @Override + public void run(Metadata metadata) { + Connection conn = null; + if (dbconn != null) { + conn = dbconn.getJDBCConnection(); + } + Quoter quoter = null; + try { + /* if connection available allow for bb meta data + and quoter to help in auto completion */ + if (conn != null) { + // get Database meta data + DatabaseMetaData dmd = conn.getMetaData(); + quoter = SQLIdentifiers.createQuoter(dmd); + } + } catch (SQLException e) { + throw new MetadataException(e); + } + /* if quoter available then allow for query for + auto completion to occur, else avoid this sort of + activities when quoter/connection not available*/ + if (quoter != null) { + doQuery(newEnv, metadata, quoter); + } else { + return; + } } - doQuery(newEnv, metadata, quoter); - } - }); + }); + } else { + // No DB Connection established presently + doQuery(newEnv, metadata, quoter); + } } catch (MetadataModelException e) { reportError(e); } @@ -149,33 +169,49 @@ SQLCompletionItems doQuery(SQLCompletionEnv env, Metadata metadata, Quoter quote this.quoter = quoter; anchorOffset = -1; substitutionOffset = 0; + + // address empty env so add basic keywords items = new SQLCompletionItems(quoter, env.getSubstitutionHandler()); + if (env.getTokenSequence().isEmpty()) { completeKeyword("SELECT", "INSERT", "DELETE", "DROP", "UPDATE", "CREATE"); //NOI18N return items; } - statement = SQLStatementAnalyzer.analyze(env.getTokenSequence(), quoter); + + // not empty so address possible statement cases + statement = SQLStatementAnalyzer.analyze(env.getTokenSequence(), quoter); + + // unable to find relevant case so include basic keywords items if (statement == null) { completeKeyword("SELECT", "INSERT", "DELETE", "DROP", "UPDATE", "CREATE"); //NOI18N return items; } + + // found a create case so update to include create case items if (statement.getKind() == SQLStatementKind.CREATE && ((CreateStatement) statement).hasBody()) { completeCreateBody(); return items; } + + // checking context of offset to see if applicable; if not then set to basics context = statement.getContextAtOffset(env.getCaretOffset()); if (context == null) { completeKeyword("SELECT", "INSERT", "DELETE", "DROP", "UPDATE", "CREATE"); //NOI18N return items; } + + // from context look for identifiers; if none then return current context ident = findIdentifier(); if (ident == null) { completeKeyword(context); return items; } + + // from ident anchorif identifier identified then determine kin anchorOffset = ident.anchorOffset; substitutionOffset = ident.substitutionOffset; SQLStatementKind kind = statement.getKind(); + switch (kind) { case SELECT: completeSelect(); @@ -474,40 +510,46 @@ private void completeColumnWithDefinedTuple(Identifier ident) { } } - /** Adds columns, tuples, schemas and catalogs according to given identifier. */ + /** + * Adds columns, tuples, schema and catalogs according to given identifier. + */ private void completeColumnSimpleIdent(String typedPrefix, boolean quoted) { if (tablesClause != null && !(tablesClause.getUnaliasedTableNames().isEmpty() && tablesClause.getAliasedTableNames().isEmpty())) { completeSimpleIdentBasedOnFromClause(typedPrefix, quoted); } else { - Schema defaultSchema = metadata.getDefaultSchema(); - if (defaultSchema != null) { - // All columns in default schema, but only if a prefix has been typed, otherwise there - // would be too many columns. - if (typedPrefix != null) { - for (Table table : defaultSchema.getTables()) { - items.addColumns(table, typedPrefix, quoted, substitutionOffset); + // have database metadata to populate + if (metadata != null) { + Schema defaultSchema = metadata.getDefaultSchema(); + if (defaultSchema != null) { + // All columns in default schema, but only if a prefix has been typed, otherwise there + // would be too many columns. + if (typedPrefix != null) { + for (Table table : defaultSchema.getTables()) { + items.addColumns(table, typedPrefix, quoted, substitutionOffset); + } + if (includeViews) { + for (View view : defaultSchema.getViews()) { + items.addColumns(view, typedPrefix, quoted, substitutionOffset); + } + } } + // All tuples in default schema. + items.addTables(defaultSchema, null, typedPrefix, quoted, substitutionOffset); if (includeViews) { - for (View view : defaultSchema.getViews()) { - items.addColumns(view, typedPrefix, quoted, substitutionOffset); - } + items.addViews(defaultSchema, null, typedPrefix, quoted, substitutionOffset); } } - // All tuples in default schema. - items.addTables(defaultSchema, null, typedPrefix, quoted, substitutionOffset); - if (includeViews) { - items.addViews(defaultSchema, null, typedPrefix, quoted, substitutionOffset); - } + // All schemas. + Catalog defaultCatalog = metadata.getDefaultCatalog(); + items.addSchemas(defaultCatalog, null, typedPrefix, quoted, substitutionOffset); + // All catalogs. + items.addCatalogs(metadata, null, typedPrefix, quoted, substitutionOffset); } - // All schemas. - Catalog defaultCatalog = metadata.getDefaultCatalog(); - items.addSchemas(defaultCatalog, null, typedPrefix, quoted, substitutionOffset); - // All catalogs. - items.addCatalogs(metadata, null, typedPrefix, quoted, substitutionOffset); } } private void completeColumnWithTupleIfSimpleIdent(String typedPrefix, boolean quoted) { + if (metadata != null) { Schema defaultSchema = metadata.getDefaultSchema(); if (defaultSchema != null) { // All columns in default schema, but only if a prefix has been typed, otherwise there @@ -534,6 +576,7 @@ private void completeColumnWithTupleIfSimpleIdent(String typedPrefix, boolean qu items.addSchemas(defaultCatalog, null, typedPrefix, quoted, substitutionOffset); // All catalogs. items.addCatalogs(metadata, null, typedPrefix, quoted, substitutionOffset); + } } private void completeColumnWithTupleIfQualIdent(QualIdent fullyTypedIdent, String lastPrefix, boolean quoted) { @@ -587,19 +630,22 @@ private void completeColumnQualIdent(QualIdent fullyTypedIdent, String lastPrefi /** Adds all tuples from default schema, all schemas from defaultcatalog * and all catalogs. */ private void completeTupleSimpleIdent(String typedPrefix, boolean quoted) { - Schema defaultSchema = metadata.getDefaultSchema(); - if (defaultSchema != null) { - // All tuples in default schema. - items.addTables(defaultSchema, null, typedPrefix, quoted, substitutionOffset); - if (includeViews) { - items.addViews(defaultSchema, null, typedPrefix, quoted, substitutionOffset); + // connection metadata available so add to available items + if (metadata != null ){ + Schema defaultSchema = metadata.getDefaultSchema(); + if (defaultSchema != null) { + // All tuples in default schema. + items.addTables(defaultSchema, null, typedPrefix, quoted, substitutionOffset); + if (includeViews) { + items.addViews(defaultSchema, null, typedPrefix, quoted, substitutionOffset); + } } + // All schemas. + Catalog defaultCatalog = metadata.getDefaultCatalog(); + items.addSchemas(defaultCatalog, null, typedPrefix, quoted, substitutionOffset); + // All catalogs. + items.addCatalogs(metadata, null, typedPrefix, quoted, substitutionOffset); } - // All schemas. - Catalog defaultCatalog = metadata.getDefaultCatalog(); - items.addSchemas(defaultCatalog, null, typedPrefix, quoted, substitutionOffset); - // All catalogs. - items.addCatalogs(metadata, null, typedPrefix, quoted, substitutionOffset); } /** Adds all tuples in schema get from fully qualified identifier or all @@ -643,6 +689,7 @@ private void completeSimpleIdentBasedOnFromClause(String typedPrefix, boolean qu items.addColumns(tuple, typedPrefix, quoted, substitutionOffset); } // Tuples from default schema, restricted to non-aliased tuple names in the FROM clause. + if (metadata != null ) { Schema defaultSchema = metadata.getDefaultSchema(); if (defaultSchema != null) { Set simpleTupleNames = new TreeSet(); @@ -674,6 +721,7 @@ private void completeSimpleIdentBasedOnFromClause(String typedPrefix, boolean qu Catalog defaultCatalog = metadata.getDefaultCatalog(); items.addSchemas(defaultCatalog, schemaNames, typedPrefix, quoted, substitutionOffset); items.addCatalogs(metadata, catalogNames, typedPrefix, quoted, substitutionOffset); + } } private void completeQualIdentBasedOnFromClause(QualIdent fullyTypedIdent, String lastPrefix, boolean quoted) { @@ -744,7 +792,7 @@ private void completeCatalog(Catalog catalog, String prefix, boolean quoted) { } private Catalog resolveCatalog(QualIdent catalogName) { - if (catalogName.isSimple()) { + if (metadata != null && catalogName.isSimple()) { return metadata.getCatalog(catalogName.getSimpleName()); } return null; @@ -752,23 +800,25 @@ private Catalog resolveCatalog(QualIdent catalogName) { private Schema resolveSchema(QualIdent schemaName) { Schema schema = null; - switch (schemaName.size()) { - case 1: - Catalog catalog = metadata.getDefaultCatalog(); - schema = catalog.getSchema(schemaName.getSimpleName()); - break; - case 2: - catalog = metadata.getCatalog(schemaName.getFirstQualifier()); - if (catalog != null) { + if (metadata != null) { + switch (schemaName.size()) { + case 1: + Catalog catalog = metadata.getDefaultCatalog(); schema = catalog.getSchema(schemaName.getSimpleName()); - } - break; + break; + case 2: + catalog = metadata.getCatalog(schemaName.getFirstQualifier()); + if (catalog != null) { + schema = catalog.getSchema(schemaName.getSimpleName()); + } + break; + } } return schema; } private Tuple resolveTuple(QualIdent tupleName) { - if (tupleName == null) { + if (tupleName == null || metadata == null) { return null; } Tuple tuple = null; @@ -918,7 +968,7 @@ private Identifier findIdentifier() { String part; int offset = caretOffset - seq.offset(); String tokenText = seq.token().text().toString(); - if (offset > 0 && offset < seq.token().length()) { + if (offset > 0 && offset < seq.token().length() && quoter != null) { String quoteString = quoter.getQuoteString(); if (tokenText.startsWith(quoteString) && tokenText.endsWith(quoteString) && offset == tokenText.length() - 1) { // identifier inside closed quotes and cursor before ending quote ("foo|") @@ -978,6 +1028,10 @@ private Identifier findIdentifier() { } /** + * Used to create an Identifier based on current parts of the expression so far, + * if the details are "Complete" and the offset of given prefix involved. + * + * @param parts the list of potentail * @param lastPrefixOffset the offset of the last prefix in the identifier, or * if no such prefix, the caret offset. * @return @@ -993,30 +1047,32 @@ private Identifier createIdentifier(List parts, boolean incomplete, int } // Fine, nothing was typed. } else { - if (!incomplete) { - lastPrefix = parts.remove(parts.size() - 1); - String quoteString = quoter.getQuoteString(); - if (quoteString.length() > 0 && lastPrefix.startsWith(quoteString)) { - if (lastPrefix.endsWith(quoteString) && lastPrefix.length() > quoteString.length()) { - // User typed '"foo"."bar"|', can't complete that. + if (quoter != null ) { + if (!incomplete) { + lastPrefix = parts.remove(parts.size() - 1); + String quoteString = quoter.getQuoteString(); + if (quoteString.length() > 0 && lastPrefix.startsWith(quoteString)) { + if (lastPrefix.endsWith(quoteString) && lastPrefix.length() > quoteString.length()) { + // User typed '"foo"."bar"|', can't complete that. + return null; + } + int lastPrefixLength = lastPrefix.length(); + lastPrefix = quoter.unquote(lastPrefix); + lastPrefixOffset = lastPrefixOffset + (lastPrefixLength - lastPrefix.length()); + quoted = true; + } else if (quoteString.length() > 0 && lastPrefix.endsWith(quoteString)) { + // User typed '"foo".bar"|', can't complete. + return null; + } + } + for (int i = 0; i < parts.size(); i++) { + String unquoted = quoter.unquote(parts.get(i)); + if (unquoted.length() == 0) { + // User typed something like '"foo".""."bar|'. return null; } - int lastPrefixLength = lastPrefix.length(); - lastPrefix = quoter.unquote(lastPrefix); - lastPrefixOffset = lastPrefixOffset + (lastPrefixLength - lastPrefix.length()); - quoted = true; - } else if (quoteString.length() > 0 && lastPrefix.endsWith(quoteString)) { - // User typed '"foo".bar"|', can't complete. - return null; - } - } - for (int i = 0; i < parts.size(); i++) { - String unquoted = quoter.unquote(parts.get(i)); - if (unquoted.length() == 0) { - // User typed something like '"foo".""."bar|'. - return null; + parts.set(i, unquoted); } - parts.set(i, unquoted); } } return new Identifier(new QualIdent(parts), lastPrefix, quoted, lastPrefixOffset, substOffset); From 0bcef3fd0f20e583567fb66129bf79b1bcdddcca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Bl=C3=A4sing?= Date: Sat, 4 Dec 2021 18:51:14 +0100 Subject: [PATCH 2/2] [NETBEANS-189][NETBEANS-5831] Introduce an identifier quoter working without DMD and reduce dialogs --- .../db/sql/analyzer/SQLStatementAnalyzer.java | 7 +- .../completion/SQLCompletionProvider.java | 14 +- .../editor/completion/SQLCompletionQuery.java | 209 +++++++++--------- ide/db/apichanges.xml | 17 ++ ide/db/nbproject/project.properties | 2 +- .../api/db/sql/support/SQLIdentifiers.java | 69 +++++- .../db/sql/support/SQLIdentifiersTest.java | 48 ++++ 7 files changed, 244 insertions(+), 122 deletions(-) create mode 100644 ide/db/test/unit/src/org/netbeans/api/db/sql/support/SQLIdentifiersTest.java diff --git a/ide/db.sql.editor/src/org/netbeans/modules/db/sql/analyzer/SQLStatementAnalyzer.java b/ide/db.sql.editor/src/org/netbeans/modules/db/sql/analyzer/SQLStatementAnalyzer.java index 155cc61f8100..ab9214c13966 100644 --- a/ide/db.sql.editor/src/org/netbeans/modules/db/sql/analyzer/SQLStatementAnalyzer.java +++ b/ide/db.sql.editor/src/org/netbeans/modules/db/sql/analyzer/SQLStatementAnalyzer.java @@ -34,6 +34,7 @@ import org.netbeans.modules.db.sql.analyzer.SQLStatement.Context; import org.netbeans.modules.db.sql.editor.StringUtils; import org.netbeans.modules.db.sql.lexer.SQLTokenId; +import org.openide.util.Parameters; /** * * @author Jiri Rechtacek, Jiri Skrivanek @@ -48,6 +49,8 @@ public class SQLStatementAnalyzer { protected final List subqueries = new ArrayList(); protected SQLStatementAnalyzer(TokenSequence seq, Quoter quoter) { + Parameters.notNull("seq", seq); + Parameters.notNull("quoter", quoter); this.seq = seq; this.quoter = quoter; } @@ -238,10 +241,6 @@ protected QualIdent parseIdentifier() { } protected String getUnquotedIdentifier() { - // quoter unavailable so returnas is - if (quoter == null) { - return seq.token().text().toString(); - } return quoter.unquote(seq.token().text().toString()); } diff --git a/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionProvider.java b/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionProvider.java index 78fbb144a626..9753981b9ebe 100644 --- a/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionProvider.java +++ b/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionProvider.java @@ -26,7 +26,6 @@ import org.netbeans.api.lexer.TokenHierarchy; import org.netbeans.api.lexer.TokenSequence; import org.netbeans.modules.db.api.sql.execute.SQLExecution; -import org.netbeans.modules.db.sql.editor.ui.actions.SQLExecutionBaseAction; import org.netbeans.modules.db.sql.lexer.SQLTokenId; import org.netbeans.spi.editor.completion.CompletionProvider; import org.netbeans.spi.editor.completion.CompletionTask; @@ -44,14 +43,14 @@ public class SQLCompletionProvider implements CompletionProvider { public CompletionTask createTask(int queryType, JTextComponent component) { if (queryType == CompletionProvider.COMPLETION_QUERY_TYPE || queryType == CompletionProvider.COMPLETION_ALL_QUERY_TYPE) { - + /* to support DB related completion tasks (i.e. auto populating table names or db columns for given schema) check for connection */ DatabaseConnection dbconn = findDBConn(component); return new AsyncCompletionTask(new SQLCompletionQuery(dbconn), component); } - + // not a completion query type so return nothing return null; } @@ -69,6 +68,7 @@ public CompletionTask createTask(int queryType, JTextComponent component) { * @param typedText * @return */ + @Override public int getAutoQueryTypes(JTextComponent component, String typedText) { // XXX: Check if "enable/disable" autocomplete is setting. See NETBEANS-188 // If "." has not been typed then acceptable to start checking for options. @@ -79,22 +79,20 @@ public int getAutoQueryTypes(JTextComponent component, String typedText) { if (!isDotAtOffset(component, component.getSelectionStart() - 1)) { return 0; } - + // check if there is a DB connection DatabaseConnection dbconn = findDBConn(component); if (dbconn == null) { String message = NbBundle.getMessage(SQLCompletionProvider.class, "MSG_NoDatabaseConnection"); StatusDisplayer.getDefault().setStatusText(message); - SQLExecutionBaseAction.notifyNoDatabaseConnection(); } // check and notify if DB connection is inactive if (dbconn != null && dbconn.getJDBCConnection() == null) { String message = NbBundle.getMessage(SQLCompletionProvider.class, "MSG_NotConnected"); StatusDisplayer.getDefault().setStatusText(message); - SQLExecutionBaseAction.notifyNoDatabaseConnection(); - // XXX: Maybe add content specific "fixs" - } + } + return COMPLETION_QUERY_TYPE; } diff --git a/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionQuery.java b/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionQuery.java index 51a47122dde4..3bd5979f4524 100644 --- a/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionQuery.java +++ b/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionQuery.java @@ -57,7 +57,6 @@ import org.netbeans.modules.db.sql.analyzer.SQLStatementKind; import org.netbeans.modules.db.sql.analyzer.UpdateStatement; import org.netbeans.modules.db.sql.editor.api.completion.SQLCompletionResultSet; -import org.netbeans.modules.db.sql.editor.ui.actions.SQLExecutionBaseAction; import org.netbeans.modules.db.sql.lexer.SQLTokenId; import org.netbeans.spi.editor.completion.CompletionResultSet; import org.netbeans.spi.editor.completion.support.AsyncCompletionQuery; @@ -123,17 +122,17 @@ private void doQuery(final SQLCompletionEnv newEnv) { try { // DB Connection available if (dbconn != null) { - // DB connection present so + // DB connection present so DBConnMetadataModelManager.get(dbconn).runReadAction(new Action() { @Override public void run(Metadata metadata) { Connection conn = null; if (dbconn != null) { conn = dbconn.getJDBCConnection(); - } + } Quoter quoter = null; try { - /* if connection available allow for bb meta data + /* if connection available allow for bb meta data and quoter to help in auto completion */ if (conn != null) { // get Database meta data @@ -143,19 +142,15 @@ public void run(Metadata metadata) { } catch (SQLException e) { throw new MetadataException(e); } - /* if quoter available then allow for query for + /* if quoter available then allow for query for auto completion to occur, else avoid this sort of activities when quoter/connection not available*/ - if (quoter != null) { - doQuery(newEnv, metadata, quoter); - } else { - return; - } + doQuery(newEnv, metadata, quoter); } }); - } else { - // No DB Connection established presently - doQuery(newEnv, metadata, quoter); + } else { + // No DB Connection established presently + doQuery(newEnv, metadata, quoter == null ? SQLIdentifiers.createQuoter(null) : quoter); } } catch (MetadataModelException e) { reportError(e); @@ -169,10 +164,10 @@ SQLCompletionItems doQuery(SQLCompletionEnv env, Metadata metadata, Quoter quote this.quoter = quoter; anchorOffset = -1; substitutionOffset = 0; - + // address empty env so add basic keywords items = new SQLCompletionItems(quoter, env.getSubstitutionHandler()); - + if (env.getTokenSequence().isEmpty()) { completeKeyword("SELECT", "INSERT", "DELETE", "DROP", "UPDATE", "CREATE"); //NOI18N return items; @@ -180,38 +175,38 @@ SQLCompletionItems doQuery(SQLCompletionEnv env, Metadata metadata, Quoter quote // not empty so address possible statement cases statement = SQLStatementAnalyzer.analyze(env.getTokenSequence(), quoter); - + // unable to find relevant case so include basic keywords items if (statement == null) { completeKeyword("SELECT", "INSERT", "DELETE", "DROP", "UPDATE", "CREATE"); //NOI18N return items; } - + // found a create case so update to include create case items if (statement.getKind() == SQLStatementKind.CREATE && ((CreateStatement) statement).hasBody()) { completeCreateBody(); return items; } - + // checking context of offset to see if applicable; if not then set to basics context = statement.getContextAtOffset(env.getCaretOffset()); if (context == null) { completeKeyword("SELECT", "INSERT", "DELETE", "DROP", "UPDATE", "CREATE"); //NOI18N return items; } - + // from context look for identifiers; if none then return current context ident = findIdentifier(); if (ident == null) { completeKeyword(context); return items; } - + // from ident anchorif identifier identified then determine kin anchorOffset = ident.anchorOffset; substitutionOffset = ident.substitutionOffset; SQLStatementKind kind = statement.getKind(); - + switch (kind) { case SELECT: completeSelect(); @@ -254,7 +249,7 @@ private void completeCreate() { completeSelect(); } } - + private void completeSelect() { SelectStatement selectStatement = (SelectStatement) statement; tablesClause = selectStatement.getTablesInEffect(env.getCaretOffset()); @@ -550,32 +545,32 @@ private void completeColumnSimpleIdent(String typedPrefix, boolean quoted) { private void completeColumnWithTupleIfSimpleIdent(String typedPrefix, boolean quoted) { if (metadata != null) { - Schema defaultSchema = metadata.getDefaultSchema(); - if (defaultSchema != null) { - // All columns in default schema, but only if a prefix has been typed, otherwise there - // would be too many columns. - if (typedPrefix != null) { - for (Table table : defaultSchema.getTables()) { - items.addColumnsWithTupleName(table, null, typedPrefix, quoted, substitutionOffset - 1); - } - if (includeViews) { - for (View view : defaultSchema.getViews()) { - items.addColumnsWithTupleName(view, null, typedPrefix, quoted, substitutionOffset - 1); + Schema defaultSchema = metadata.getDefaultSchema(); + if (defaultSchema != null) { + // All columns in default schema, but only if a prefix has been typed, otherwise there + // would be too many columns. + if (typedPrefix != null) { + for (Table table : defaultSchema.getTables()) { + items.addColumnsWithTupleName(table, null, typedPrefix, quoted, substitutionOffset - 1); + } + if (includeViews) { + for (View view : defaultSchema.getViews()) { + items.addColumnsWithTupleName(view, null, typedPrefix, quoted, substitutionOffset - 1); + } + } + } else { + // All tuples in default schema. + items.addTablesAtInsertInto(defaultSchema, null, null, typedPrefix, quoted, substitutionOffset - 1); + if (includeViews) { + items.addViewsAtInsertInto(defaultSchema, null, null, typedPrefix, quoted, substitutionOffset - 1); } - } - } else { - // All tuples in default schema. - items.addTablesAtInsertInto (defaultSchema, null, null, typedPrefix, quoted, substitutionOffset - 1); - if (includeViews) { - items.addViewsAtInsertInto(defaultSchema, null, null, typedPrefix, quoted, substitutionOffset - 1); } } - } - // All schemas. - Catalog defaultCatalog = metadata.getDefaultCatalog(); - items.addSchemas(defaultCatalog, null, typedPrefix, quoted, substitutionOffset); - // All catalogs. - items.addCatalogs(metadata, null, typedPrefix, quoted, substitutionOffset); + // All schemas. + Catalog defaultCatalog = metadata.getDefaultCatalog(); + items.addSchemas(defaultCatalog, null, typedPrefix, quoted, substitutionOffset); + // All catalogs. + items.addCatalogs(metadata, null, typedPrefix, quoted, substitutionOffset); } } @@ -670,8 +665,8 @@ private void completeSimpleIdentBasedOnFromClause(String typedPrefix, boolean qu assert tablesClause != null; Set tupleNames = tablesClause.getUnaliasedTableNames(); Set tuples = resolveTuples(tupleNames); - Set allTupleNames = new TreeSet(tupleNames); - Set allTuples = new LinkedHashSet(tuples); + Set allTupleNames = new TreeSet<>(tupleNames); + Set allTuples = new LinkedHashSet<>(tuples); Map aliases = tablesClause.getAliasedTableNames(); for (Entry entry : aliases.entrySet()) { QualIdent tupleName = entry.getValue(); @@ -682,45 +677,45 @@ private void completeSimpleIdentBasedOnFromClause(String typedPrefix, boolean qu } } // Aliases. - Map sortedAliases = new TreeMap(aliases); + Map sortedAliases = new TreeMap<>(aliases); items.addAliases(sortedAliases, typedPrefix, quoted, substitutionOffset); // Columns from aliased and non-aliased tuples in the FROM clause. for (Tuple tuple : allTuples) { items.addColumns(tuple, typedPrefix, quoted, substitutionOffset); } // Tuples from default schema, restricted to non-aliased tuple names in the FROM clause. - if (metadata != null ) { - Schema defaultSchema = metadata.getDefaultSchema(); - if (defaultSchema != null) { - Set simpleTupleNames = new TreeSet(); - for (Tuple tuple : tuples) { - if (tuple.getParent().isDefault()) { - simpleTupleNames.add(tuple.getName()); + if (metadata != null) { + Schema defaultSchema = metadata.getDefaultSchema(); + if (defaultSchema != null) { + Set simpleTupleNames = new TreeSet<>(); + for (Tuple tuple : tuples) { + if (tuple.getParent().isDefault()) { + simpleTupleNames.add(tuple.getName()); + } + } + items.addTables(defaultSchema, simpleTupleNames, typedPrefix, quoted, substitutionOffset); + if (includeViews) { + items.addViews(defaultSchema, simpleTupleNames, typedPrefix, quoted, substitutionOffset); } } - items.addTables(defaultSchema, simpleTupleNames, typedPrefix, quoted, substitutionOffset); - if (includeViews) { - items.addViews(defaultSchema, simpleTupleNames, typedPrefix, quoted, substitutionOffset); - } - } - // Schemas from default catalog other than the default schema, based on non-aliased tuple names in the FROM clause. - // Catalogs based on non-aliased tuples names in the FROM clause. - Set schemaNames = new TreeSet(); - Set catalogNames = new TreeSet(); - for (Tuple tuple : tuples) { - Schema schema = tuple.getParent(); - Catalog catalog = schema.getParent(); - if (!schema.isDefault() && !schema.isSynthetic() && catalog.isDefault()) { - schemaNames.add(schema.getName()); - } - if (!catalog.isDefault()) { - catalogNames.add(catalog.getName()); - } + // Schemas from default catalog other than the default schema, based on non-aliased tuple names in the FROM clause. + // Catalogs based on non-aliased tuples names in the FROM clause. + Set schemaNames = new TreeSet<>(); + Set catalogNames = new TreeSet<>(); + for (Tuple tuple : tuples) { + Schema schema = tuple.getParent(); + Catalog catalog = schema.getParent(); + if (!schema.isDefault() && !schema.isSynthetic() && catalog.isDefault()) { + schemaNames.add(schema.getName()); + } + if (!catalog.isDefault()) { + catalogNames.add(catalog.getName()); + } - } - Catalog defaultCatalog = metadata.getDefaultCatalog(); - items.addSchemas(defaultCatalog, schemaNames, typedPrefix, quoted, substitutionOffset); - items.addCatalogs(metadata, catalogNames, typedPrefix, quoted, substitutionOffset); + } + Catalog defaultCatalog = metadata.getDefaultCatalog(); + items.addSchemas(defaultCatalog, schemaNames, typedPrefix, quoted, substitutionOffset); + items.addCatalogs(metadata, catalogNames, typedPrefix, quoted, substitutionOffset); } } @@ -746,7 +741,7 @@ private void completeQualIdentBasedOnFromClause(QualIdent fullyTypedIdent, Strin // Now assume fullyTypedIdent is the name of a schema in the default catalog. Schema schema = resolveSchema(fullyTypedIdent); if (schema != null) { - Set tupleNames = new TreeSet(); + Set tupleNames = new TreeSet<>(); for (Tuple tuple : tuples) { if (tuple.getParent().equals(schema)) { tupleNames.add(tuple.getName()); @@ -760,8 +755,8 @@ private void completeQualIdentBasedOnFromClause(QualIdent fullyTypedIdent, Strin // Now assume fullyTypedIdent is the name of a catalog. Catalog catalog = resolveCatalog(fullyTypedIdent); if (catalog != null) { - Set syntheticSchemaTupleNames = new TreeSet(); - Set schemaNames = new TreeSet(); + Set syntheticSchemaTupleNames = new TreeSet<>(); + Set schemaNames = new TreeSet<>(); for (Tuple tuple : tuples) { schema = tuple.getParent(); if (schema.getParent().equals(catalog)) { @@ -821,7 +816,7 @@ private Tuple resolveTuple(QualIdent tupleName) { if (tupleName == null || metadata == null) { return null; } - Tuple tuple = null; + Tuple tuple; switch (tupleName.size()) { case 1: Schema schema = metadata.getDefaultSchema(); @@ -874,7 +869,7 @@ private Tuple getTuple(Schema schema, QualIdent tupleName) { } private Set resolveTuples(Set tupleNames) { - Set result = new LinkedHashSet(tupleNames.size()); + Set result = new LinkedHashSet<>(tupleNames.size()); for (QualIdent tupleName : tupleNames) { Tuple tuple = resolveTuple(tupleName); if (tuple != null) { @@ -890,7 +885,7 @@ private Set resolveTuples(Set tupleNames) { private Symbol findPrefix() { TokenSequence seq = env.getTokenSequence(); int caretOffset = env.getCaretOffset(); - String prefix = null; + String prefix; if (seq.move(caretOffset) > 0) { // Not on token boundary. if (!seq.moveNext() && !seq.movePrevious()) { @@ -923,7 +918,7 @@ private Symbol findPrefix() { private Identifier findIdentifier() { TokenSequence seq = env.getTokenSequence(); int caretOffset = env.getCaretOffset(); - final List parts = new ArrayList(); + final List parts = new ArrayList<>(); if (seq.move(caretOffset) > 0) { // Not on token boundary. if (!seq.moveNext() && !seq.movePrevious()) { @@ -968,7 +963,7 @@ private Identifier findIdentifier() { String part; int offset = caretOffset - seq.offset(); String tokenText = seq.token().text().toString(); - if (offset > 0 && offset < seq.token().length() && quoter != null) { + if (offset > 0 && offset < seq.token().length()) { String quoteString = quoter.getQuoteString(); if (tokenText.startsWith(quoteString) && tokenText.endsWith(quoteString) && offset == tokenText.length() - 1) { // identifier inside closed quotes and cursor before ending quote ("foo|") @@ -1047,32 +1042,30 @@ private Identifier createIdentifier(List parts, boolean incomplete, int } // Fine, nothing was typed. } else { - if (quoter != null ) { - if (!incomplete) { - lastPrefix = parts.remove(parts.size() - 1); - String quoteString = quoter.getQuoteString(); - if (quoteString.length() > 0 && lastPrefix.startsWith(quoteString)) { - if (lastPrefix.endsWith(quoteString) && lastPrefix.length() > quoteString.length()) { - // User typed '"foo"."bar"|', can't complete that. - return null; - } - int lastPrefixLength = lastPrefix.length(); - lastPrefix = quoter.unquote(lastPrefix); - lastPrefixOffset = lastPrefixOffset + (lastPrefixLength - lastPrefix.length()); - quoted = true; - } else if (quoteString.length() > 0 && lastPrefix.endsWith(quoteString)) { - // User typed '"foo".bar"|', can't complete. - return null; - } - } - for (int i = 0; i < parts.size(); i++) { - String unquoted = quoter.unquote(parts.get(i)); - if (unquoted.length() == 0) { - // User typed something like '"foo".""."bar|'. + if (!incomplete) { + lastPrefix = parts.remove(parts.size() - 1); + String quoteString = quoter.getQuoteString(); + if (quoteString.length() > 0 && lastPrefix.startsWith(quoteString)) { + if (lastPrefix.endsWith(quoteString) && lastPrefix.length() > quoteString.length()) { + // User typed '"foo"."bar"|', can't complete that. return null; } - parts.set(i, unquoted); + int lastPrefixLength = lastPrefix.length(); + lastPrefix = quoter.unquote(lastPrefix); + lastPrefixOffset = lastPrefixOffset + (lastPrefixLength - lastPrefix.length()); + quoted = true; + } else if (quoteString.length() > 0 && lastPrefix.endsWith(quoteString)) { + // User typed '"foo".bar"|', can't complete. + return null; + } + } + for (int i = 0; i < parts.size(); i++) { + String unquoted = quoter.unquote(parts.get(i)); + if (unquoted.length() == 0) { + // User typed something like '"foo".""."bar|'. + return null; } + parts.set(i, unquoted); } } return new Identifier(new QualIdent(parts), lastPrefix, quoted, lastPrefixOffset, substOffset); diff --git a/ide/db/apichanges.xml b/ide/db/apichanges.xml index 90fcf69ee597..a6b649074a10 100644 --- a/ide/db/apichanges.xml +++ b/ide/db/apichanges.xml @@ -83,6 +83,23 @@ is the proper place. + + + Return a fallback quoter if no DatabaseMetaData is available. + + + + + + SQLIdentifiers now returns a fallback + Quoter if there is no DatabaseMetaData. + The fallback quoter supports unquoting most common identifier + quoting formats, uses SQL-99 quotes for quoting and quotes all + identifiers, that don't start with an ascii character or + contain ascii non-characters or non-numbers. + + + Concept of preferred connection introduced diff --git a/ide/db/nbproject/project.properties b/ide/db/nbproject/project.properties index f3dd9544cc99..4f62a7a94717 100644 --- a/ide/db/nbproject/project.properties +++ b/ide/db/nbproject/project.properties @@ -20,7 +20,7 @@ javac.source=1.8 javadoc.arch=${basedir}/arch.xml javadoc.apichanges=${basedir}/apichanges.xml -spec.version.base=1.83.0 +spec.version.base=1.84.0 extra.module.files=modules/ext/ddl.jar diff --git a/ide/db/src/org/netbeans/api/db/sql/support/SQLIdentifiers.java b/ide/db/src/org/netbeans/api/db/sql/support/SQLIdentifiers.java index fa50189d757f..185591afe017 100644 --- a/ide/db/src/org/netbeans/api/db/sql/support/SQLIdentifiers.java +++ b/ide/db/src/org/netbeans/api/db/sql/support/SQLIdentifiers.java @@ -24,6 +24,7 @@ import java.sql.SQLException; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Pattern; import org.openide.util.Lookup; import org.openide.util.Parameters; @@ -48,7 +49,11 @@ private SQLIdentifiers() { * @return a {@code Quoter} instance. */ public static Quoter createQuoter(DatabaseMetaData dbmd) { - return new DatabaseMetaDataQuoter(dbmd); + if(dbmd == null) { + return new FallbackQuoter(); + } else { + return new DatabaseMetaDataQuoter(dbmd); + } } @@ -365,4 +370,66 @@ private static int getCaseRule(DatabaseMetaData dbmd) { } } + private static class FallbackQuoter extends Quoter { + + private static final Pattern ASCII_IDENTIFIER = Pattern.compile("[a-zA-z][a-zA-Z0-9_]+"); + + public FallbackQuoter() { + super("\""); + } + + @Override + boolean alreadyQuoted(String identifier) { + Parameters.notNull("identifier", identifier); + return getEndQuoteString(identifier) != null; + } + + @Override + public String quoteIfNeeded(String identifier) { + Parameters.notNull("identifier", identifier); + if (!alreadyQuoted(identifier) && !ASCII_IDENTIFIER.matcher(identifier).matches()) { + return doQuote(identifier); + } else { + return identifier; + } + } + + @Override + public String quoteAlways(String identifier) { + Parameters.notNull("identifier", identifier); + if (!alreadyQuoted(identifier)) { + return doQuote(identifier); + } else { + return identifier; + } + } + + private String getEndQuoteString(String identifier) { + if (identifier.startsWith("\"") && identifier.endsWith("\"")) { + return "\""; + } else if (identifier.startsWith("`") && identifier.endsWith("`")) { + return "`"; + } else if (identifier.startsWith("[") && identifier.endsWith("]")) { + return "]"; + } + return null; + } + + @Override + public String unquote(String identifier) { + Parameters.notNull("identifier", identifier); + String workidentifier = identifier.trim(); + String endQuoteString = getEndQuoteString(identifier); + if(endQuoteString == null) { + return workidentifier; + } + + String result = workidentifier; + // Extract the contents of the quoted string + result = result.substring(1, result.length() - 1); + // remove potentially present quotes + result = result.replace(endQuoteString + endQuoteString, endQuoteString); + return result; + } + } } diff --git a/ide/db/test/unit/src/org/netbeans/api/db/sql/support/SQLIdentifiersTest.java b/ide/db/test/unit/src/org/netbeans/api/db/sql/support/SQLIdentifiersTest.java new file mode 100644 index 000000000000..7ce40ed3ed6f --- /dev/null +++ b/ide/db/test/unit/src/org/netbeans/api/db/sql/support/SQLIdentifiersTest.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.netbeans.api.db.sql.support; + +import org.junit.Test; +import org.netbeans.api.db.sql.support.SQLIdentifiers.Quoter; + +import static org.junit.Assert.*; + +public class SQLIdentifiersTest { + @Test + public void testQuoterFromNull() { + // Without DatabaseMetaData a fallback quoter is returned, that + // unquotes all popular identifier quotes (SQL-99, mssql and mysql) and + // quotes with SQL-99 quotes (") + Quoter quoter = SQLIdentifiers.createQuoter(null); + assertNotNull(quoter); + + assertEquals("hello\"", quoter.unquote("hello\"")); + assertEquals("\"hello", quoter.unquote("\"hello")); + assertEquals("hello", quoter.unquote("\"hello\"")); + assertEquals("hello`", quoter.unquote("hello`")); + assertEquals("`hello", quoter.unquote("`hello")); + assertEquals("hello", quoter.unquote("`hello`")); + assertEquals("hello]", quoter.unquote("hello]")); + assertEquals("[hello", quoter.unquote("[hello")); + assertEquals("hello", quoter.unquote("[hello]")); + assertEquals("hello", quoter.quoteIfNeeded("hello")); + assertEquals("\"hello world\"", quoter.quoteIfNeeded("hello world")); + assertEquals("\"hello\"", quoter.quoteAlways("hello")); + } +}