Skip to content

Commit

Permalink
feat: Google BigQuery CAST with FORMAT clause
Browse files Browse the repository at this point in the history
Signed-off-by: Andreas Reichel <[email protected]>
  • Loading branch information
manticore-projects committed Mar 31, 2024
1 parent 236793a commit 0d813f0
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 2 deletions.
21 changes: 19 additions & 2 deletions src/main/java/net/sf/jsqlparser/expression/CastExpression.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public class CastExpression extends ASTNodeAccessImpl implements Expression {
private ColDataType colDataType = null;
private ArrayList<ColumnDefinition> 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;
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -704,13 +704,18 @@ 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 ");
buffer.append(
cast.getColumnDefinitions().size() > 1
? "ROW(" + Select.getStringList(cast.getColumnDefinitions()) + ")"
: cast.getColDataType().toString());
buffer.append(formatStr);
buffer.append(")");
} else {
cast.getLeftExpression().accept(this);
Expand Down
6 changes: 6 additions & 0 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -5033,6 +5033,7 @@ CastExpression CastExpression():
ColDataType type;
Expression expression = null;
boolean useCastKeyword;
Token formatCharLiteral;
}
{
(
Expand Down Expand Up @@ -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
[ <K_FORMAT> formatCharLiteral=<S_CHAR_LITERAL> { retval.setFormat(formatCharLiteral.image); } ]

")"

{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
}

0 comments on commit 0d813f0

Please sign in to comment.