diff --git a/impl/src/main/java/com/force/formula/commands/FunctionDatetimeValue.java b/impl/src/main/java/com/force/formula/commands/FunctionDatetimeValue.java index 2032620f..c7bd2606 100644 --- a/impl/src/main/java/com/force/formula/commands/FunctionDatetimeValue.java +++ b/impl/src/main/java/com/force/formula/commands/FunctionDatetimeValue.java @@ -2,7 +2,7 @@ import java.lang.reflect.Type; import java.math.BigDecimal; -import java.text.ParseException; +import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Deque; @@ -19,6 +19,7 @@ import com.force.formula.FormulaProperties; import com.force.formula.FormulaRuntimeContext; import com.force.formula.impl.FormulaAST; +import com.force.formula.impl.FormulaSqlHooks; import com.force.formula.impl.IllegalArgumentTypeException; import com.force.formula.impl.JsValue; import com.force.formula.impl.TableAliasRegistry; @@ -56,17 +57,19 @@ public SQLPair getSQL(FormulaAST node, FormulaContext context, String[] args, St sql = args[0]; guard = SQLPair.generateGuard(guards, null); } else { - sql = String.format(getSqlHooks(context).sqlToTimestampIso(), args[0]); + FormulaSqlHooks hooks = getSqlHooks(context); + sql = String.format(hooks.sqlToTimestampIso(), args[0]); FormulaAST child = (FormulaAST)node.getFirstChild(); if (child != null && child.isLiteral() && child.getDataType() == String.class) { - if (OperatorDatetimeValueFormulaCommand.isValidDateTime(ConstantString.getStringValue(child, true))) { + boolean noExtraChars = !hooks.isPostgresStyle(); // Postgres works like JDK, so allow it. + if (OperatorDatetimeValueFormulaCommand.isValidDateTime(ConstantString.getStringValue(child, true), noExtraChars)) { // no guard needed guard = SQLPair.generateGuard(guards, null); } else { // we know it's false guard = SQLPair.generateGuard(guards, "0=0"); - sql = "NULL"; + sql = hooks.sqlNullToDate(); } } else { // Guard protects against malformed dates as strings @@ -133,7 +136,7 @@ public void execute(FormulaRuntimeContext context, Deque stack) throws F value = new FormulaDateTime((Date)input); } else { try { - value = parseDateTime(checkStringType(input)); + value = parseDateTime(checkStringType(input), false); } catch (FormulaDateException ex) { FormulaEngine.getHooks().handleFormulaDateException(ex); } @@ -143,9 +146,14 @@ public void execute(FormulaRuntimeContext context, Deque stack) throws F stack.push(value); } - protected static boolean isValidDateTime(String datetime) { + /** + * @return whether datetime is a valid value + * @param datetime the string of the format "yyyy-MM-dd HH:mm:ss" + * @param forSqlGeneration whether to be "strict" and only allow whitespace after parsing for sql generation + */ + protected static boolean isValidDateTime(String datetime, boolean forSqlGeneration) { try { - parseDateTime(datetime); + parseDateTime(datetime, forSqlGeneration); return true; } catch (FormulaDateException x) { return false; @@ -153,20 +161,30 @@ protected static boolean isValidDateTime(String datetime) { } private static Pattern DATE_PATTERN = Pattern.compile("\\d{4}-.*"); - protected static FormulaDateTime parseDateTime(String input) throws FormulaDateException { + /** + * @param input the date time to parse + * @param forSqlGeneration means that only whitespace is allowed at the end of the date format string, as is required for oracle + * @return the DateTime of the parsed value + * @throws FormulaDateException + */ + protected static FormulaDateTime parseDateTime(String input, boolean forSqlGeneration) throws FormulaDateException { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); dateFormat.setLenient(false); dateFormat.setTimeZone(BaseLocalizer.GMT_TZ); - try { - // Do a pre-check for 4-digit year (setLenient does not require this) - if (!DATE_PATTERN.matcher(input).matches()) { - throw new FormulaDateException("Invalid year for DATEVALUE function"); - } - return new FormulaDateTime(dateFormat.parse(input)); + + // Do a pre-check for 4-digit year (setLenient does not require this) + if (!DATE_PATTERN.matcher(input).matches()) { + throw new FormulaDateException("Invalid year for DATEVALUE function"); } - catch (ParseException x) { - throw new FormulaDateException(x); + ParsePosition p = new ParsePosition(0); + Date ret = dateFormat.parse(input, p); + // JDK's Date Format (and postgres) ignores characters past the date format, but Oracle and javascript do not. + // So for non-literal values, we don't allow extra characters, but for literal values we do, but only + // in Java. We don't know if we're generating SQL or JS at this point, so we allow it for backwards compatibilty. + if (ret == null || p.getErrorIndex() != -1 || (forSqlGeneration && !input.substring(p.getIndex()).isBlank())) { + throw new FormulaDateException("Invalid date format: " + input); } + return new FormulaDateTime(ret); } } diff --git a/impl/src/test/goldfiles/FormulaFields/testDateTimeValueWithInvalidString.xml b/impl/src/test/goldfiles/FormulaFields/testDateTimeValueWithInvalidString.xml index 96f0c5ef..2ca9fcbe 100644 --- a/impl/src/test/goldfiles/FormulaFields/testDateTimeValueWithInvalidString.xml +++ b/impl/src/test/goldfiles/FormulaFields/testDateTimeValueWithInvalidString.xml @@ -2,11 +2,11 @@ - NULL + CAST(NULL AS DATE) 0=0 - NULL + CAST(NULL AS DATE) 0=0 new Date("sample " + ' GMT') diff --git a/impl/src/test/goldfiles/FormulaFields/testDateTimeValueWithTimeZone.xml b/impl/src/test/goldfiles/FormulaFields/testDateTimeValueWithTimeZone.xml new file mode 100644 index 00000000..54829b30 --- /dev/null +++ b/impl/src/test/goldfiles/FormulaFields/testDateTimeValueWithTimeZone.xml @@ -0,0 +1,29 @@ + + + + + TO_TIMESTAMP('2021-08-12 03:26:09+1100', 'YYYY-MM-DD HH24:MI:SS') + null + + + TO_TIMESTAMP('2021-08-12 03:26:09+1100', 'YYYY-MM-DD HH24:MI:SS') + null + + new Date("2021-08-12 03:26:09+1100" + ' GMT') + new Date("2021-08-12 03:26:09+1100" + ' GMT') + new Date("2021-08-12 03:26:09+1100" + ' GMT') + new Date("2021-08-12 03:26:09+1100" + ' GMT') + + + No data + Thu Aug 12 03:26:09 GMT 2021 + 2021-08-12 03:26:09.0 + Error: Cannot convert 'NaN'(language: Java, type: java.lang.Double) to Java type 'long' using Value.asLong(): Invalid or lossy primitive coercion. You can ensure that the value can be converted using Value.fitsInLong(). + Error: Cannot convert 'NaN'(language: Java, type: java.lang.Double) to Java type 'long' using Value.asLong(): Invalid or lossy primitive coercion. You can ensure that the value can be converted using Value.fitsInLong(). + Thu Aug 12 03:26:09 GMT 2021 + 2021-08-12 03:26:09.0 + Error: Cannot convert 'NaN'(language: Java, type: java.lang.Double) to Java type 'long' using Value.asLong(): Invalid or lossy primitive coercion. You can ensure that the value can be converted using Value.fitsInLong(). + Error: Cannot convert 'NaN'(language: Java, type: java.lang.Double) to Java type 'long' using Value.asLong(): Invalid or lossy primitive coercion. You can ensure that the value can be converted using Value.fitsInLong(). + + + diff --git a/impl/src/test/goldfiles/FormulaFields/testIfErrorDateTimeValueWithInvalidString.xml b/impl/src/test/goldfiles/FormulaFields/testIfErrorDateTimeValueWithInvalidString.xml index ba44f89f..72141ebf 100644 --- a/impl/src/test/goldfiles/FormulaFields/testIfErrorDateTimeValueWithInvalidString.xml +++ b/impl/src/test/goldfiles/FormulaFields/testIfErrorDateTimeValueWithInvalidString.xml @@ -2,11 +2,11 @@ - CASE WHEN 0=0 THEN TO_TIMESTAMP('2005-11-15 17:00:00 ', 'YYYY-MM-DD HH24:MI:SS') ELSE NULL END + CASE WHEN 0=0 THEN TO_TIMESTAMP('2005-11-15 17:00:00 ', 'YYYY-MM-DD HH24:MI:SS') ELSE CAST(NULL AS DATE) END null - CASE WHEN 0=0 THEN TO_TIMESTAMP('2005-11-15 17:00:00 ', 'YYYY-MM-DD HH24:MI:SS') ELSE NULL END + CASE WHEN 0=0 THEN TO_TIMESTAMP('2005-11-15 17:00:00 ', 'YYYY-MM-DD HH24:MI:SS') ELSE CAST(NULL AS DATE) END null (Object.prototype.toString.call(new Date("sample " + ' GMT')) !== '[object Date]') || isNaN(new Date("sample " + ' GMT').getTime())?new Date("2005-11-15 17:00:00 " + ' GMT'):new Date("sample " + ' GMT') diff --git a/impl/src/test/resources/com/force/formula/impl/formulatests.xml b/impl/src/test/resources/com/force/formula/impl/formulatests.xml index c2f9c8c2..335c66e4 100644 --- a/impl/src/test/resources/com/force/formula/impl/formulatests.xml +++ b/impl/src/test/resources/com/force/formula/impl/formulatests.xml @@ -19,6 +19,13 @@ author: Srikanth Yendluri/Doug Chasman labelName="testDateTimeValueWithValidString" dataType="DateOnly" eval="formula,template" scale="2" precision="12" code="DATETIMEVALUE("2005-11-15 17:00:00 ")"> + + - NULL + CAST(NULL AS DATE) 0=0 - NULL + CAST(NULL AS DATE) 0=0 diff --git a/mysql-test/src/test/goldfiles/FormulaFields/MySQL/testDateTimeValueWithTimeZone.xml b/mysql-test/src/test/goldfiles/FormulaFields/MySQL/testDateTimeValueWithTimeZone.xml new file mode 100644 index 00000000..d7584495 --- /dev/null +++ b/mysql-test/src/test/goldfiles/FormulaFields/MySQL/testDateTimeValueWithTimeZone.xml @@ -0,0 +1,20 @@ + + + + + CAST(NULL AS DATE) + 0=0 + + + CAST(NULL AS DATE) + 0=0 + + + No data + Thu Aug 12 03:26:09 GMT 2021 + null + Thu Aug 12 03:26:09 GMT 2021 + null + + + diff --git a/mysql-test/src/test/goldfiles/FormulaFields/MySQL/testIfErrorDateTimeValueWithInvalidString.xml b/mysql-test/src/test/goldfiles/FormulaFields/MySQL/testIfErrorDateTimeValueWithInvalidString.xml index fb86b38d..ae8933f8 100644 --- a/mysql-test/src/test/goldfiles/FormulaFields/MySQL/testIfErrorDateTimeValueWithInvalidString.xml +++ b/mysql-test/src/test/goldfiles/FormulaFields/MySQL/testIfErrorDateTimeValueWithInvalidString.xml @@ -2,11 +2,11 @@ - CASE WHEN 0=0 THEN TIMESTAMP('2005-11-15 17:00:00 ') ELSE NULL END + CASE WHEN 0=0 THEN TIMESTAMP('2005-11-15 17:00:00 ') ELSE CAST(NULL AS DATE) END null - CASE WHEN 0=0 THEN TIMESTAMP('2005-11-15 17:00:00 ') ELSE NULL END + CASE WHEN 0=0 THEN TIMESTAMP('2005-11-15 17:00:00 ') ELSE CAST(NULL AS DATE) END null diff --git a/oracle-test/src/test/goldfiles/FormulaFields/testDateTimeValueWithInvalidString.xml b/oracle-test/src/test/goldfiles/FormulaFields/testDateTimeValueWithInvalidString.xml index 0e691850..94f90170 100644 --- a/oracle-test/src/test/goldfiles/FormulaFields/testDateTimeValueWithInvalidString.xml +++ b/oracle-test/src/test/goldfiles/FormulaFields/testDateTimeValueWithInvalidString.xml @@ -2,11 +2,11 @@ - NULL + TO_DATE(NULL) 0=0 - NULL + TO_DATE(NULL) 0=0 diff --git a/oracle-test/src/test/goldfiles/FormulaFields/testDateTimeValueWithTimeZone.xml b/oracle-test/src/test/goldfiles/FormulaFields/testDateTimeValueWithTimeZone.xml new file mode 100644 index 00000000..367efd63 --- /dev/null +++ b/oracle-test/src/test/goldfiles/FormulaFields/testDateTimeValueWithTimeZone.xml @@ -0,0 +1,20 @@ + + + + + TO_DATE(NULL) + 0=0 + + + TO_DATE(NULL) + 0=0 + + + No data + Thu Aug 12 03:26:09 GMT 2021 + null + Thu Aug 12 03:26:09 GMT 2021 + null + + + diff --git a/oracle-test/src/test/goldfiles/FormulaFields/testIfErrorDateTimeValueWithInvalidString.xml b/oracle-test/src/test/goldfiles/FormulaFields/testIfErrorDateTimeValueWithInvalidString.xml index 8de7f9a3..4384b851 100644 --- a/oracle-test/src/test/goldfiles/FormulaFields/testIfErrorDateTimeValueWithInvalidString.xml +++ b/oracle-test/src/test/goldfiles/FormulaFields/testIfErrorDateTimeValueWithInvalidString.xml @@ -2,11 +2,11 @@ - CASE WHEN 0=0 THEN TO_DATE('2005-11-15 17:00:00 ', 'YYYY-MM-DD HH24:MI:SS') ELSE NULL END + CASE WHEN 0=0 THEN TO_DATE('2005-11-15 17:00:00 ', 'YYYY-MM-DD HH24:MI:SS') ELSE TO_DATE(NULL) END null - CASE WHEN 0=0 THEN TO_DATE('2005-11-15 17:00:00 ', 'YYYY-MM-DD HH24:MI:SS') ELSE NULL END + CASE WHEN 0=0 THEN TO_DATE('2005-11-15 17:00:00 ', 'YYYY-MM-DD HH24:MI:SS') ELSE TO_DATE(NULL) END null diff --git a/sqlserver-test/src/test/goldfiles/FormulaFields/testDateTimeValueWithInvalidString.xml b/sqlserver-test/src/test/goldfiles/FormulaFields/testDateTimeValueWithInvalidString.xml index a3e621ad..3f6303fa 100644 --- a/sqlserver-test/src/test/goldfiles/FormulaFields/testDateTimeValueWithInvalidString.xml +++ b/sqlserver-test/src/test/goldfiles/FormulaFields/testDateTimeValueWithInvalidString.xml @@ -2,19 +2,19 @@ - NULL + CONVERT(DATETIME,NULL) 0=0 - NULL + CONVERT(DATETIME,NULL) 0=0 No data Error: com.force.formula.FormulaDateException - Error: At least one of the result expressions in a CASE specification must be an expression other than the NULL constant. + null Error: com.force.formula.FormulaDateException - Error: At least one of the result expressions in a CASE specification must be an expression other than the NULL constant. + null diff --git a/sqlserver-test/src/test/goldfiles/FormulaFields/testDateTimeValueWithTimeZone.xml b/sqlserver-test/src/test/goldfiles/FormulaFields/testDateTimeValueWithTimeZone.xml new file mode 100644 index 00000000..3af7bde5 --- /dev/null +++ b/sqlserver-test/src/test/goldfiles/FormulaFields/testDateTimeValueWithTimeZone.xml @@ -0,0 +1,20 @@ + + + + + CONVERT(DATETIME,NULL) + 0=0 + + + CONVERT(DATETIME,NULL) + 0=0 + + + No data + Thu Aug 12 03:26:09 GMT 2021 + null + Thu Aug 12 03:26:09 GMT 2021 + null + + + diff --git a/sqlserver-test/src/test/goldfiles/FormulaFields/testIfErrorDateTimeValueWithInvalidString.xml b/sqlserver-test/src/test/goldfiles/FormulaFields/testIfErrorDateTimeValueWithInvalidString.xml index 5f62eb5e..e4991b7d 100644 --- a/sqlserver-test/src/test/goldfiles/FormulaFields/testIfErrorDateTimeValueWithInvalidString.xml +++ b/sqlserver-test/src/test/goldfiles/FormulaFields/testIfErrorDateTimeValueWithInvalidString.xml @@ -2,11 +2,11 @@ - CASE WHEN 0=0 THEN CONVERT(DATETIME, '2005-11-15 17:00:00 ') ELSE NULL END + CASE WHEN 0=0 THEN CONVERT(DATETIME, '2005-11-15 17:00:00 ') ELSE CONVERT(DATETIME,NULL) END null - CASE WHEN 0=0 THEN CONVERT(DATETIME, '2005-11-15 17:00:00 ') ELSE NULL END + CASE WHEN 0=0 THEN CONVERT(DATETIME, '2005-11-15 17:00:00 ') ELSE CONVERT(DATETIME,NULL) END null