/* // Licensed to Julian Hyde under one or more contributor license // agreements. See the NOTICE file distributed with this work for // additional information regarding copyright ownership. // // Julian Hyde licenses this file to you under the Modified BSD License // (the "License"); you may not use this file except in compliance with // the License. You may obtain a copy of the License at: // // http://opensource.org/licenses/BSD-3-Clause */ package sqlline; import java.sql.DatabaseMetaData; import java.util.*; import java.util.stream.Collectors; import org.jline.reader.Candidate; import org.jline.reader.LineReader; import org.jline.reader.ParsedLine; import org.jline.reader.Parser; import org.jline.reader.impl.completer.StringsCompleter; /** * Suggests completions for SQL statements. */ class SqlCompleter extends StringsCompleter { private static final String ALLOWED_UPPER_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; private static final String ALLOWED_LOWER_CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789_"; private final SqlLine sqlLine; private final boolean skipMeta; SqlCompleter(SqlLine sqlLine, boolean skipMeta) { super(getCompletions(sqlLine, skipMeta)); this.sqlLine = sqlLine; this.skipMeta = skipMeta; } private static Candidate[] getCompletions(SqlLine sqlLine, boolean skipMeta) { Set<Candidate> completions = new TreeSet<>(); // now add the keywords from the current connection final DatabaseMetaData meta = sqlLine.getDatabaseConnection().meta; try { for (String sqlKeyWord: meta.getSQLKeywords().split(",")) { final String keyWord = sqlKeyWord.trim(); completions.add( new SqlLineCommandCompleter.SqlLineCandidate(sqlLine, keyWord, keyWord, null, sqlLine.loc("keyword"), null, null, true)); } } catch (Throwable t) { // ignore } try { for (String numericFunction: meta.getNumericFunctions().split(",")) { final String function = numericFunction.trim(); completions.add( new SqlLineCommandCompleter.SqlLineCandidate(sqlLine, function, function, null, sqlLine.loc("function"), null, null, false)); } } catch (Throwable t) { // ignore } try { for (String stringFunction: meta.getStringFunctions().split(",")) { final String function = stringFunction.trim(); completions.add( new SqlLineCommandCompleter.SqlLineCandidate(sqlLine, function, function, null, sqlLine.loc("function"), null, null, false)); } } catch (Throwable t) { // ignore } try { for (String systemFunction: meta.getSystemFunctions().split(",")) { final String function = systemFunction.trim(); completions.add( new SqlLineCommandCompleter.SqlLineCandidate(sqlLine, function, function, null, sqlLine.loc("function"), null, null, false)); } } catch (Throwable t) { // ignore } try { for (String timeDateFunction: meta.getTimeDateFunctions().split(",")) { final String function = timeDateFunction.trim(); completions.add( new SqlLineCommandCompleter.SqlLineCandidate(sqlLine, function, function, null, sqlLine.loc("function"), null, null, false)); } } catch (Throwable t) { // ignore } if (!skipMeta) { try { final Dialect dialect = sqlLine.getDialect(); Map<String, Map<String, Set<String>>> schema2tables = sqlLine.getDatabaseConnection() .getSchema(true).getSchema2tables(); for (String schemaName : schema2tables.keySet()) { // mariadb/mysql case of connection without specific db like // under user without grants to read any db if (schemaName == null) { continue; } String value = writeAsDialectSpecificValue(dialect, false, schemaName); completions.add( generateCandidate(schemaName, value, sqlLine, "schema", false)); } for (String tableName : schema2tables.values().stream() .flatMap(t -> t.keySet().stream()).collect(Collectors.toSet())) { String value = writeAsDialectSpecificValue(dialect, false, tableName); completions.add( generateCandidate(tableName, value, sqlLine, "table", false)); } } catch (Throwable t) { // ignore } } for (String keyWord : Dialect.DEFAULT_KEYWORD_SET) { completions.add( generateCandidate(keyWord, keyWord, sqlLine, "keyword", true)); } // set the Strings that will be completed return completions.toArray(new Candidate[0]); } @Override public void complete( LineReader reader, ParsedLine commandLine, List<Candidate> candidates) { String sql = commandLine.line().substring(0, commandLine.cursor()); SqlLineParser.SqlLineArgumentList argumentList = ((SqlLineParser) sqlLine.getLineReader().getParser()) .parseState(sql, sql.length(), Parser.ParseContext.UNSPECIFIED); final String supplierMsg = argumentList.getSupplier().get(); final char openQuote = sqlLine.getDialect().getOpenQuote(); if (argumentList.getState() == SqlLineParser.SqlParserState.MULTILINE_COMMENT || (argumentList.getState() == SqlLineParser.SqlParserState.QUOTED && ((openQuote == '"' && !supplierMsg.endsWith("dquote")) || (openQuote == '`' && !supplierMsg.endsWith("`"))))) { return; } if (!skipMeta) { Deque<String> lastWords = getSchemaTableColumn(argumentList.word()); candidates.addAll(getSchemaBasedCandidates(new ArrayDeque<>(lastWords))); candidates.addAll(getTableBasedCandidates(new ArrayDeque<>(lastWords))); } // suggest other candidates if not quoted // and previous word not finished with '.' if (argumentList.getState() != SqlLineParser.SqlParserState.QUOTED && ((argumentList.getState() != SqlLineParser.SqlParserState.SEMICOLON_REQUIRED && argumentList.getState() != SqlLineParser.SqlParserState.ROUND_BRACKET_BALANCE_FAILED) || sql.isEmpty() || sql.charAt(sql.length() - 1) != '.')) { candidates.addAll(this.candidates); } } private Collection<Candidate> getSchemaBasedCandidates( Deque<String> schemaTableColumn) { // schema + table + column == 3 if (schemaTableColumn.size() > 3) { return Collections.emptySet(); } Collection<Candidate> candidates = new ArrayList<>(); final Map<String, Map<String, Set<String>>> schema2tables = sqlLine.getDatabaseConnection().getSchema().getSchema2tables(); final String originalSchemaName = schemaTableColumn.pollFirst(); final Dialect dialect = sqlLine.getDialect(); final String schemaName = readAsDialectSpecificName(dialect, originalSchemaName); final boolean need2Quote = isOriginalNameStartedQuoted(dialect, originalSchemaName); if (schemaName == null || schema2tables.get(schemaName) == null) { for (String sName: schema2tables.keySet()) { // without quotes covered in getCompletions if (sName == null || !need2Quote) { // case without schema like select epms.* from sales.emps; // instead of select sales.epms.* from sales.emps; continue; } String value = writeAsDialectSpecificValue(dialect, true, sName); candidates.add( generateCandidate(sName, value, sqlLine, "schema", false)); } return candidates; } final String originalTableName = schemaTableColumn.pollFirst(); final String tableName = readAsDialectSpecificName(dialect, originalTableName); final boolean need2QuoteTableName = isOriginalNameStartedQuoted(dialect, originalTableName); if (tableName == null || !schema2tables.get(schemaName).containsKey(tableName)) { for (String tName: schema2tables.get(schemaName).keySet()) { String value = writeAsDialectSpecificValue(dialect, need2Quote, schemaName) + "." + writeAsDialectSpecificValue(dialect, need2QuoteTableName, tName); candidates.add( generateCandidate(tName, value, sqlLine, "table", true)); } } else { Collection<String> columnNames = sqlLine.getDatabaseConnection() .getSchema().getColumnNames(schemaName, tableName); String userWrittenColumnName = schemaTableColumn.pollFirst(); final boolean need2QuoteColumnName = isOriginalNameStartedQuoted(dialect, userWrittenColumnName); for (String columnName: columnNames) { String value = writeAsDialectSpecificValue(dialect, need2Quote, schemaName) + "." + writeAsDialectSpecificValue( dialect, need2QuoteTableName, tableName) + "." + writeAsDialectSpecificValue( dialect, need2QuoteColumnName, columnName); candidates.add( generateCandidate(columnName, value, sqlLine, "column", true)); } } return candidates; } private Collection<Candidate> getTableBasedCandidates( Deque<String> tableColumn) { // table + column == 2 if (tableColumn.size() > 2) { return Collections.emptySet(); } Collection<Candidate> candidates = new ArrayList<>(); final Map<String, Set<String>> tables2columns = new HashMap<>(); for (Map<String, Set<String>> map: sqlLine.getDatabaseConnection() .getSchema().getSchema2tables().values()) { for (Map.Entry<String, Set<String>> entry : map.entrySet()) { tables2columns.put(entry.getKey(), entry.getValue()); } } final Dialect dialect = sqlLine.getDialect(); final String originalTableName = tableColumn.pollFirst(); final String tableName = readAsDialectSpecificName(dialect, originalTableName); final boolean need2QuoteTableName = isOriginalNameStartedQuoted(dialect, originalTableName); if (!tables2columns.containsKey(tableName)) { for (String tName: tables2columns.keySet()) { // covered in getCompletions if (!need2QuoteTableName) { continue; } String value = writeAsDialectSpecificValue(dialect, true, tName); candidates.add( generateCandidate(tName, value, sqlLine, "table", false)); } return candidates; } Collection<String> columnNames = sqlLine.getDatabaseConnection() .getSchema().getColumnNames(null, tableName); String userWrittenColumnName = tableColumn.pollFirst(); final boolean need2QuoteColumnName = isOriginalNameStartedQuoted(dialect, userWrittenColumnName); for (String columnName: columnNames) { String value = writeAsDialectSpecificValue(dialect, need2QuoteTableName, tableName) + "." + writeAsDialectSpecificValue( dialect, need2QuoteColumnName, columnName); candidates.add( generateCandidate(columnName, value, sqlLine, "column", true)); } return candidates; } static Candidate generateCandidate( String sName, String value, SqlLine sqlLine, String descr, boolean complete) { return new Candidate(value, sName, null, sqlLine.loc(descr), "table".equalsIgnoreCase(descr) || "schema".equalsIgnoreCase(descr) ? "." : null, null, complete); } Deque<String> getSchemaTableColumn(String word) { if (word.length() == 0) { return new ArrayDeque<>(Collections.emptyList()); } Deque<String> wordList = new ArrayDeque<>(); Dialect dialect = sqlLine.getDialect(); String wordToCheck = addClosingSqlIdentifierIfRequired(dialect, word) ? word + dialect.getCloseQuote() : word; String[][] splitted = sqlLine.splitCompound(wordToCheck, true); if (splitted.length > 0) { for (String wordItem : splitted[0]) { if (wordItem.length() > 0) { wordList.addLast(wordItem); } else if (wordToCheck.charAt(wordToCheck.length() - 1) == dialect.getOpenQuote()) { wordList.addLast(String.valueOf(dialect.getOpenQuote())); } } } return wordList; } private boolean addClosingSqlIdentifierIfRequired( Dialect dialect, String word) { if (word == null || word.isEmpty()) { return false; } int sqlIdentifierQuoteCounter = 0; for (int i = 0; i < word.length(); i++) { if ((word.charAt(i) == dialect.getOpenQuote() || word.charAt(i) == dialect.getCloseQuote()) && !sqlLine.isCharEscaped(word, i)) { sqlIdentifierQuoteCounter++; } } return sqlIdentifierQuoteCounter % 2 == 1; } String readAsDialectSpecificName( Dialect dialect, String originalName) { if (originalName == null) { return null; } final boolean isQuoted = !originalName.isEmpty() && originalName.charAt(0) == dialect.getOpenQuote(); if (isQuoted) { if (originalName.charAt(originalName.length() - 1) == dialect.getCloseQuote()) { return originalName.length() == 1 ? "" : originalName.substring(1, originalName.length() - 1); } else { return originalName.substring(1); } } else { return dialect.isUpper() ? originalName.toUpperCase(Locale.ROOT) : dialect.isLower() ? originalName.toLowerCase(Locale.ROOT) : originalName; } } static String writeAsDialectSpecificValue( Dialect dialect, boolean forceQuote, String name2Write) { if (name2Write == null) { return null; } boolean needToQuote = false; if (forceQuote) { needToQuote = true; } else { boolean isUpper = dialect.isUpper(); boolean isLower = dialect.isLower(); String extraChars = dialect.getExtraNameCharacters(); for (int i = 0; i < name2Write.length(); i++) { if (isLower && ALLOWED_LOWER_CHARACTERS.indexOf(name2Write.charAt(i)) == -1 && extraChars.indexOf(name2Write.charAt(i)) == -1) { needToQuote = true; break; } else if (isUpper && ALLOWED_UPPER_CHARACTERS.indexOf(name2Write.charAt(i)) == -1 && extraChars.indexOf(name2Write.charAt(i)) == -1) { needToQuote = true; break; } else if (!isLower && !isUpper && ALLOWED_LOWER_CHARACTERS.indexOf(name2Write.charAt(i)) == -1 && ALLOWED_UPPER_CHARACTERS.indexOf(name2Write.charAt(i)) == -1 && extraChars.indexOf(name2Write.charAt(i)) == -1) { needToQuote = true; break; } } } if (needToQuote) { StringBuilder result = new StringBuilder(); result.append(dialect.getOpenQuote()); for (char c: name2Write.toCharArray()) { if (c == dialect.getOpenQuote() || c == dialect.getCloseQuote()) { result.append(c); } result.append(c); } result.append(dialect.getCloseQuote()); return result.toString(); } else { return name2Write; } } private boolean isOriginalNameStartedQuoted( Dialect dialect, String originalName) { return originalName != null && !originalName.isEmpty() && originalName.charAt(0) == dialect.getOpenQuote(); } } // End SqlCompleter.java