All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hazelcast.org.apache.calcite.sql.advise.SqlAdvisor Maven / Gradle / Ivy

/*
 * 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 com.hazelcast.org.apache.calcite.sql.advise;

import com.hazelcast.org.apache.calcite.avatica.util.Casing;
import com.hazelcast.org.apache.calcite.runtime.CalciteContextException;
import com.hazelcast.org.apache.calcite.runtime.CalciteException;
import com.hazelcast.org.apache.calcite.sql.SqlIdentifier;
import com.hazelcast.org.apache.calcite.sql.SqlNode;
import com.hazelcast.org.apache.calcite.sql.SqlSelect;
import com.hazelcast.org.apache.calcite.sql.SqlUtil;
import com.hazelcast.org.apache.calcite.sql.parser.SqlAbstractParserImpl;
import com.hazelcast.org.apache.calcite.sql.parser.SqlParseException;
import com.hazelcast.org.apache.calcite.sql.parser.SqlParser;
import com.hazelcast.org.apache.calcite.sql.parser.SqlParserPos;
import com.hazelcast.org.apache.calcite.sql.parser.SqlParserUtil;
import com.hazelcast.org.apache.calcite.sql.validate.SqlMoniker;
import com.hazelcast.org.apache.calcite.sql.validate.SqlMonikerImpl;
import com.hazelcast.org.apache.calcite.sql.validate.SqlMonikerType;
import com.hazelcast.org.apache.calcite.sql.validate.SqlValidatorWithHints;
import com.hazelcast.org.apache.calcite.util.Util;
import com.hazelcast.org.apache.calcite.util.trace.CalciteTrace;

import com.hazelcast.com.google.common.collect.ImmutableList;
import com.hazelcast.com.google.common.collect.Lists;

import com.hazelcast.org.slf4j.Logger;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;

/**
 * An assistant which offers hints and corrections to a partially-formed SQL
 * statement. It is used in the SQL editor user-interface.
 */
public class SqlAdvisor {
  //~ Static fields/initializers ---------------------------------------------

  public static final Logger LOGGER = CalciteTrace.PARSER_LOGGER;
  private static final String HINT_TOKEN = "_suggest_";
  private static final String UPPER_HINT_TOKEN =
      HINT_TOKEN.toUpperCase(Locale.ROOT);

  //~ Instance fields --------------------------------------------------------

  // Flags indicating precision/scale combinations
  private final SqlValidatorWithHints validator;
  private final SqlParser.Config parserConfig;

  // Cache for getPreferredCasing
  private String prevWord;
  private Casing prevPreferredCasing;

  // Reserved words cache
  private Set reservedWordsSet;
  private List reservedWordsList;

  //~ Constructors -----------------------------------------------------------

  /**
   * Creates a SqlAdvisor with a validator instance
   *
   * @param validator Validator
   * @deprecated use {@link #SqlAdvisor(SqlValidatorWithHints, SqlParser.Config)}
   */
  @Deprecated
  public SqlAdvisor(
      SqlValidatorWithHints validator) {
    this(validator, SqlParser.Config.DEFAULT);
  }

  /**
   * Creates a SqlAdvisor with a validator instance and given parser configuration
   *
   * @param validator Validator
   * @param parserConfig parser config
   */
  public SqlAdvisor(
      SqlValidatorWithHints validator,
      SqlParser.Config parserConfig) {
    this.validator = validator;
    this.parserConfig = parserConfig;
  }

  //~ Methods ----------------------------------------------------------------

  private char quoteStart() {
    return parserConfig.quoting().string.charAt(0);
  }

  private char quoteEnd() {
    char quote = quoteStart();
    return quote == '[' ? ']' : quote;
  }

