Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 147 additions & 10 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,82 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
return left;
}

/**
* Whether the token after the ":" of a JSON path is a word that is no identifier token, i.e. a
* keyword, which names the path element all the same. A word followed by "(" stays a function.
*/
private boolean isKeywordJsonPathName() {
Token colon = getToken(0);
return colon != null && ":".equals(colon.image) && isKeywordWord(1);
}

/**
* Whether the token at the given position is a word that is no identifier token, i.e. a keyword,
* and not the name of a function call.
*/
private boolean isKeywordWord(int position) {
Token word = getToken(position);
if (word.image == null || word.kind == S_IDENTIFIER || word.kind == S_QUOTED_IDENTIFIER
|| "(".equals(getToken(position + 1).image)) {
return false;
}
String image = word.image;
if (image.isEmpty() || (!Character.isLetter(image.charAt(0)) && image.charAt(0) != '_')) {
return false;
}
for (int i = 1; i < image.length(); i++) {
if (!Character.isLetterOrDigit(image.charAt(i)) && image.charAt(i) != '_') {
return false;
}
}
return true;
}

/**
* Whether the path after the ":" of a JSON path, which starts with an identifier, goes on with a
* keyword after a ".", e.g. v:a.output or v:a[0].b.from. The expression parser would read such a
* path as a qualified column name, which does not take a keyword as a part.
*/
private boolean isJsonPathWithKeywordAfterDot() {
Token colon = getToken(0);
if (colon == null || !":".equals(colon.image)) {
return false;
}
Token first = getToken(1);
if (first.kind != S_IDENTIFIER && first.kind != S_QUOTED_IDENTIFIER) {
return false;
}
int position = 2;
while (true) {
Token token = getToken(position);
if (".".equals(token.image)) {
if (isKeywordWord(position + 1)) {
return true;
}
Token name = getToken(position + 1);
if (name.kind != S_IDENTIFIER && name.kind != S_QUOTED_IDENTIFIER) {
return false;
}
position += 2;
} else if ("[".equals(token.image)) {
int depth = 0;
do {
token = getToken(position);
if ("[".equals(token.image)) {
depth++;
} else if ("]".equals(token.image)) {
depth--;
} else if (token.kind == EOF) {
return false;
}
position++;
} while (depth > 0);
} else {
return false;
}
}
}

private boolean isPostgreSqlJsonAccess(Token token) {
return ("->".equals(token.image) || "->>".equals(token.image)
|| "#>".equals(token.image) || "#>>".equals(token.image))
Expand Down Expand Up @@ -14586,11 +14662,7 @@ Expression PrimaryExpression() #PrimaryExpression:
token="#>>"
)

(
LOOKAHEAD({getAsBoolean(Feature.allowComplexParsing) && !interrupted }) expression=Expression()
|
expression=SimpleExpression()
)
expression=JsonPathElement()
{
if (jsonIdents == null) {
jsonIdents = new ArrayList<Map.Entry<Expression, String>>();
Expand Down Expand Up @@ -14948,6 +15020,75 @@ StructType StructType() #StruckType:
}
}

/**
* The element after a ":" in a JSON path (Snowflake, Databricks). A path name is mandatory there,
* so any word is a name, reserved or not: v:from::INT, v:select, v:output.x[0].
*/
Expression JsonPathElement() #void:
{ Expression element; }
{
(
LOOKAHEAD({ isKeywordJsonPathName() || isJsonPathWithKeywordAfterDot() }) element=JsonPathKeyword()
|
LOOKAHEAD({getAsBoolean(Feature.allowComplexParsing) && !interrupted}) element=Expression()
|
element=SimpleExpression()
)
{ return element; }
}

/**
* A keyword used as a JSON path name. The token is taken as it is, as no token kind in the grammar
* stands for "any keyword", then the path name continues like a primary expression: further
* navigation steps, casts and the arithmetic tier SimpleExpression() also applies.
*/
Expression JsonPathKeyword() #void:
{
Token first = getToken(1);
Token keyword;
Expression element;
Expression step;
ColDataType type;
CastExpression cast;
}
{
{
keyword = getNextToken();
element = linkToLastToken(new Column(keyword.image));
}
( LOOKAHEAD(2) step=JsonPathStep(element) { element = linked(step, first); } )*
(
LOOKAHEAD(2) "::" type=ColDataType() {
cast = new CastExpression();
cast.setUseCastKeyword(false);
cast.setLeftExpression(element);
cast.setColDataType(type);
element = linked(cast, first);
}
)*
{
int arithFloor = Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) ? 2 : 4;
return prattArithRest(element, first, arithFloor);
}
}

