com.hazelcast.org.apache.calcite.sql.parser.SqlParserUtil 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.parser;
import com.hazelcast.org.apache.calcite.avatica.util.Casing;
import com.hazelcast.org.apache.calcite.avatica.util.DateTimeUtils;
import com.hazelcast.org.apache.calcite.config.CalciteSystemProperty;
import com.hazelcast.org.apache.calcite.rel.type.RelDataTypeSystem;
import com.hazelcast.org.apache.calcite.runtime.CalciteContextException;
import com.hazelcast.org.apache.calcite.sql.SqlBinaryOperator;
import com.hazelcast.org.apache.calcite.sql.SqlDateLiteral;
import com.hazelcast.org.apache.calcite.sql.SqlIntervalLiteral;
import com.hazelcast.org.apache.calcite.sql.SqlIntervalQualifier;
import com.hazelcast.org.apache.calcite.sql.SqlKind;
import com.hazelcast.org.apache.calcite.sql.SqlLiteral;
import com.hazelcast.org.apache.calcite.sql.SqlNode;
import com.hazelcast.org.apache.calcite.sql.SqlNodeList;
import com.hazelcast.org.apache.calcite.sql.SqlNumericLiteral;
import com.hazelcast.org.apache.calcite.sql.SqlOperator;
import com.hazelcast.org.apache.calcite.sql.SqlPostfixOperator;
import com.hazelcast.org.apache.calcite.sql.SqlPrefixOperator;
import com.hazelcast.org.apache.calcite.sql.SqlSpecialOperator;
import com.hazelcast.org.apache.calcite.sql.SqlTimeLiteral;
import com.hazelcast.org.apache.calcite.sql.SqlTimestampLiteral;
import com.hazelcast.org.apache.calcite.sql.SqlUtil;
import com.hazelcast.org.apache.calcite.sql.fun.SqlStdOperatorTable;
import com.hazelcast.org.apache.calcite.util.DateString;
import com.hazelcast.org.apache.calcite.util.PrecedenceClimbingParser;
import com.hazelcast.org.apache.calcite.util.TimeString;
import com.hazelcast.org.apache.calcite.util.TimestampString;
import com.hazelcast.org.apache.calcite.util.Util;
import com.hazelcast.org.apache.calcite.util.trace.CalciteTrace;
import com.hazelcast.com.google.common.base.Preconditions;
import com.hazelcast.org.slf4j.Logger;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.StringTokenizer;
import java.util.function.Predicate;
import static com.hazelcast.org.apache.calcite.util.Static.RESOURCE;
/**
* Utility methods relating to parsing SQL.
*/
public final class SqlParserUtil {
//~ Static fields/initializers ---------------------------------------------
static final Logger LOGGER = CalciteTrace.getParserTracer();
//~ Constructors -----------------------------------------------------------
private SqlParserUtil() {
}
//~ Methods ----------------------------------------------------------------
/**
* @return the character-set prefix of an sql string literal; returns null
* if there is none
*/
public static String getCharacterSet(String s) {
if (s.charAt(0) == '\'') {
return null;
}
if (Character.toUpperCase(s.charAt(0)) == 'N') {
return CalciteSystemProperty.DEFAULT_NATIONAL_CHARSET.value();
}
int i = s.indexOf("'");
return s.substring(1, i); // skip prefixed '_'
}
/**
* Converts the contents of an sql quoted string literal into the
* corresponding Java string representation (removing leading and trailing
* quotes and unescaping internal doubled quotes).
*/
public static String parseString(String s) {
int i = s.indexOf("'"); // start of body
if (i > 0) {
s = s.substring(i);
}
return strip(s, "'", "'", "''", Casing.UNCHANGED);
}
public static BigDecimal parseDecimal(String s) {
return new BigDecimal(s);
}
public static BigDecimal parseInteger(String s) {
return new BigDecimal(s);
}
/**
* @deprecated this method is not localized for Farrago standards
*/
@Deprecated // to be removed before 2.0
public static java.sql.Date parseDate(String s) {
return java.sql.Date.valueOf(s);
}
/**
* @deprecated Does not parse SQL:99 milliseconds
*/
@Deprecated // to be removed before 2.0
public static java.sql.Time parseTime(String s) {
return java.sql.Time.valueOf(s);
}
/**
* @deprecated this method is not localized for Farrago standards
*/
@Deprecated // to be removed before 2.0
public static java.sql.Timestamp parseTimestamp(String s) {
return java.sql.Timestamp.valueOf(s);
}
public static SqlDateLiteral parseDateLiteral(String s, SqlParserPos pos) {
final String dateStr = parseString(s);
final Calendar cal =
DateTimeUtils.parseDateFormat(dateStr, Format.PER_THREAD.get().date,
DateTimeUtils.UTC_ZONE);
if (cal == null) {
throw SqlUtil.newContextException(pos,
RESOURCE.illegalLiteral("DATE", s,
RESOURCE.badFormat(DateTimeUtils.DATE_FORMAT_STRING).str()));
}
final DateString d = DateString.fromCalendarFields(cal);
return SqlLiteral.createDate(d, pos);
}
public static SqlTimeLiteral parseTimeLiteral(String s, SqlParserPos pos) {
final String dateStr = parseString(s);
final DateTimeUtils.PrecisionTime pt =
DateTimeUtils.parsePrecisionDateTimeLiteral(dateStr,
Format.PER_THREAD.get().time, DateTimeUtils.UTC_ZONE, -1);
if (pt == null) {
throw SqlUtil.newContextException(pos,
RESOURCE.illegalLiteral("TIME", s,
RESOURCE.badFormat(DateTimeUtils.TIME_FORMAT_STRING).str()));
}
final TimeString t = TimeString.fromCalendarFields(pt.getCalendar())
.withFraction(pt.getFraction());
return SqlLiteral.createTime(t, pt.getPrecision(), pos);
}
public static SqlTimestampLiteral parseTimestampLiteral(String s,
SqlParserPos pos) {
final String dateStr = parseString(s);
final Format format = Format.PER_THREAD.get();
DateTimeUtils.PrecisionTime pt = null;
// Allow timestamp literals with and without time fields (as does
// PostgreSQL); TODO: require time fields except in Babel's lenient mode
final DateFormat[] dateFormats = {format.timestamp, format.date};
for (DateFormat dateFormat : dateFormats) {
pt = DateTimeUtils.parsePrecisionDateTimeLiteral(dateStr,
dateFormat, DateTimeUtils.UTC_ZONE, -1);
if (pt != null) {
break;
}
}
if (pt == null) {
throw SqlUtil.newContextException(pos,
RESOURCE.illegalLiteral("TIMESTAMP", s,
RESOURCE.badFormat(DateTimeUtils.TIMESTAMP_FORMAT_STRING).str()));
}
final TimestampString ts =
TimestampString.fromCalendarFields(pt.getCalendar())
.withFraction(pt.getFraction());
return SqlLiteral.createTimestamp(ts, pt.getPrecision(), pos);
}
public static SqlIntervalLiteral parseIntervalLiteral(SqlParserPos pos,
int sign, String s, SqlIntervalQualifier intervalQualifier) {
final String intervalStr = parseString(s);
if (intervalStr.equals("")) {
throw SqlUtil.newContextException(pos,
RESOURCE.illegalIntervalLiteral(s + " "
+ intervalQualifier.toString(), pos.toString()));
}
return SqlLiteral.createInterval(sign, intervalStr, intervalQualifier, pos);
}
/**
* Checks if the date/time format is valid
*
* @param pattern {@link SimpleDateFormat} pattern
*/
public static void checkDateFormat(String pattern) {
SimpleDateFormat df = new SimpleDateFormat(pattern, Locale.ROOT);
Util.discard(df);
}
/**
* Converts the interval value into a millisecond representation.
*
* @param interval Interval
* @return a long value that represents millisecond equivalent of the
* interval value.
*/
public static long intervalToMillis(
SqlIntervalLiteral.IntervalValue interval) {
return intervalToMillis(
interval.getIntervalLiteral(),
interval.getIntervalQualifier());
}
public static long intervalToMillis(
String literal,
SqlIntervalQualifier intervalQualifier) {
Preconditions.checkArgument(!intervalQualifier.isYearMonth(),
"interval must be day time");
int[] ret;
try {
ret = intervalQualifier.evaluateIntervalLiteral(literal,
intervalQualifier.getParserPosition(), RelDataTypeSystem.DEFAULT);
assert ret != null;
} catch (CalciteContextException e) {
throw new RuntimeException("while parsing day-to-second interval "
+ literal, e);
}
long l = 0;
long[] conv = new long[5];
conv[4] = 1; // millisecond
conv[3] = conv[4] * 1000; // second
conv[2] = conv[3] * 60; // minute
conv[1] = conv[2] * 60; // hour
conv[0] = conv[1] * 24; // day
for (int i = 1; i < ret.length; i++) {
l += conv[i - 1] * ret[i];
}
return ret[0] * l;
}
/**
* Converts the interval value into a months representation.
*
* @param interval Interval
* @return a long value that represents months equivalent of the interval
* value.
*/
public static long intervalToMonths(
SqlIntervalLiteral.IntervalValue interval) {
return intervalToMonths(
interval.getIntervalLiteral(),
interval.getIntervalQualifier());
}
public static long intervalToMonths(
String literal,
SqlIntervalQualifier intervalQualifier) {
Preconditions.checkArgument(intervalQualifier.isYearMonth(),
"interval must be year month");
int[] ret;
try {
ret = intervalQualifier.evaluateIntervalLiteral(literal,
intervalQualifier.getParserPosition(), RelDataTypeSystem.DEFAULT);
assert ret != null;
} catch (CalciteContextException e) {
throw new RuntimeException("Error while parsing year-to-month interval "
+ literal, e);
}
long l = 0;
long[] conv = new long[2];
conv[1] = 1; // months
conv[0] = conv[1] * 12; // years
for (int i = 1; i < ret.length; i++) {
l += conv[i - 1] * ret[i];
}
return ret[0] * l;
}
/**
* Parses a positive int. All characters have to be digits.
*
* @see Integer#parseInt(String)
* @throws java.lang.NumberFormatException if invalid number or leading '-'
*/
public static int parsePositiveInt(String value) {
value = value.trim();
if (value.charAt(0) == '-') {
throw new NumberFormatException(value);
}
return Integer.parseInt(value);
}
/**
* Parses a Binary string. SQL:99 defines a binary string as a hexstring
* with EVEN nbr of hex digits.
*/
@Deprecated // to be removed before 2.0
public static byte[] parseBinaryString(String s) {
s = s.replace(" ", "");
s = s.replace("\n", "");
s = s.replace("\t", "");
s = s.replace("\r", "");
s = s.replace("\f", "");
s = s.replace("'", "");
if (s.length() == 0) {
return new byte[0];
}
assert (s.length() & 1) == 0; // must be even nbr of hex digits
final int lengthToBe = s.length() / 2;
s = "ff" + s;
BigInteger bigInt = new BigInteger(s, 16);
byte[] ret = new byte[lengthToBe];
System.arraycopy(
bigInt.toByteArray(),
2,
ret,
0,
ret.length);
return ret;
}
/**
* Unquotes a quoted string, using different quotes for beginning and end.
*/
public static String strip(String s, String startQuote, String endQuote,
String escape, Casing casing) {
if (startQuote != null) {
assert endQuote != null;
assert startQuote.length() == 1;
assert endQuote.length() == 1;
assert escape != null;
assert s.startsWith(startQuote) && s.endsWith(endQuote) : s;
s = s.substring(1, s.length() - 1).replace(escape, endQuote);
}
switch (casing) {
case TO_UPPER:
return s.toUpperCase(Locale.ROOT);
case TO_LOWER:
return s.toLowerCase(Locale.ROOT);
default:
return s;
}
}
/**
* Trims a string for given characters from left and right. E.g.
* {@code trim("aBaac123AabC","abBcC")} returns {@code "123A"}.
*/
public static String trim(
String s,
String chars) {
if (s.length() == 0) {
return "";
}
int start;
for (start = 0; start < s.length(); start++) {
char c = s.charAt(start);
if (chars.indexOf(c) < 0) {
break;
}
}
int stop;
for (stop = s.length(); stop > start; stop--) {
char c = s.charAt(stop - 1);
if (chars.indexOf(c) < 0) {
break;
}
}
if (start >= stop) {
return "";
}
return s.substring(start, stop);
}
/**
* Looks for one or two carets in a SQL string, and if present, converts
* them into a parser position.
*
* Examples:
*
*
* - findPos("xxx^yyy") yields {"xxxyyy", position 3, line 1 column 4}
*
- findPos("xxxyyy") yields {"xxxyyy", null}
*
- findPos("xxx^yy^y") yields {"xxxyyy", position 3, line 4 column 4
* through line 1 column 6}
*
*/
public static StringAndPos findPos(String sql) {
int firstCaret = sql.indexOf('^');
if (firstCaret < 0) {
return new StringAndPos(sql, -1, null);
}
int secondCaret = sql.indexOf('^', firstCaret + 1);
if (secondCaret < 0) {
String sqlSansCaret =
sql.substring(0, firstCaret)
+ sql.substring(firstCaret + 1);
int[] start = indexToLineCol(sql, firstCaret);
SqlParserPos pos = new SqlParserPos(start[0], start[1]);
return new StringAndPos(sqlSansCaret, firstCaret, pos);
} else {
String sqlSansCaret =
sql.substring(0, firstCaret)
+ sql.substring(firstCaret + 1, secondCaret)
+ sql.substring(secondCaret + 1);
int[] start = indexToLineCol(sql, firstCaret);
// subtract 1 because the col position needs to be inclusive
--secondCaret;
int[] end = indexToLineCol(sql, secondCaret);
// if second caret is on same line as first, decrement its column,
// because first caret pushed the string out
if (start[0] == end[0]) {
--end[1];
}
SqlParserPos pos =
new SqlParserPos(start[0], start[1], end[0], end[1]);
return new StringAndPos(sqlSansCaret, firstCaret, pos);
}
}
/**
* Returns the (1-based) line and column corresponding to a particular
* (0-based) offset in a string.
*
* Converse of {@link #lineColToIndex(String, int, int)}.
*/
public static int[] indexToLineCol(String sql, int i) {
int line = 0;
int j = 0;
while (true) {
int prevj = j;
j = nextLine(sql, j);
if ((j < 0) || (j > i)) {
return new int[]{line + 1, i - prevj + 1};
}
++line;
}
}
public static int nextLine(String sql, int j) {
int rn = sql.indexOf("\r\n", j);
int r = sql.indexOf("\r", j);
int n = sql.indexOf("\n", j);
if ((r < 0) && (n < 0)) {
assert rn < 0;
return -1;
} else if ((rn >= 0) && (rn < n) && (rn <= r)) {
return rn + 2; // looking at "\r\n"
} else if ((r >= 0) && (r < n)) {
return r + 1; // looking at "\r"
} else {
return n + 1; // looking at "\n"
}
}
/**
* Finds the position (0-based) in a string which corresponds to a given
* line and column (1-based).
*
*
Converse of {@link #indexToLineCol(String, int)}.
*/
public static int lineColToIndex(String sql, int line, int column) {
--line;
--column;
int i = 0;
while (line-- > 0) {
i = nextLine(sql, i);
}
return i + column;
}
/**
* Converts a string to a string with one or two carets in it. For example,
* addCarets("values (foo)", 1, 9, 1, 12)
yields "values
* (^foo^)".
*/
public static String addCarets(
String sql,
int line,
int col,
int endLine,
int endCol) {
String sqlWithCarets;
int cut = lineColToIndex(sql, line, col);
sqlWithCarets = sql.substring(0, cut) + "^"
+ sql.substring(cut);
if ((col != endCol) || (line != endLine)) {
cut = lineColToIndex(sqlWithCarets, endLine, endCol);
++cut; // for caret
if (cut < sqlWithCarets.length()) {
sqlWithCarets =
sqlWithCarets.substring(0, cut)
+ "^" + sqlWithCarets.substring(cut);
} else {
sqlWithCarets += "^";
}
}
return sqlWithCarets;
}
public static String getTokenVal(String token) {
// We don't care about the token which are not string
if (!token.startsWith("\"")) {
return null;
}
// Remove the quote from the token
int startIndex = token.indexOf("\"");
int endIndex = token.lastIndexOf("\"");
String tokenVal = token.substring(startIndex + 1, endIndex);
char c = tokenVal.charAt(0);
if (Character.isLetter(c)) {
return tokenVal;
}
return null;
}
/**
* Extracts the values from a collation name.
*
*
Collation names are on the form charset$locale$strength.
*
* @param in The collation name
* @return A {@link ParsedCollation}
*/
public static ParsedCollation parseCollation(String in) {
StringTokenizer st = new StringTokenizer(in, "$");
String charsetStr = st.nextToken();
String localeStr = st.nextToken();
String strength;
if (st.countTokens() > 0) {
strength = st.nextToken();
} else {
strength =
CalciteSystemProperty.DEFAULT_COLLATION_STRENGTH.value();
}
Charset charset = Charset.forName(charsetStr);
String[] localeParts = localeStr.split("_");
Locale locale;
if (1 == localeParts.length) {
locale = new Locale(localeParts[0]);
} else if (2 == localeParts.length) {
locale = new Locale(localeParts[0], localeParts[1]);
} else if (3 == localeParts.length) {
locale = new Locale(localeParts[0], localeParts[1], localeParts[2]);
} else {
throw RESOURCE.illegalLocaleFormat(localeStr).ex();
}
return new ParsedCollation(charset, locale, strength);
}
@Deprecated // to be removed before 2.0
public static String[] toStringArray(List list) {
return list.toArray(new String[0]);
}
public static SqlNode[] toNodeArray(List list) {
return list.toArray(new SqlNode[0]);
}
public static SqlNode[] toNodeArray(SqlNodeList list) {
return list.toArray();
}
@Deprecated // to be removed before 2.0
public static String rightTrim(
String s,
char c) {
int stop;
for (stop = s.length(); stop > 0; stop--) {
if (s.charAt(stop - 1) != c) {
break;
}
}
if (stop > 0) {
return s.substring(0, stop);
}
return "";
}
/**
* Replaces a range of elements in a list with a single element. For
* example, if list contains {A, B, C, D, E}
then
* replaceSublist(list, X, 1, 4)
returns {A, X, E}
.
*/
public static void replaceSublist(
List list,
int start,
int end,
T o) {
Objects.requireNonNull(list);
Preconditions.checkArgument(start < end);
for (int i = end - 1; i > start; --i) {
list.remove(i);
}
list.set(start, o);
}
/**
* Converts a list of {expression, operator, expression, ...} into a tree,
* taking operator precedence and associativity into account.
*/
public static SqlNode toTree(List