  /**
   * Gets completion hints for a partially completed or syntactically incorrect
   * sql statement with cursor pointing to the position where completion hints
   * are requested.
   *
   * 

Writes into replaced[0] the string that is being * replaced. Includes the cursor and the preceding identifier. For example, * if sql is "select abc^de from t", sets * replaced[0] to "abc". If the cursor is in the middle of * whitespace, the replaced string is empty. The replaced string is never * null. * * @param sql A partial or syntactically incorrect sql statement for * which to retrieve completion hints * @param cursor to indicate the 0-based cursor position in the query at * @param replaced String which is being replaced (output) * @return completion hints */ public List getCompletionHints( String sql, int cursor, String[] replaced) { // search backward starting from current position to find a "word" int wordStart = cursor; boolean quoted = false; while (wordStart > 0 && Character.isJavaIdentifierPart(sql.charAt(wordStart - 1))) { --wordStart; } if ((wordStart > 0) && (sql.charAt(wordStart - 1) == quoteStart())) { quoted = true; --wordStart; } if (wordStart < 0) { return Collections.emptyList(); } // Search forwards to the end of the word we should remove. Eat up // trailing double-quote, if any int wordEnd = cursor; while (wordEnd < sql.length() && Character.isJavaIdentifierPart(sql.charAt(wordEnd))) { ++wordEnd; } if (quoted && (wordEnd < sql.length()) && (sql.charAt(wordEnd) == quoteEnd())) { ++wordEnd; } // remove the partially composed identifier from the // sql statement - otherwise we get a parser exception String word = replaced[0] = sql.substring(wordStart, cursor); if (wordStart < wordEnd) { sql = sql.substring(0, wordStart) + sql.substring(wordEnd); } final List completionHints = getCompletionHints0(sql, wordStart); if (quoted) { word = word.substring(1); } if (word.isEmpty()) { return completionHints; } // If cursor was part of the way through a word, only include hints // which start with that word in the result. final List result = new ArrayList<>(); Casing preferredCasing = getPreferredCasing(word); boolean ignoreCase = preferredCasing != Casing.UNCHANGED; for (SqlMoniker hint : completionHints) { List names = hint.getFullyQualifiedNames(); // For now we treat only simple cases where the added name is the last // See [CALCITE-2439] Smart complete for SqlAdvisor String cname = Util.last(names); if (cname.regionMatches(ignoreCase, 0, word, 0, word.length())) { result.add(hint); } } return result; } public List getCompletionHints0(String sql, int cursor) { String simpleSql = simplifySql(sql, cursor); int idx = simpleSql.indexOf(HINT_TOKEN); if (idx < 0) { return Collections.emptyList(); } SqlParserPos pos = new SqlParserPos(1, idx + 1); return getCompletionHints(simpleSql, pos); } /** * Returns casing which is preferred for replacement. * For instance, {@code en => ename, EN => ENAME}. * When input has mixed case, {@code Casing.UNCHANGED} is returned. * @param word input word * @return preferred casing when replacing input word */ private Casing getPreferredCasing(String word) { if (word == prevWord) { return prevPreferredCasing; } boolean hasLower = false; boolean hasUpper = false; int i = 0; while (i < word.length() && !(hasLower && hasUpper)) { int codePoint = word.codePointAt(i); hasLower |= Character.isLowerCase(codePoint); hasUpper |= Character.isUpperCase(codePoint); i += Character.charCount(codePoint); } Casing preferredCasing; if (hasUpper && !hasLower) { preferredCasing = Casing.TO_UPPER; } else if (!hasUpper && hasLower) { preferredCasing = Casing.TO_LOWER; } else { preferredCasing = Casing.UNCHANGED; } prevWord = word; prevPreferredCasing = preferredCasing; return preferredCasing; } public String getReplacement(SqlMoniker hint, String word) { Casing preferredCasing = getPreferredCasing(word); boolean quoted = !word.isEmpty() && word.charAt(0) == quoteStart(); return getReplacement(hint, quoted, preferredCasing); } public String getReplacement(SqlMoniker hint, boolean quoted, Casing preferredCasing) { String name = Util.last(hint.getFullyQualifiedNames()); boolean isKeyword = hint.getType() == SqlMonikerType.KEYWORD; // If replacement has mixed case, we need to quote it (or not depending // on quotedCasing/unquotedCasing quoted &= !isKeyword; if (!quoted && !isKeyword && getReservedAndKeyWordsSet().contains(name)) { quoted = true; } StringBuilder sb = new StringBuilder(name.length() + (quoted ? 2 : 0)); if (!isKeyword && !Util.isValidJavaIdentifier(name)) { // needs quotes ==> quoted quoted = true; } String idToAppend = name; if (!quoted) { // id ==preferredCasing==> preferredId ==unquotedCasing==> recasedId // if recasedId matches id, then use preferredId String preferredId = applyCasing(name, preferredCasing); if (isKeyword || matchesUnquoted(name, preferredId)) { idToAppend = preferredId; } else { // Check if we can use unquoted identifier as is: for instance, unquotedCasing==UNCHANGED quoted = !matchesUnquoted(name, idToAppend); } } if (quoted) { sb.append(quoteStart()); } sb.append(idToAppend); if (quoted) { sb.append(quoteEnd()); } return sb.toString(); } private boolean matchesUnquoted(String name, String idToAppend) { String recasedId = applyCasing(idToAppend, parserConfig.unquotedCasing()); return recasedId.regionMatches(!parserConfig.caseSensitive(), 0, name, 0, name.length()); } private String applyCasing(String value, Casing casing) { return SqlParserUtil.strip(value, null, null, null, casing); } /** * Gets completion hints for a syntactically correct sql statement with dummy * SqlIdentifier * * @param sql A syntactically correct sql statement for which to retrieve * completion hints * @param pos to indicate the line and column position in the query at which * completion hints need to be retrieved. For example, "select * a.ename, b.deptno from sales.emp a join sales.dept b "on * a.deptno=b.deptno where empno=1"; setting pos to 'Line 1, Column * 17' returns all the possible column names that can be selected * from sales.dept table setting pos to 'Line 1, Column 31' returns * all the possible table names in 'sales' schema * @return an array of hints ({@link SqlMoniker}) that can fill in at the * indicated position */ public List getCompletionHints(String sql, SqlParserPos pos) { // First try the statement they gave us. If this fails, just return // the tokens which were expected at the failure point. List hintList = new ArrayList<>(); SqlNode sqlNode = tryParse(sql, hintList); if (sqlNode == null) { return hintList; } // Now construct a statement which is bound to fail. (Character 7 BEL // is not legal in any SQL statement.) final int x = pos.getColumnNum() - 1; sql = sql.substring(0, x) + " \07" + sql.substring(x); tryParse(sql, hintList); final SqlMoniker star = new SqlMonikerImpl(ImmutableList.of("*"), SqlMonikerType.KEYWORD); String hintToken = parserConfig.unquotedCasing() == Casing.TO_UPPER ? UPPER_HINT_TOKEN : HINT_TOKEN; if (hintList.contains(star) && !isSelectListItem(sqlNode, pos, hintToken)) { hintList.remove(star); } // Add the identifiers which are expected at the point of interest. try { validator.validate(sqlNode); } catch (Exception e) { // mask any exception that is thrown during the validation, i.e. // try to continue even if the sql is invalid. we are doing a best // effort here to try to come up with the requested completion // hints Util.swallow(e, LOGGER); } final List validatorHints = validator.lookupHints(sqlNode, pos); hintList.addAll(validatorHints); return hintList; } private static boolean isSelectListItem(SqlNode root, final SqlParserPos pos, String hintToken) { List nodes = SqlUtil.getAncestry(root, input -> input instanceof SqlIdentifier && ((SqlIdentifier) input).names.contains(hintToken), input -> Objects.requireNonNull(input).getParserPosition() .startsAt(pos)); assert nodes.get(0) == root; nodes = Lists.reverse(nodes); return nodes.size() > 2 && nodes.get(2) instanceof SqlSelect && nodes.get(1) == ((SqlSelect) nodes.get(2)).getSelectList(); } /** * Tries to parse a SQL statement. * *

If succeeds, returns the parse tree node; if fails, populates the list * of hints and returns null. * * @param sql SQL statement * @param hintList List of hints suggesting allowable tokens at the point of * failure * @return Parse tree if succeeded, null if parse failed */ private SqlNode tryParse(String sql, List hintList) { try { return parseQuery(sql); } catch (SqlParseException e) { for (String tokenName : e.getExpectedTokenNames()) { // Only add tokens which are keywords, like '"BY"'; ignore // symbols such as ''. if (tokenName.startsWith("\"") && tokenName.endsWith("\"")) { hintList.add( new SqlMonikerImpl( tokenName.substring(1, tokenName.length() - 1), SqlMonikerType.KEYWORD)); } } return null; } catch (CalciteException e) { Util.swallow(e, null); return null; } } /** * Gets the fully qualified name for a {@link SqlIdentifier} at a given * position of a sql statement. * * @param sql A syntactically correct sql statement for which to retrieve a * fully qualified SQL identifier name * @param cursor to indicate the 0-based cursor position in the query that * represents a SQL identifier for which its fully qualified * name is to be returned. * @return a {@link SqlMoniker} that contains the fully qualified name of * the specified SQL identifier, returns null if none is found or the SQL * statement is invalid. */ public SqlMoniker getQualifiedName(String sql, int cursor) { SqlNode sqlNode; try { sqlNode = parseQuery(sql); validator.validate(sqlNode); } catch (Exception e) { return null; } SqlParserPos pos = new SqlParserPos(1, cursor + 1); try { return validator.lookupQualifiedName(sqlNode, pos); } catch (CalciteContextException | AssertionError e) { return null; } } /** * Attempts to complete and validate a given partially completed sql * statement, and returns whether it is valid. * * @param sql A partial or syntactically incorrect sql statement to validate * @return whether SQL statement is valid */ public boolean isValid(String sql) { SqlSimpleParser simpleParser = new SqlSimpleParser(HINT_TOKEN, parserConfig); String simpleSql = simpleParser.simplifySql(sql); SqlNode sqlNode; try { sqlNode = parseQuery(simpleSql); } catch (Exception e) { // if the sql can't be parsed we wont' be able to validate it return false; } try { validator.validate(sqlNode); } catch (Exception e) { return false; } return true; } /** * Attempts to parse and validate a SQL statement. Throws the first * exception encountered. The error message of this exception is to be * displayed on the UI * * @param sql A user-input sql statement to be validated * @return a List of ValidateErrorInfo (null if sql is valid) */ public List validate(String sql) { SqlNode sqlNode; List errorList = new ArrayList<>(); sqlNode = collectParserError(sql, errorList); if (!errorList.isEmpty()) { return errorList; } try { validator.validate(sqlNode); } catch (CalciteContextException e) { ValidateErrorInfo errInfo = new ValidateErrorInfo(e); // validator only returns 1 exception now errorList.add(errInfo); return errorList; } catch (Exception e) { ValidateErrorInfo errInfo = new ValidateErrorInfo( 1, 1, 1, sql.length(), e.getMessage()); // parser only returns 1 exception now errorList.add(errInfo); return errorList; } return null; } /** * Turns a partially completed or syntactically incorrect sql statement into * a simplified, valid one that can be passed into getCompletionHints() * * @param sql A partial or syntactically incorrect sql statement * @param cursor to indicate column position in the query at which * completion hints need to be retrieved. * @return a completed, valid (and possibly simplified SQL statement */ public String simplifySql(String sql, int cursor) { SqlSimpleParser parser = new SqlSimpleParser(HINT_TOKEN, parserConfig); return parser.simplifySql(sql, cursor); } /** * Return an array of SQL reserved and keywords * * @return an of SQL reserved and keywords */ public List getReservedAndKeyWords() { ensureReservedAndKeyWords(); return reservedWordsList; } private Set getReservedAndKeyWordsSet() { ensureReservedAndKeyWords(); return reservedWordsSet; } private void ensureReservedAndKeyWords() { if (reservedWordsSet != null) { return; } Collection c = SqlAbstractParserImpl.getSql92ReservedWords(); List l = Arrays.asList( getParserMetadata().getJdbcKeywords().split(",")); List al = new ArrayList<>(); al.addAll(c); al.addAll(l); reservedWordsList = al; reservedWordsSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); reservedWordsSet.addAll(reservedWordsList); } /** * Returns the underlying Parser metadata. * *

To use a different parser (recognizing a different dialect of SQL), * derived class should override. * * @return metadata */ protected SqlAbstractParserImpl.Metadata getParserMetadata() { SqlParser parser = SqlParser.create("", parserConfig); return parser.getMetadata(); } /** * Wrapper function to parse a SQL query (SELECT or VALUES, but not INSERT, * UPDATE, DELETE, CREATE, DROP etc.), throwing a {@link SqlParseException} * if the statement is not syntactically valid. * * @param sql SQL statement * @return parse tree * @throws SqlParseException if not syntactically valid */ protected SqlNode parseQuery(String sql) throws SqlParseException { SqlParser parser = SqlParser.create(sql, parserConfig); return parser.parseStmt(); } /** * Attempts to parse a SQL statement and adds to the errorList if any syntax * error is found. This implementation uses {@link SqlParser}. Subclass can * re-implement this with a different parser implementation * * @param sql A user-input sql statement to be parsed * @param errorList A {@link List} of error to be added to * @return {@link SqlNode } that is root of the parse tree, null if the sql * is not valid */ protected SqlNode collectParserError( String sql, List errorList) { try { return parseQuery(sql); } catch (SqlParseException e) { ValidateErrorInfo errInfo = new ValidateErrorInfo( e.getPos(), e.getMessage()); // parser only returns 1 exception now errorList.add(errInfo); return null; } } //~ Inner Classes ---------------------------------------------------------- /** * An inner class that represents error message text and position info of a * validator or parser exception */ public class ValidateErrorInfo { private int startLineNum; private int startColumnNum; private int endLineNum; private int endColumnNum; private String errorMsg; /** * Creates a new ValidateErrorInfo with the position coordinates and an * error string. * * @param startLineNum Start line number * @param startColumnNum Start column number * @param endLineNum End line number * @param endColumnNum End column number * @param errorMsg Error message */ public ValidateErrorInfo( int startLineNum, int startColumnNum, int endLineNum, int endColumnNum, String errorMsg) { this.startLineNum = startLineNum; this.startColumnNum = startColumnNum; this.endLineNum = endLineNum; this.endColumnNum = endColumnNum; this.errorMsg = errorMsg; } /** * Creates a new ValidateErrorInfo with an CalciteContextException. * * @param e Exception */ public ValidateErrorInfo( CalciteContextException e) { this.startLineNum = e.getPosLine(); this.startColumnNum = e.getPosColumn(); this.endLineNum = e.getEndPosLine(); this.endColumnNum = e.getEndPosColumn(); this.errorMsg = e.getCause().getMessage(); } /** * Creates a new ValidateErrorInfo with a SqlParserPos and an error * string. * * @param pos Error position * @param errorMsg Error message */ public ValidateErrorInfo( SqlParserPos pos, String errorMsg) { this.startLineNum = pos.getLineNum(); this.startColumnNum = pos.getColumnNum(); this.endLineNum = pos.getEndLineNum(); this.endColumnNum = pos.getEndColumnNum(); this.errorMsg = errorMsg; } /** * @return 1-based starting line number */ public int getStartLineNum() { return startLineNum; } /** * @return 1-based starting column number */ public int getStartColumnNum() { return startColumnNum; } /** * @return 1-based end line number */ public int getEndLineNum() { return endLineNum; } /** * @return 1-based end column number */ public int getEndColumnNum() { return endColumnNum; } /** * @return error message */ public String getMessage() { return errorMsg; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy