matchColumns) {
}
public FullTextSearch withAgainstValue(StringValue againstValue) {
+ return withAgainstValue((Expression) againstValue);
+ }
+
+ public FullTextSearch withAgainstValue(Expression againstValue) {
this.setAgainstValue(againstValue);
return this;
}
diff --git a/src/main/java/net/sf/jsqlparser/parser/ParserKeywordsUtils.java b/src/main/java/net/sf/jsqlparser/parser/ParserKeywordsUtils.java
index bfaa4a647..5bdb93830 100644
--- a/src/main/java/net/sf/jsqlparser/parser/ParserKeywordsUtils.java
+++ b/src/main/java/net/sf/jsqlparser/parser/ParserKeywordsUtils.java
@@ -10,425 +10,215 @@
package net.sf.jsqlparser.parser;
import java.io.File;
-import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
-import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.*;
+import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+/**
+ * Utilities for querying the parser's reserved and non-reserved keyword sets.
+ *
+ *
+ * Non-reserved keywords are derived from the generated {@link CCJSqlParserConstants} token
+ * table using the {@code MIN_NON_RESERVED_WORD} / {@code MAX_NON_RESERVED_WORD} sentinels.
+ *
+ *
+ * Reserved keywords are determined by scanning the Grammar file for all simple string token
+ * definitions ({@code }) and subtracting the non-reserved set.
+ */
public class ParserKeywordsUtils {
- public final static CharsetEncoder CHARSET_ENCODER = StandardCharsets.US_ASCII.newEncoder();
-
- public final static int RESTRICTED_FUNCTION = 1;
- public final static int RESTRICTED_SCHEMA = 2;
- public final static int RESTRICTED_TABLE = 4;
- public final static int RESTRICTED_COLUMN = 8;
- public final static int RESTRICTED_EXPRESSION = 16;
- public final static int RESTRICTED_ALIAS = 32;
- public final static int RESTRICTED_SQL2016 = 64;
-
- public final static int RESTRICTED_JSQLPARSER = 128
- | RESTRICTED_FUNCTION
- | RESTRICTED_SCHEMA
- | RESTRICTED_TABLE
- | RESTRICTED_COLUMN
- | RESTRICTED_EXPRESSION
- | RESTRICTED_ALIAS
- | RESTRICTED_SQL2016;
+ private static final CharsetEncoder ASCII_ENCODER = StandardCharsets.US_ASCII.newEncoder();
- // Classification follows http://www.h2database.com/html/advanced.html#keywords
- public final static Object[][] ALL_RESERVED_KEYWORDS = {
- {"ABSENT", RESTRICTED_JSQLPARSER},
- {"ALL", RESTRICTED_SQL2016},
- {"AND", RESTRICTED_SQL2016},
- {"ANY", RESTRICTED_JSQLPARSER},
- {"AS", RESTRICTED_SQL2016},
- {"BETWEEN", RESTRICTED_SQL2016},
- {"BOTH", RESTRICTED_SQL2016},
- {"CASEWHEN", RESTRICTED_ALIAS},
- {"CHECK", RESTRICTED_SQL2016},
- {"CONNECT", RESTRICTED_ALIAS},
- {"CONNECT_BY_ROOT", RESTRICTED_JSQLPARSER},
- {"CSV", RESTRICTED_JSQLPARSER},
- {"PRIOR", RESTRICTED_JSQLPARSER},
- {"CONSTRAINT", RESTRICTED_SQL2016},
- {"CREATE", RESTRICTED_ALIAS},
- {"CROSS", RESTRICTED_SQL2016},
- {"CURRENT", RESTRICTED_JSQLPARSER},
- {"DEFAULT", RESTRICTED_ALIAS},
- {"DISTINCT", RESTRICTED_SQL2016},
- {"DISTINCTROW", RESTRICTED_SQL2016},
- {"DOUBLE", RESTRICTED_ALIAS},
- {"ELSE", RESTRICTED_JSQLPARSER},
- {"ERRORS", RESTRICTED_JSQLPARSER},
- {"EXCEPT", RESTRICTED_SQL2016},
- {"EXCLUDES", RESTRICTED_JSQLPARSER},
- {"EXISTS", RESTRICTED_SQL2016},
- {"EXTEND", RESTRICTED_JSQLPARSER},
- {"FALSE", RESTRICTED_SQL2016},
- {"FBV", RESTRICTED_JSQLPARSER},
- {"FETCH", RESTRICTED_SQL2016},
- {"FILE", RESTRICTED_JSQLPARSER},
- {"FINAL", RESTRICTED_JSQLPARSER},
- {"FOR", RESTRICTED_SQL2016},
- {"FORCE", RESTRICTED_SQL2016},
- {"FOREIGN", RESTRICTED_SQL2016},
- {"FROM", RESTRICTED_SQL2016},
- {"FULL", RESTRICTED_SQL2016},
- {"GLOBAL", RESTRICTED_ALIAS},
- {"GROUP", RESTRICTED_SQL2016},
- {"GROUPING", RESTRICTED_ALIAS},
- {"QUALIFY", RESTRICTED_ALIAS},
- {"HAVING", RESTRICTED_SQL2016},
- {"IF", RESTRICTED_SQL2016},
- {"IIF", RESTRICTED_ALIAS},
- {"IGNORE", RESTRICTED_ALIAS},
- {"ILIKE", RESTRICTED_SQL2016},
- {"IMPORT", RESTRICTED_JSQLPARSER},
- {"IN", RESTRICTED_SQL2016},
- {"INCLUDES", RESTRICTED_JSQLPARSER},
- {"INNER", RESTRICTED_SQL2016},
- {"INTERSECT", RESTRICTED_SQL2016},
- {"INTERVAL", RESTRICTED_SQL2016},
- {"INTO", RESTRICTED_JSQLPARSER},
- {"IS", RESTRICTED_SQL2016},
- {"JOIN", RESTRICTED_JSQLPARSER},
- {"LATERAL", RESTRICTED_SQL2016},
- {"LEFT", RESTRICTED_SQL2016},
- {"LIKE", RESTRICTED_SQL2016},
- {"LIMIT", RESTRICTED_SQL2016},
- {"MINUS", RESTRICTED_SQL2016},
- {"NATURAL", RESTRICTED_SQL2016},
- {"NOCYCLE", RESTRICTED_JSQLPARSER},
- {"NOT", RESTRICTED_SQL2016},
- {"NULL", RESTRICTED_SQL2016},
- {"OFFSET", RESTRICTED_SQL2016},
- {"ON", RESTRICTED_SQL2016},
- {"ONLY", RESTRICTED_JSQLPARSER},
- {"OPTIMIZE", RESTRICTED_ALIAS},
- {"OR", RESTRICTED_SQL2016},
- {"ORDER", RESTRICTED_SQL2016},
- {"OUTER", RESTRICTED_JSQLPARSER},
- {"OUTPUT", RESTRICTED_JSQLPARSER},
- {"OPTIMIZE ", RESTRICTED_JSQLPARSER},
- {"OVERWRITE ", RESTRICTED_JSQLPARSER},
- {"PIVOT", RESTRICTED_JSQLPARSER},
- {"PREFERRING", RESTRICTED_JSQLPARSER},
- {"PRIOR", RESTRICTED_ALIAS},
- {"PROCEDURE", RESTRICTED_ALIAS},
- {"PUBLIC", RESTRICTED_ALIAS},
- {"RETURNING", RESTRICTED_JSQLPARSER},
- {"RIGHT", RESTRICTED_SQL2016},
- {"SAMPLE", RESTRICTED_ALIAS},
- {"SCRIPT", RESTRICTED_JSQLPARSER},
- {"SEL", RESTRICTED_ALIAS},
- {"SELECT", RESTRICTED_ALIAS},
- {"SEMI", RESTRICTED_JSQLPARSER},
- {"SET", RESTRICTED_JSQLPARSER},
- {"SOME", RESTRICTED_JSQLPARSER},
- {"START", RESTRICTED_JSQLPARSER},
- {"STATEMENT", RESTRICTED_JSQLPARSER},
- {"TABLES", RESTRICTED_ALIAS},
- {"TOP", RESTRICTED_SQL2016},
- {"TRAILING", RESTRICTED_SQL2016},
- {"TRUE", RESTRICTED_SQL2016},
- {"UNBOUNDED", RESTRICTED_JSQLPARSER},
- {"UNION", RESTRICTED_SQL2016},
- {"UNIQUE", RESTRICTED_SQL2016},
- {"UNKNOWN", RESTRICTED_SQL2016},
- {"UNPIVOT", RESTRICTED_JSQLPARSER},
- {"USE", RESTRICTED_JSQLPARSER},
- {"USING", RESTRICTED_SQL2016},
- {"SQL_CACHE", RESTRICTED_JSQLPARSER},
- {"SQL_CALC_FOUND_ROWS", RESTRICTED_JSQLPARSER},
- {"SQL_NO_CACHE", RESTRICTED_JSQLPARSER},
- {"STRAIGHT_JOIN", RESTRICTED_JSQLPARSER},
- {"TABLESAMPLE", RESTRICTED_ALIAS},
- {"VALUE", RESTRICTED_JSQLPARSER},
- {"VALUES", RESTRICTED_SQL2016},
- {"VARYING", RESTRICTED_JSQLPARSER},
- {"VERIFY", RESTRICTED_JSQLPARSER},
- {"WHEN", RESTRICTED_SQL2016},
- {"WHERE", RESTRICTED_SQL2016},
- {"WINDOW", RESTRICTED_SQL2016},
- {"WITH", RESTRICTED_SQL2016},
- {"XOR", RESTRICTED_JSQLPARSER},
- {"XMLSERIALIZE", RESTRICTED_JSQLPARSER},
+ /** Matches a pure keyword image: word characters, at least two characters, pure US-ASCII. */
+ private static final Pattern KEYWORD_PATTERN = Pattern.compile("[A-Za-z_][A-Za-z_0-9]+");
- // add keywords from the composite token definitions:
- // tk= | tk= | tk=
- // we will use the composite tokens instead, which are always hit first before the
- // simple keywords
- // @todo: figure out a way to remove these composite tokens, as they do more harm than
- // good
- {"SEL", RESTRICTED_JSQLPARSER},
- {"SELECT", RESTRICTED_JSQLPARSER},
- {"DATE", RESTRICTED_JSQLPARSER},
- {"TIME", RESTRICTED_JSQLPARSER},
- {"TIMESTAMP", RESTRICTED_JSQLPARSER},
- {"YEAR", RESTRICTED_JSQLPARSER},
- {"MONTH", RESTRICTED_JSQLPARSER},
- {"DAY", RESTRICTED_JSQLPARSER},
- {"HOUR", RESTRICTED_JSQLPARSER},
- {"MINUTE", RESTRICTED_JSQLPARSER},
- {"SECOND", RESTRICTED_JSQLPARSER},
- {"SUBSTR", RESTRICTED_JSQLPARSER},
- {"SUBSTRING", RESTRICTED_JSQLPARSER},
- {"TRIM", RESTRICTED_JSQLPARSER},
- {"POSITION", RESTRICTED_JSQLPARSER},
- {"OVERLAY", RESTRICTED_JSQLPARSER},
- {"NEXTVAL", RESTRICTED_COLUMN},
-
- // @todo: Object Names should not start with Hex-Prefix, we shall not find that Token
- {"0x", RESTRICTED_JSQLPARSER}
- };
+ /**
+ * Matches simple token definitions in the grammar: {@code }. Group 1 captures
+ * the string value. Only matches definitions where the value is a plain quoted string —
+ * compound regex tokens like {@code } won't match.
+ */
+ private static final Pattern SIMPLE_TOKEN_PATTERN =
+ Pattern.compile("", Pattern.MULTILINE);
- @SuppressWarnings({"PMD.ExcessiveMethodLength"})
- public static List getReservedKeywords(int restriction) {
- ArrayList keywords = new ArrayList<>();
- for (Object[] data : ALL_RESERVED_KEYWORDS) {
- int value = (int) data[1];
+ private ParserKeywordsUtils() {
+ // utility class
+ }
- // test if bit is not set
- if ((value & restriction) == restriction || (restriction & value) == value) {
- keywords.add((String) data[0]);
+ /**
+ * Returns the set of non-reserved keywords, i.e. tokens whose kind lies between
+ * {@code MIN_NON_RESERVED_WORD} and {@code MAX_NON_RESERVED_WORD}. These keywords can be used
+ * as unquoted identifiers.
+ */
+ public static TreeSet getNonReservedKeywords() {
+ TreeSet keywords = new TreeSet<>();
+ String[] images = CCJSqlParserConstants.tokenImage;
+
+ for (int kind = CCJSqlParserConstants.MIN_NON_RESERVED_WORD
+ + 1; kind < CCJSqlParserConstants.MAX_NON_RESERVED_WORD; kind++) {
+ String image = extractKeyword(images[kind]);
+ if (image != null && isKeywordImage(image)) {
+ keywords.add(image);
}
}
-
return keywords;
}
/**
- * @param args with: Grammar File, Keyword Documentation File
- * @throws Exception
+ * Returns the set of reserved keywords by scanning the Grammar file for all simple
+ * string token definitions and subtracting the non-reserved keywords.
+ *
+ * @param grammarFile the {@code .jjt} grammar file
+ * @return reserved keyword strings
+ * @throws IOException if reading the grammar file fails
*/
- public static void main(String[] args) throws Exception {
- if (args.length < 2) {
- throw new IllegalArgumentException("No filename provided aS context ARGS[0]");
- }
-
- File grammarFile = new File(args[0]);
- if (grammarFile.exists() && grammarFile.canRead() && grammarFile.canWrite()) {
- buildGrammarForRelObjectName(grammarFile);
- buildGrammarForRelObjectNameWithoutValue(grammarFile);
- } else {
- throw new FileNotFoundException("Can't read file " + args[0]);
- }
-
- File keywordDocumentationFile = new File(args[1]);
- keywordDocumentationFile.createNewFile();
- if (keywordDocumentationFile.canWrite()) {
- writeKeywordsDocumentationFile(keywordDocumentationFile);
- } else {
- throw new FileNotFoundException("Can't read file " + args[1]);
- }
+ public static TreeSet getReservedKeywords(File grammarFile) throws IOException {
+ TreeSet allSimple = getAllSimpleKeywords(grammarFile);
+ allSimple.removeAll(getNonReservedKeywords());
+ return allSimple;
}
- public static TreeSet getAllKeywordsUsingRegex(File file) throws IOException {
- Pattern tokenBlockPattern = Pattern.compile(
- "TOKEN\\s*:\\s*/\\*.*\\*/*(?:\\r?\\n|\\r)\\{(?:[^}{]+|\\{(?:[^}{]+|\\{[^}{]*})*})*}",
- Pattern.MULTILINE);
- Pattern tokenStringValuePattern = Pattern.compile("\"(\\w{2,})\"", Pattern.MULTILINE);
-
- TreeSet allKeywords = new TreeSet<>();
-
- Path path = file.toPath();
- Charset charset = Charset.defaultCharset();
- String content = new String(Files.readAllBytes(path), charset);
-
- Matcher tokenBlockmatcher = tokenBlockPattern.matcher(content);
- while (tokenBlockmatcher.find()) {
- String tokenBlock = tokenBlockmatcher.group(0);
- // remove single and multiline comments
- tokenBlock = tokenBlock.replaceAll("(?sm)((\\/\\*.*?\\*\\/)|(\\/\\/.*?$))", "");
- for (String tokenDefinition : getTokenDefinitions(tokenBlock)) {
- // check if token definition is private
- if (tokenDefinition.matches("(?sm)^<\\s*[^#].*")) {
- Matcher tokenStringValueMatcher =
- tokenStringValuePattern.matcher(tokenDefinition);
- while (tokenStringValueMatcher.find()) {
- String tokenValue = tokenStringValueMatcher.group(1);
- // test if pure US-ASCII
- if (CHARSET_ENCODER.canEncode(tokenValue) && tokenValue.matches("\\w+")) {
- allKeywords.add(tokenValue);
- }
- }
- }
+ /**
+ * Returns all simple string keywords defined in the grammar file. Scans for
+ * {@code } patterns and collects the string values.
+ *
+ * @param grammarFile the {@code .jjt} grammar file
+ * @return all simple keyword strings
+ * @throws IOException if reading the grammar file fails
+ */
+ public static TreeSet getAllSimpleKeywords(File grammarFile) throws IOException {
+ TreeSet keywords = new TreeSet<>();
+ String content = Files.readString(grammarFile.toPath(), StandardCharsets.UTF_8);
+
+ Matcher matcher = SIMPLE_TOKEN_PATTERN.matcher(content);
+ while (matcher.find()) {
+ String value = matcher.group(1);
+ if (isKeywordImage(value) && ASCII_ENCODER.canEncode(value)) {
+ keywords.add(value);
}
}
- return allKeywords;
+ return keywords;
}
- @SuppressWarnings({"PMD.EmptyWhileStmt"})
- private static List getTokenDefinitions(String tokenBlock) {
- List tokenDefinitions = new ArrayList<>();
- int level = 0;
- char openChar = '<';
- char closeChar = '>';
- char[] tokenBlockChars = tokenBlock.toCharArray();
- int tokenDefinitionStart = -1;
- for (int i = 0; i < tokenBlockChars.length; ++i) {
- if (isQuotationMark(i, tokenBlockChars)) {
- // skip everything inside quotation marks
- while (!isQuotationMark(++i, tokenBlockChars)) {
- // skip until quotation ends
- }
- }
-
- char character = tokenBlockChars[i];
- if (character == openChar) {
- if (level == 0) {
- tokenDefinitionStart = i;
- }
+ /**
+ * Checks whether the given token kind is a non-reserved keyword that can be used as an unquoted
+ * identifier.
+ */
+ public static boolean isNonReservedKeyword(int tokenKind) {
+ return tokenKind > CCJSqlParserConstants.MIN_NON_RESERVED_WORD
+ && tokenKind < CCJSqlParserConstants.MAX_NON_RESERVED_WORD;
+ }
- ++level;
- } else if (character == closeChar) {
- --level;
+ /**
+ * Writes a reStructuredText documentation file listing all reserved keywords.
+ *
+ * @param grammarFile the {@code .jjt} grammar file
+ * @param rstFile the output {@code .rst} file to write
+ * @throws IOException if reading or writing fails
+ */
+ public static void writeKeywordsDocumentationFile(File grammarFile, File rstFile)
+ throws IOException {
+ TreeSet reserved = getReservedKeywords(grammarFile);
- if (level == 0 && tokenDefinitionStart >= 0) {
- tokenDefinitions.add(tokenBlock.substring(tokenDefinitionStart, i + 1));
- tokenDefinitionStart = -1;
- }
- }
- }
+ StringBuilder builder = new StringBuilder();
+ builder.append("***********************\n");
+ builder.append("Reserved Keywords\n");
+ builder.append("***********************\n");
+ builder.append("\n");
- return tokenDefinitions;
- }
+ builder.append(
+ "The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and must not be used for **Naming Objects**: \n");
+ builder.append("\n");
- private static boolean isQuotationMark(int index, char[] str) {
- if (str[index] == '\"') {
- // check if quotation is escaped
- if (index > 0 && str[index - 1] == '\\') {
- return index > 1 && str[index - 2] == '\\';
- }
+ builder.append("+---------------------------+\n");
+ builder.append("| **Keyword** |\n");
+ builder.append("+---------------------------+\n");
- return true;
+ for (String keyword : reserved) {
+ builder.append("| ").append(rightPadding(keyword, ' ', 25)).append(" |\n");
+ builder.append("+---------------------------+\n");
}
- return false;
+ try (FileWriter fileWriter = new FileWriter(rstFile)) {
+ fileWriter.append(builder);
+ fileWriter.flush();
+ }
}
- public static void buildGrammarForRelObjectNameWithoutValue(File file) throws Exception {
- Pattern methodBlockPattern = Pattern.compile(
- "String\\W*RelObjectNameWithoutValue\\W*\\(\\W*\\)\\W*:\\s*\\{(?:[^}{]+|\\{(?:[^}{]+|\\{[^}{]*})*})*}\\s*\\{(?:[^}{]+|\\{(?:[^}{]+|\\{[^}{]*})*})*}",
- Pattern.MULTILINE);
-
- TreeSet allKeywords = getAllKeywords(file);
+ public static String rightPadding(String input, char ch, int length) {
+ return String.format("%" + (-length) + "s", input).replace(' ', ch);
+ }
- for (String reserved : getReservedKeywords(RESTRICTED_JSQLPARSER)) {
- allKeywords.remove(reserved);
+ /**
+ * Entry point for the {@code updateKeywords} Gradle/Maven task.
+ *
+ *
+ * Usage: {@code java net.sf.jsqlparser.parser.ParserKeywordsUtils }
+ *
+ * @param args {@code args[0]}: path to the grammar file, {@code args[1]}: path to the output
+ * RST file
+ * @throws Exception if reading or writing fails
+ */
+ public static void main(String[] args) throws Exception {
+ if (args.length < 2) {
+ throw new IllegalArgumentException(
+ "Usage: ParserKeywordsUtils ");
}
- StringBuilder builder = new StringBuilder();
- builder.append("String RelObjectNameWithoutValue() :\n"
- + "{ Token tk = null; }\n"
- + "{\n"
- // @todo: find a way to avoid those hardcoded compound tokens
- + " ( tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= \n"
- + " ");
-
- for (String keyword : allKeywords) {
- builder.append(" | tk=\"").append(keyword).append("\"");
+ File grammarFile = new File(args[0]);
+ if (!grammarFile.canRead()) {
+ throw new IOException("Cannot read grammar file: " + grammarFile);
}
- builder.append(" )\n" + " { return tk.image; }\n" + "}");
+ File rstFile = new File(args[1]);
+ rstFile.getParentFile().mkdirs();
+ writeKeywordsDocumentationFile(grammarFile, rstFile);
- replaceInFile(file, methodBlockPattern, builder.toString());
+ System.out.println("Reserved keywords: " + getReservedKeywords(grammarFile).size());
+ System.out.println("Non-reserved keywords: " + getNonReservedKeywords().size());
+ System.out.println("Written to: " + rstFile.getAbsolutePath());
}
- public static void buildGrammarForRelObjectName(File file) throws Exception {
- // Pattern pattern =
- // Pattern.compile("String\\W*RelObjectName\\W*\\(\\W*\\)\\W*:\\s*\\{(?:[^}{]+|\\{(?:[^}{]+|\\{[^}{]*})*})*}\\s*\\{(?:[^}{]+|\\{(?:[^}{]+|\\{[^}{]*})*})*}",
- // Pattern.MULTILINE);
- TreeSet allKeywords = new TreeSet<>();
- for (String reserved : getReservedKeywords(RESTRICTED_ALIAS)) {
- allKeywords.add(reserved);
+ /**
+ * Extracts the keyword string from a {@code tokenImage} entry.
+ *
+ *
+ * JavaCC renders inline BNF token declarations as {@code } in {@code tokenImage}.
+ * Stripping the {@code K_} prefix and angle brackets yields the keyword string.
+ *
+ * @return the keyword string, or {@code null} if the entry is not a {@code K_} token
+ */
+ private static String extractKeyword(String tokenImage) {
+ if (tokenImage == null || tokenImage.length() < 5) {
+ return null;
}
- for (String reserved : getReservedKeywords(RESTRICTED_JSQLPARSER & ~RESTRICTED_ALIAS)) {
- allKeywords.remove(reserved);
+ // Format: → ACTION
+ if (tokenImage.charAt(0) == '<'
+ && tokenImage.charAt(tokenImage.length() - 1) == '>'
+ && tokenImage.startsWith(" getAllKeywords(File file) throws Exception {
- return getAllKeywordsUsingRegex(file);
- }
-
- private static void replaceInFile(File file, Pattern pattern, String replacement)
- throws IOException {
- Path path = file.toPath();
- Charset charset = Charset.defaultCharset();
-
- String content = new String(Files.readAllBytes(path), charset);
- content = pattern.matcher(content).replaceAll(replacement);
- Files.write(file.toPath(), content.getBytes(charset));
+ return null;
}
- public static String rightPadding(String input, char ch, int length) {
- return String.format("%" + (-length) + "s", input).replace(' ', ch);
- }
-
- public static void writeKeywordsDocumentationFile(File file) throws IOException {
- StringBuilder builder = new StringBuilder();
- builder.append("***********************\n");
- builder.append("Restricted Keywords\n");
- builder.append("***********************\n");
- builder.append("\n");
-
- builder.append(
- "The following Keywords are **restricted** in JSQLParser-|JSQLPARSER_VERSION| and must not be used for **Naming Objects**: \n");
- builder.append("\n");
-
- builder.append("+----------------------+-------------+-----------+\n");
- builder.append("| **Keyword** | JSQL Parser | SQL:2016 |\n");
- builder.append("+----------------------+-------------+-----------+\n");
-
- for (Object[] keywordDefinition : ALL_RESERVED_KEYWORDS) {
- builder.append("| ").append(rightPadding(keywordDefinition[0].toString(), ' ', 20))
- .append(" | ");
-
- int value = (int) keywordDefinition[1];
- int restriction = RESTRICTED_JSQLPARSER;
- String s = (value & restriction) == restriction || (restriction & value) == value
- ? "Yes"
- : "";
- builder.append(rightPadding(s, ' ', 11)).append(" | ");
-
- restriction = RESTRICTED_SQL2016;
- s = (value & restriction) == restriction || (restriction & value) == value
- ? "Yes"
- : "";
- builder.append(rightPadding(s, ' ', 9)).append(" | ");
-
- builder.append("\n");
- builder.append("+----------------------+-------------+-----------+\n");
- }
- try (FileWriter fileWriter = new FileWriter(file)) {
- fileWriter.append(builder);
- fileWriter.flush();
- }
+ /**
+ * Returns {@code true} if the image looks like a SQL keyword: alphabetic start, word characters
+ * only, at least 2 characters, pure US-ASCII.
+ */
+ private static boolean isKeywordImage(String image) {
+ return KEYWORD_PATTERN.matcher(image).matches()
+ && ASCII_ENCODER.canEncode(image);
}
}
diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
index 7f4cf2af0..d786f5170 100644
--- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
+++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
@@ -809,6 +809,19 @@ public enum Feature {
* "EXPORT"
*/
export,
+
+ /**
+ * MySQL allows a ',' as a separator between key and value entries. We allow that by default,
+ * but it can be disabled here
+ */
+ allowCommaAsKeyValueSeparator(true),
+
+ /**
+ * DB2 and Oracle allow Expressions as JSON_OBJECT key values. This clashes with Informix and
+ * Snowflake Json-Extraction syntax
+ */
+ allowExpressionAsJsonObjectKey(false)
+
;
private final Object value;
diff --git a/src/main/java/net/sf/jsqlparser/schema/Column.java b/src/main/java/net/sf/jsqlparser/schema/Column.java
index 400d34c3a..33240ac52 100644
--- a/src/main/java/net/sf/jsqlparser/schema/Column.java
+++ b/src/main/java/net/sf/jsqlparser/schema/Column.java
@@ -12,11 +12,12 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-
import net.sf.jsqlparser.expression.ArrayConstructor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.ExpressionVisitor;
+import net.sf.jsqlparser.expression.operators.relational.SupportsOldOracleJoinSyntax;
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
+import net.sf.jsqlparser.statement.ReturningReferenceType;
/**
* A column. It can have the table name it belongs to.
@@ -28,6 +29,9 @@ public class Column extends ASTNodeAccessImpl implements Expression, MultiPartNa
private String commentText;
private ArrayConstructor arrayConstructor;
private String tableDelimiter = ".";
+ private int oldOracleJoinSyntax = SupportsOldOracleJoinSyntax.NO_ORACLE_JOIN;
+ private ReturningReferenceType returningReferenceType = null;
+ private String returningQualifier = null;
// holds the physical table when resolved against an actual schema information
private Table resolvedTable = null;
@@ -53,7 +57,8 @@ public Column(List nameParts, List delimiters) {
}
public Column(String columnName) {
- this(null, columnName);
+ this();
+ setColumnName(columnName);
}
public ArrayConstructor getArrayConstructor() {
@@ -131,8 +136,56 @@ public String getUnquotedColumnName() {
return MultiPartName.unquote(columnName);
}
- public void setColumnName(String string) {
- columnName = string;
+ public void setColumnName(String name) {
+ // BigQuery seems to allow things like: `catalogName.schemaName.tableName` in only one pair
+ // of quotes
+ // however, some people believe that Dots in Names are a good idea, so provide a switch-off
+ boolean splitNamesOnDelimiter = System.getProperty("SPLIT_NAMES_ON_DELIMITER") == null ||
+ !List
+ .of("0", "N", "n", "FALSE", "false", "OFF", "off")
+ .contains(System.getProperty("SPLIT_NAMES_ON_DELIMITER"));
+
+ setName(name, splitNamesOnDelimiter);
+ }
+
+ public void setName(String name, boolean splitNamesOnDelimiter) {
+ if (MultiPartName.isQuoted(name) && name.contains(".") && splitNamesOnDelimiter) {
+ String[] parts = MultiPartName.unquote(name).split("\\.");
+ switch (parts.length) {
+ case 3:
+ this.table = new Table("\"" + parts[0] + "\".\"" + parts[1] + "\"");
+ this.columnName = "\"" + parts[2] + "\"";
+ break;
+ case 2:
+ this.table = new Table("\"" + parts[0] + "\"");
+ this.columnName = "\"" + parts[1] + "\"";
+ break;
+ case 1:
+ this.columnName = "\"" + parts[0] + "\"";
+ break;
+ default:
+ throw new RuntimeException("Invalid column name: " + name);
+ }
+ } else if (name.contains(".") && splitNamesOnDelimiter) {
+ String[] parts = MultiPartName.unquote(name).split("\\.");
+ switch (parts.length) {
+ case 3:
+ this.table = new Table(parts[0] + "." + parts[1]);
+ this.columnName = parts[2];
+ break;
+ case 2:
+ this.table = new Table(parts[0]);
+ this.columnName = parts[1];
+ break;
+ case 1:
+ this.columnName = parts[0];
+ break;
+ default:
+ throw new RuntimeException("Invalid column name: " + name);
+ }
+ } else {
+ this.columnName = name;
+ }
}
public String getTableDelimiter() {
@@ -143,6 +196,14 @@ public void setTableDelimiter(String tableDelimiter) {
this.tableDelimiter = tableDelimiter;
}
+ public int getOldOracleJoinSyntax() {
+ return oldOracleJoinSyntax;
+ }
+
+ public void setOldOracleJoinSyntax(int oldOracleJoinSyntax) {
+ this.oldOracleJoinSyntax = oldOracleJoinSyntax;
+ }
+
@Override
public String getFullyQualifiedName() {
return getFullyQualifiedName(false);
@@ -156,7 +217,9 @@ public String getUnquotedName() {
public String getFullyQualifiedName(boolean aliases) {
StringBuilder fqn = new StringBuilder();
- if (table != null) {
+ if (returningQualifier != null) {
+ fqn.append(returningQualifier);
+ } else if (table != null) {
if (table.getAlias() != null && aliases) {
fqn.append(table.getAlias().getName());
} else {
@@ -196,6 +259,7 @@ public T accept(ExpressionVisitor expressionVisitor, S context) {
@Override
public String toString() {
return getFullyQualifiedName(true)
+ + (oldOracleJoinSyntax != SupportsOldOracleJoinSyntax.NO_ORACLE_JOIN ? "(+)" : "")
+ (commentText != null ? " /* " + commentText + "*/ " : "");
}
@@ -219,6 +283,36 @@ public Column withTableDelimiter(String delimiter) {
return this;
}
+ public Column withOldOracleJoinSyntax(int oldOracleJoinSyntax) {
+ this.setOldOracleJoinSyntax(oldOracleJoinSyntax);
+ return this;
+ }
+
+ public ReturningReferenceType getReturningReferenceType() {
+ return returningReferenceType;
+ }
+
+ public Column setReturningReferenceType(ReturningReferenceType returningReferenceType) {
+ this.returningReferenceType = returningReferenceType;
+ return this;
+ }
+
+ public String getReturningQualifier() {
+ return returningQualifier;
+ }
+
+ public Column setReturningQualifier(String returningQualifier) {
+ this.returningQualifier = returningQualifier;
+ return this;
+ }
+
+ public Column withReturningReference(ReturningReferenceType returningReferenceType,
+ String returningQualifier) {
+ this.returningReferenceType = returningReferenceType;
+ this.returningQualifier = returningQualifier;
+ return this;
+ }
+
public String getCommentText() {
return commentText;
}
diff --git a/src/main/java/net/sf/jsqlparser/schema/MultiPartName.java b/src/main/java/net/sf/jsqlparser/schema/MultiPartName.java
index e2d985bc1..ce954780d 100644
--- a/src/main/java/net/sf/jsqlparser/schema/MultiPartName.java
+++ b/src/main/java/net/sf/jsqlparser/schema/MultiPartName.java
@@ -9,10 +9,12 @@
*/
package net.sf.jsqlparser.schema;
+import java.util.regex.Matcher;
import java.util.regex.Pattern;
public interface MultiPartName {
Pattern LEADING_TRAILING_QUOTES_PATTERN = Pattern.compile("^[\"\\[`]+|[\"\\]`]+$");
+ Pattern BACKTICK_PATTERN = Pattern.compile("`([^`]*)`");
/**
* Removes leading and trailing quotes from a SQL quoted identifier
@@ -27,10 +29,32 @@ static String unquote(String quotedIdentifier) {
}
static boolean isQuoted(String identifier) {
- return identifier!=null && LEADING_TRAILING_QUOTES_PATTERN.matcher(identifier).find();
+ return identifier != null && LEADING_TRAILING_QUOTES_PATTERN.matcher(identifier).find();
}
String getFullyQualifiedName();
String getUnquotedName();
+
+
+ static String replaceBackticksWithDoubleQuotes(String input) {
+ if (input == null || input.isEmpty()) {
+ return input;
+ }
+
+ Matcher matcher = BACKTICK_PATTERN.matcher(input);
+ StringBuilder sb = new StringBuilder();
+ int lastEnd = 0;
+
+ while (matcher.find()) {
+ sb.append(input, lastEnd, matcher.start()); // text before match
+ sb.append('"').append(matcher.group(1)).append('"'); // replace with double quotes
+ lastEnd = matcher.end();
+ }
+
+ sb.append(input.substring(lastEnd)); // append remaining text
+ return sb.toString();
+ }
+
+
}
diff --git a/src/main/java/net/sf/jsqlparser/schema/Sequence.java b/src/main/java/net/sf/jsqlparser/schema/Sequence.java
index 2f813c1d7..764294db6 100644
--- a/src/main/java/net/sf/jsqlparser/schema/Sequence.java
+++ b/src/main/java/net/sf/jsqlparser/schema/Sequence.java
@@ -9,13 +9,12 @@
*/
package net.sf.jsqlparser.schema;
-import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
-
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
/**
* Represents the database type for a {@code SEQUENCE}
@@ -29,6 +28,7 @@ public class Sequence extends ASTNodeAccessImpl implements MultiPartName {
private List partItems = new ArrayList<>();
private List parameters;
+ private String dataType;
public Sequence() {}
@@ -45,6 +45,19 @@ public void setParameters(List parameters) {
this.parameters = parameters;
}
+ public String getDataType() {
+ return dataType;
+ }
+
+ public void setDataType(String dataType) {
+ this.dataType = dataType;
+ }
+
+ public Sequence withDataType(String dataType) {
+ this.setDataType(dataType);
+ return this;
+ }
+
public Database getDatabase() {
return new Database(getIndex(DATABASE_IDX));
}
@@ -129,6 +142,9 @@ public String getUnquotedName() {
@Override
public String toString() {
StringBuilder sql = new StringBuilder(getFullyQualifiedName());
+ if (dataType != null) {
+ sql.append(" AS ").append(dataType);
+ }
if (parameters != null) {
for (Sequence.Parameter parameter : parameters) {
sql.append(" ").append(parameter.formatParameter());
@@ -158,7 +174,7 @@ public Sequence addParameters(Collection extends Parameter> parameters) {
* The available parameters to a sequence
*/
public enum ParameterType {
- INCREMENT_BY, START_WITH, RESTART_WITH, MAXVALUE, NOMAXVALUE, MINVALUE, NOMINVALUE, CYCLE, NOCYCLE, CACHE, NOCACHE, ORDER, NOORDER, KEEP, NOKEEP, SESSION, GLOBAL;
+ INCREMENT_BY, INCREMENT, START_WITH, START, RESTART_WITH, MAXVALUE, NOMAXVALUE, MINVALUE, NOMINVALUE, CYCLE, NOCYCLE, CACHE, NOCACHE, ORDER, NOORDER, KEEP, NOKEEP, SESSION, GLOBAL;
public static ParameterType from(String type) {
return Enum.valueOf(ParameterType.class, type.toUpperCase());
@@ -189,8 +205,12 @@ public String formatParameter() {
switch (option) {
case INCREMENT_BY:
return prefix("INCREMENT BY");
+ case INCREMENT:
+ return prefix("INCREMENT");
case START_WITH:
return prefix("START WITH");
+ case START:
+ return prefix("START");
case RESTART_WITH:
if (value != null) {
return prefix("RESTART WITH");
diff --git a/src/main/java/net/sf/jsqlparser/schema/Table.java b/src/main/java/net/sf/jsqlparser/schema/Table.java
index cd0aa679d..1de14a8a5 100644
--- a/src/main/java/net/sf/jsqlparser/schema/Table.java
+++ b/src/main/java/net/sf/jsqlparser/schema/Table.java
@@ -170,7 +170,49 @@ public String getUnquotedSchemaName() {
}
public Table setSchemaName(String schemaName) {
- this.setIndex(SCHEMA_IDX, schemaName);
+ if (schemaName == null) {
+ setIndex(SCHEMA_IDX, null);
+ return this;
+ }
+
+ // BigQuery seems to allow things like: `catalogName.schemaName.tableName` in only one pair
+ // of quotes
+ // however, some people believe that Dots in Names are a good idea, so provide a switch-off
+ boolean splitNamesOnDelimiter = System.getProperty("SPLIT_NAMES_ON_DELIMITER") == null ||
+ !List
+ .of("0", "N", "n", "FALSE", "false", "OFF", "off")
+ .contains(System.getProperty("SPLIT_NAMES_ON_DELIMITER"));
+
+ if (MultiPartName.isQuoted(schemaName) && schemaName.contains(".")
+ && splitNamesOnDelimiter) {
+ String[] parts = MultiPartName.unquote(schemaName).split("\\.");
+ switch (parts.length) {
+ case 2:
+ setIndex(DATABASE_IDX, "\"" + parts[0] + "\"");
+ setIndex(SCHEMA_IDX, "\"" + parts[1] + "\"");
+ break;
+ case 1:
+ setIndex(SCHEMA_IDX, "\"" + parts[0] + "\"");
+ break;
+ default:
+ throw new RuntimeException("Invalid schema name: " + schemaName);
+ }
+ } else if (schemaName.contains(".") && splitNamesOnDelimiter) {
+ String[] parts = MultiPartName.unquote(schemaName).split("\\.");
+ switch (parts.length) {
+ case 2:
+ setIndex(DATABASE_IDX, parts[0]);
+ setIndex(SCHEMA_IDX, parts[1]);
+ break;
+ case 1:
+ setIndex(SCHEMA_IDX, parts[0]);
+ break;
+ default:
+ throw new RuntimeException("Invalid schema name: " + schemaName);
+ }
+ } else {
+ this.setIndex(SCHEMA_IDX, schemaName);
+ }
return this;
}
@@ -260,6 +302,10 @@ public Table setTimeTravelStrAfterAlias(String timeTravelStrAfterAlias) {
return this;
}
+ public void setNameParts(List nameParts) {
+ this.partItems = nameParts;
+ }
+
private void setIndex(int idx, String value) {
int size = partItems.size();
for (int i = 0; i < idx - size + 1; i++) {
diff --git a/src/main/java/net/sf/jsqlparser/statement/ExplainStatement.java b/src/main/java/net/sf/jsqlparser/statement/ExplainStatement.java
index 048356425..544aedf67 100644
--- a/src/main/java/net/sf/jsqlparser/statement/ExplainStatement.java
+++ b/src/main/java/net/sf/jsqlparser/statement/ExplainStatement.java
@@ -13,16 +13,14 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.stream.Collectors;
-
import net.sf.jsqlparser.schema.Table;
-import net.sf.jsqlparser.statement.select.Select;
/**
* An {@code EXPLAIN} statement
*/
public class ExplainStatement implements Statement {
private String keyword;
- private Select select;
+ private Statement statement;
private LinkedHashMap options;
private Table table;
@@ -37,24 +35,17 @@ public ExplainStatement() {
public ExplainStatement(String keyword, Table table) {
this.keyword = keyword;
this.table = table;
- this.select = null;
}
- public ExplainStatement(String keyword, Select select, List