diff --git a/src/main/java/net/sf/jsqlparser/expression/CastExpression.java b/src/main/java/net/sf/jsqlparser/expression/CastExpression.java index 1c6502d40..99665b3e6 100644 --- a/src/main/java/net/sf/jsqlparser/expression/CastExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/CastExpression.java @@ -23,6 +23,10 @@ public class CastExpression extends ASTNodeAccessImpl implements Expression { private ColDataType colDataType = null; private ArrayList columnDefinitions = new ArrayList<>(); + // BigQuery specific FORMAT clause: + // https://cloud.google.com/bigquery/docs/reference/standard-sql/conversion_functions#cast_as_date + private String format = null; + public CastExpression(String keyword, Expression leftExpression, String dataType) { this.keyword = keyword; this.leftExpression = leftExpression; @@ -89,13 +93,26 @@ public void setUseCastKeyword(boolean useCastKeyword) { } } + public String getFormat() { + return format; + } + + public CastExpression setFormat(String format) { + this.format = format; + return this; + } + @Override public String toString() { + String formatStr = format != null && !format.isEmpty() + ? " FORMAT " + format + : ""; if (keyword != null && !keyword.isEmpty()) { return columnDefinitions.size() > 1 ? keyword + "(" + leftExpression + " AS ROW(" - + Select.getStringList(columnDefinitions) + "))" - : keyword + "(" + leftExpression + " AS " + colDataType.toString() + ")"; + + Select.getStringList(columnDefinitions) + ")" + formatStr + ")" + : keyword + "(" + leftExpression + " AS " + colDataType.toString() + formatStr + + ")"; } else { return leftExpression + "::" + colDataType.toString(); } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java index 068509d25..b3eb781c0 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -704,6 +704,10 @@ public void visit(BitwiseXor bitwiseXor) { @Override public void visit(CastExpression cast) { if (cast.isUseCastKeyword()) { + String formatStr = cast.getFormat() != null && !cast.getFormat().isEmpty() + ? " FORMAT " + cast.getFormat() + : ""; + buffer.append(cast.keyword).append("("); cast.getLeftExpression().accept(this); buffer.append(" AS "); @@ -711,6 +715,7 @@ public void visit(CastExpression cast) { cast.getColumnDefinitions().size() > 1 ? "ROW(" + Select.getStringList(cast.getColumnDefinitions()) + ")" : cast.getColDataType().toString()); + buffer.append(formatStr); buffer.append(")"); } else { cast.getLeftExpression().accept(this); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index c8755ac66..6560588bc 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -5033,6 +5033,7 @@ CastExpression CastExpression(): ColDataType type; Expression expression = null; boolean useCastKeyword; + Token formatCharLiteral; } { ( @@ -5061,6 +5062,11 @@ CastExpression CastExpression(): | type=ColDataType() { retval.setColDataType(type); } ) + + // BigQuery FORMAT clause + // https://cloud.google.com/bigquery/docs/reference/standard-sql/conversion_functions#cast_as_date + [ formatCharLiteral= { retval.setFormat(formatCharLiteral.image); } ] + ")" { diff --git a/src/test/java/net/sf/jsqlparser/expression/CaseExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/CaseExpressionTest.java index e86f92a2b..6c0cbb50c 100644 --- a/src/test/java/net/sf/jsqlparser/expression/CaseExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/CaseExpressionTest.java @@ -160,4 +160,10 @@ void testPerformanceIssue1889() throws JSQLParserException { TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + void testFormatClause() throws JSQLParserException { + String sqlStr = "SELECT CAST('18-12-03' AS DATE FORMAT 'YY-MM-DD') AS string_to_date"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/select/NestedBracketsPerformanceTest.java b/src/test/java/net/sf/jsqlparser/statement/select/NestedBracketsPerformanceTest.java index 7448bc29d..83a4fe7ae 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/NestedBracketsPerformanceTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/NestedBracketsPerformanceTest.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.statement.select; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -353,4 +354,168 @@ public void testDeepFunctionParameters() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + @Disabled + void testIssue1983() throws JSQLParserException { + String sqlStr = "INSERT INTO\n" + + "C01_INDIV_TELBK_CUST_INFO_H_T2 (PARTY_ID, PARTY_SIGN_STAT_CD, SIGN_TM, CLOSE_TM)\n" + + + "SELECT\n" + + "A1.PARTY_ID,\n" + + "A1.PARTY_SIGN_STAT_CD,\n" + + "CAST(\n" + + "(\n" + + "CASE\n" + + "WHEN A1.SIGN_TM IS NULL\n" + + "OR A1.SIGN_TM = '' THEN CAST(\n" + + "CAST(\n" + + "CAST('ATkkIVQJZm' AS DATE FORMAT 'YYYYMMDD') AS DATE\n" + + ") || ' 00:00:00' AS TIMESTAMP\n" + + ")\n" + + "WHEN CHARACTERS (TRIM(A1.SIGN_TM)) <> 19\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 1, 1) < '0'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 1, 1) > '9'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 2, 1) < '0'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 2, 1) > '9'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 3, 1) < '0'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 3, 1) > '9'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 4, 1) < '0'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 4, 1) > '9'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 6, 1) < '0'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 6, 1) > '1'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 7, 1) < '0'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 7, 1) > '9'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 9, 1) < '0'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 9, 1) > '3'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 10, 1) < '0'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 10, 1) > '9'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 1, 4) = '0000'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 6, 2) = '00'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 9, 2) = '00'\n" + + "OR SUBSTR (TRIM(A1.SIGN_TM), 1, 1) = '0' THEN CAST(\n" + + "CAST(\n" + + "CAST('cDXtwdFyky' AS DATE FORMAT 'YYYYMMDD') AS DATE\n" + + ") || ' 00:00:00' AS TIMESTAMP\n" + + ")\n" + + "ELSE (\n" + + "CASE\n" + + "WHEN (\n" + + "CAST(SUBSTR (TRIM(A1.SIGN_TM), 9, 2) AS INTEGER) < 29\n" + + "AND SUBSTR (TRIM(A1.SIGN_TM), 6, 2) = '02'\n" + + ")\n" + + "OR (\n" + + "CAST(SUBSTR (TRIM(A1.SIGN_TM), 9, 2) AS INTEGER) < 31\n" + + "AND SUBSTR (TRIM(A1.SIGN_TM), 6, 2) <> '02'\n" + + "AND SUBSTR (TRIM(A1.SIGN_TM), 6, 2) <= 12\n" + + ")\n" + + "OR (\n" + + "CAST(SUBSTR (TRIM(A1.SIGN_TM), 9, 2) AS INTEGER) = 31\n" + + "AND SUBSTR (TRIM(A1.SIGN_TM), 6, 2) IN ('01', '03', '05', '07', '08', '10', '12')\n" + + + ") THEN CAST(A1.SIGN_TM AS TIMESTAMP)\n" + + "WHEN SUBSTR (TRIM(A1.SIGN_TM), 6, 2) || SUBSTR (TRIM(A1.SIGN_TM), 9, 2) = '0229'\n" + + + "AND (\n" + + "CAST(SUBSTR (TRIM(A1.SIGN_TM), 1, 4) AS INTEGER) MOD 400 = 0\n" + + "OR (\n" + + "CAST(SUBSTR (TRIM(A1.SIGN_TM), 1, 4) AS INTEGER) MOD 4 = 0\n" + + "AND CAST(SUBSTR (TRIM(A1.SIGN_TM), 1, 4) AS INTEGER) MOD 100 <> 0\n" + + ")\n" + + ") THEN CAST(A1.SIGN_TM AS TIMESTAMP)\n" + + "ELSE CAST(\n" + + "CAST(\n" + + "CAST('cDXtwdFyky' AS DATE FORMAT 'YYYYMMDD') AS DATE\n" + + ") || ' 00:00:00' AS TIMESTAMP\n" + + ")\n" + + "END\n" + + ")\n" + + "END\n" + + ") AS DATE FORMAT 'YYYYMMDD'\n" + + "),\n" + + "CAST(\n" + + "(\n" + + "CASE\n" + + "WHEN A1.CLOSE_TM IS NULL\n" + + "OR A1.CLOSE_TM = '' THEN CAST(\n" + + "CAST(\n" + + "CAST('ATkkIVQJZm' AS DATE FORMAT 'YYYYMMDD') AS DATE\n" + + ") || ' 00:00:00' AS TIMESTAMP\n" + + ")\n" + + "WHEN CHARACTERS (TRIM(A1.CLOSE_TM)) <> 19\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 1, 1) < '0'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 1, 1) > '9'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 2, 1) < '0'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 2, 1) > '9'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 3, 1) < '0'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 3, 1) > '9'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 4, 1) < '0'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 4, 1) > '9'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 6, 1) < '0'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 6, 1) > '1'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 7, 1) < '0'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 7, 1) > '9'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 9, 1) < '0'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 9, 1) > '3'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 10, 1) < '0'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 10, 1) > '9'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 1, 4) = '0000'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 6, 2) = '00'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 9, 2) = '00'\n" + + "OR SUBSTR (TRIM(A1.CLOSE_TM), 1, 1) = '0' THEN CAST(\n" + + "CAST(\n" + + "CAST('cDXtwdFyky' AS DATE FORMAT 'YYYYMMDD') AS DATE\n" + + ") || ' 00:00:00' AS TIMESTAMP\n" + + ")\n" + + "ELSE (\n" + + "CASE\n" + + "WHEN (\n" + + "CAST(SUBSTR (TRIM(A1.CLOSE_TM), 9, 2) AS INTEGER) < 29\n" + + "AND SUBSTR (TRIM(A1.CLOSE_TM), 6, 2) = '02'\n" + + ")\n" + + "OR (\n" + + "CAST(SUBSTR (TRIM(A1.CLOSE_TM), 9, 2) AS INTEGER) < 31\n" + + "AND SUBSTR (TRIM(A1.CLOSE_TM), 6, 2) <> '02'\n" + + "AND SUBSTR (TRIM(A1.CLOSE_TM), 6, 2) <= 12\n" + + ")\n" + + "OR (\n" + + "CAST(SUBSTR (TRIM(A1.CLOSE_TM), 9, 2) AS INTEGER) = 31\n" + + "AND SUBSTR (TRIM(A1.CLOSE_TM), 6, 2) IN ('01', '03', '05', '07', '08', '10', '12')\n" + + + ") THEN CAST(A1.CLOSE_TM AS TIMESTAMP)\n" + + "WHEN SUBSTR (TRIM(A1.CLOSE_TM), 6, 2) || SUBSTR (TRIM(A1.CLOSE_TM), 9, 2) = '0229'\n" + + + "AND (\n" + + "CAST(SUBSTR (TRIM(A1.CLOSE_TM), 1, 4) AS INTEGER) MOD 400 = 0\n" + + "OR (\n" + + "CAST(SUBSTR (TRIM(A1.CLOSE_TM), 1, 4) AS INTEGER) MOD 4 = 0\n" + + "AND CAST(SUBSTR (TRIM(A1.CLOSE_TM), 1, 4) AS INTEGER) MOD 100 <> 0\n" + + ")\n" + + ") THEN CAST(A1.CLOSE_TM AS TIMESTAMP)\n" + + "ELSE CAST(\n" + + "CAST(\n" + + "CAST('cDXtwdFyky' AS DATE FORMAT 'YYYYMMDD') AS DATE\n" + + ") || ' 00:00:00' AS TIMESTAMP\n" + + ")\n" + + "END\n" + + ")\n" + + "END\n" + + ") AS DATE FORMAT 'YYYYMMDD'\n" + + ")\n" + + "FROM\n" + + "T01_PTY_SIGN_H_T1 A1\n" + + "WHERE\n" + + "A1.PARTY_SIGN_TYPE_CD = 'CD_021'\n" + + "AND A1.ST_DT <= CAST('LDBCGtCIyo' AS DATE FORMAT 'YYYYMMDD')\n" + + "AND A1.END_DT > CAST('LDBCGtCIyo' AS DATE FORMAT 'YYYYMMDD')\n" + + "GROUP BY\n" + + "1,\n" + + "2,\n" + + "3,\n" + + "4"; + CCJSqlParserUtil.parse(sqlStr, parser -> parser + .withSquareBracketQuotation(false) + .withAllowComplexParsing(true) + .withTimeOut(60000)); + } }