/**
* A navigation step within a JSON path, where a keyword after a "." names a path element as well:
* v:a.output, v:a[0].from.
*/
Expression JsonPathStep(Expression base) #void:
{ Expression step; }
{
(
LOOKAHEAD({ ".".equals(getToken(1).image) && isKeywordWord(2) }) "." {
step = new RowGetExpression(base, getNextToken().image);
}
|
step=NavigationStep(base)
)
{ return step; }
}

JsonExpression JsonExpression(Expression expr, List<Map.Entry<Expression, String>> idents) : {
JsonExpression result = new JsonExpression(expr, idents);
Expression expression;
Expand Down Expand Up @@ -14991,11 +15132,7 @@ JsonExpression JsonExpression(Expression expr, List<Map.Entry<Expression, String
token="#>>"
)

(
LOOKAHEAD({getAsBoolean(Feature.allowComplexParsing) && !interrupted}) expression=Expression()
|
expression=SimpleExpression()
)
expression=JsonPathElement()
{
result.addIdent( expression, token.image );
}
Expand Down
94 changes: 94 additions & 0 deletions src/test/java/net/sf/jsqlparser/expression/JsonExpressionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
package net.sf.jsqlparser.expression;

import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.select.PlainSelect;
import org.junit.jupiter.api.Assertions;
Expand Down Expand Up @@ -55,6 +56,99 @@ void testHashArrowOperatorsRoundTrip() throws JSQLParserException {
}
}

@ParameterizedTest
@ValueSource(strings = {"select", "from", "where", "join", "union", "and", "or", "not", "case",
"when", "else", "as", "is", "like", "having", "distinct", "inner", "into", "with",
"outer", "cross", "using", "output", "null", "order", "group"})
void testSnowflakeKeywordAsPathName(String keyword) throws JSQLParserException {
String sqlStr = "SELECT v:" + keyword + "::INT, v:" + keyword + ".x[0] FROM vartab";
PlainSelect st = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sqlStr, true);
Assertions.assertInstanceOf(JsonExpression.class, st.getSelectItem(0).getExpression());
Assertions.assertInstanceOf(JsonExpression.class, st.getSelectItem(1).getExpression());
}

@Test
void testSnowflakeKeywordPathNameBeforeClause() throws JSQLParserException {
assertSqlCanBeParsedAndDeparsed(
"SELECT v:from FROM t WHERE v:where::INT = 1 ORDER BY v:order",
true);
}

@Test
void testSnowflakeKeywordPathNameInInsertSelect() throws JSQLParserException {
assertSqlCanBeParsedAndDeparsed("INSERT INTO \"MY_DB\".\"MY_SCHEMA\".\"T\" (\"c\")\n"
+ "SELECT \"F\".\"DATA_VALUE\":window::VARCHAR FROM \"MY_DB\".\"MY_SCHEMA\".\"RAW_TRACES\" AS \"F\"",
true);
}

@Test
void testSnowflakeKeywordPathNameOnLateralFlatten() throws JSQLParserException {
assertSqlCanBeParsedAndDeparsed("INSERT INTO \"MY_DB\".\"MY_SCHEMA\".\"OBS\" (\"o\")\n"
+ "SELECT \"obs\".VALUE:output::VARIANT\n"
+ "FROM \"MY_DB\".\"MY_SCHEMA\".\"RAW_TRACES\" AS \"F\",\n"
+ " lateral flatten(input=>\"F\".\"DATA_VALUE\":observations, outer => true, mode => 'ARRAY') AS \"obs\"",
true);
}

@ParameterizedTest
@ValueSource(strings = {"SELECT v:from::INT, v:output.x[0] FROM t",
"SELECT v:select + 1, v:where * 2 - v:from FROM t",
"SELECT v:window::INT + 1 FROM t WHERE v:order::INT > 2"})
void testKeywordPathNameDatabricksAndMysqlArithmetic(String sql) throws JSQLParserException {
assertSqlCanBeParsedAndDeparsed(sql, true,
parser -> parser.withDialect(Dialect.DATABRICKS));
assertSqlCanBeParsedAndDeparsed(sql, true, parser -> parser.withDialect(Dialect.SNOWFLAKE));
assertSqlCanBeParsedAndDeparsed(sql, true, parser -> parser.withDialect(Dialect.MYSQL));
}

@ParameterizedTest
@ValueSource(strings = {
"SELECT coalesce(v:from, v:select::INT) FROM t",
"SELECT CASE WHEN v:when::INT = 1 THEN v:else ELSE v:end END FROM t",
"SELECT * FROM (SELECT v:from AS a FROM t) x WHERE x.a IN (SELECT v:where FROM u)",
"SELECT * FROM t WHERE EXISTS (SELECT 1 FROM u WHERE u.v:order::INT = t.id)",
"SELECT f(v:window, (SELECT w:output FROM u)) FROM t"})
void testKeywordPathNameInsideNestedConstructs(String sql) throws JSQLParserException {
assertSqlCanBeParsedAndDeparsed(sql, true, parser -> parser.withDialect(Dialect.SNOWFLAKE));
}

@ParameterizedTest
@ValueSource(strings = {"select", "from", "where", "join", "union", "and", "or", "not", "case",
"when", "else", "as", "is", "like", "having", "distinct", "inner", "into", "with",
"outer", "cross", "using", "output", "null", "true", "false", "order", "group"})
void testSnowflakeKeywordAfterDotInPath(String keyword) throws JSQLParserException {
String sqlStr = "SELECT v:a." + keyword + ", v:a.b." + keyword + "::INT, v:a[0]." + keyword
+ ".x[1] FROM vartab";
PlainSelect st = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sqlStr, true);
for (int i = 0; i < 3; i++) {
Assertions.assertInstanceOf(JsonExpression.class, st.getSelectItem(i).getExpression());
}
}

@ParameterizedTest
@ValueSource(strings = {
"SELECT v:a.output + 1, \"F\".\"X\":\"a\".from::VARIANT AS y FROM t F",
"SELECT * FROM t WHERE v:payload.output::INT = 1 ORDER BY v:payload.from",
"SELECT v:a.output FROM t, lateral flatten(input => t.v:a.input.where) f"})
void testKeywordAfterDotInPathInContext(String sql) throws JSQLParserException {
assertSqlCanBeParsedAndDeparsed(sql, true, parser -> parser.withDialect(Dialect.SNOWFLAKE));
}

@Test
void testKeywordAfterDotOutsideJsonPathIsStillRejected() {
// a reserved word is not a part of a qualified name, only of a JSON path
Assertions.assertThrows(JSQLParserException.class,
() -> CCJSqlParserUtil.parse("SELECT t.output FROM t",
parser -> parser.withDialect(Dialect.SNOWFLAKE)));
}

@Test
void testKeywordFollowedByParenthesisStaysFunction() throws JSQLParserException {
// only parsing is asserted: a function behind ":" deparses with "."
Assertions.assertNotNull(CCJSqlParserUtil.parse("SELECT v:left(x) FROM t",
parser -> parser.withDialect(Dialect.SNOWFLAKE)));
}

@Test
void testSnowflakeGetOperator() throws JSQLParserException {
// https://docs.snowflake.com/en/user-guide/querying-semistructured
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,12 @@ public void testStatementsTheCorpusLacks() throws JSQLParserException {
"DELETE FROM t USING s JOIN u ON s.id = u.id, v WHERE t.id = s.id"}) {
new Walk(sql, sql, problems).check(CCJSqlParserUtil.parseStatements(sql));
}
for (String sql : new String[] {"SELECT v:select FROM t", "SELECT v:select::INT FROM t",
"SELECT v:select.x[0], v:from::INT + 1 FROM t",
"SELECT v:a.output, v:a.b.from::INT, v:a[0].from.x[1] + 1 FROM t"}) {
new Walk(sql, sql, problems).check(CCJSqlParserUtil.parseStatements(sql,
parser -> parser.withDialect(AbstractJSqlParser.Dialect.SNOWFLAKE)));
}
String block = "BEGIN x := 1; NULL; foo(1); END;";
new Walk(block, block, problems).check(CCJSqlParserUtil.parseStatements(block,
parser -> parser.withDialect(AbstractJSqlParser.Dialect.ORACLE)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1388,4 +1388,21 @@ void testPostgreSqlDeclarativePartitionChildrenIssue2522() throws JSQLParserExce
defaultSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL));
assertEquals(PartitionBound.Type.DEFAULT, defaultPartition.getPartitionBound().getType());
}

@Test
void testCreateOrReplaceTableCopyGrantsAsSelectFromFlattenedVariant()
throws JSQLParserException {
String sql =
"CREATE OR REPLACE TABLE \"MY_DB\".\"MY_SCHEMA\".\"STAGING_TRACES\" COPY GRANTS\n"
+ "AS (SELECT\n"
+ " *\n"
+ "FROM (SELECT\n"
+ " \"FLATTEN_VARIANT_INPUT\".\"DATA_VALUE\":id::VARCHAR AS \"id\",\n"
+ " \"FLATTEN_VARIANT_INPUT\".\"DATA_VALUE\":input::VARIANT AS \"input\",\n"
+ " \"FLATTEN_VARIANT_INPUT\".\"DATA_VALUE\":output::VARIANT AS \"output\"\n"
+ "FROM (SELECT\n"
+ " \"DATA_VALUE\"\n"
+ " FROM \"MY_DB\".\"MY_SCHEMA\".\"RAW_TRACES\") AS \"FLATTEN_VARIANT_INPUT\"))";
assertSqlCanBeParsedAndDeparsed(sql, true, parser -> parser.withDialect(Dialect.SNOWFLAKE));
}
}