diff --git a/legend-engine-core/legend-engine-core-base/legend-engine-core-executionPlan-execution/legend-engine-executionPlan-execution/src/main/java/org/finos/legend/engine/plan/execution/result/freemarker/PlanDateParameter.java b/legend-engine-core/legend-engine-core-base/legend-engine-core-executionPlan-execution/legend-engine-executionPlan-execution/src/main/java/org/finos/legend/engine/plan/execution/result/freemarker/PlanDateParameter.java index 55a5e904b64..54627fbcc65 100644 --- a/legend-engine-core/legend-engine-core-base/legend-engine-core-executionPlan-execution/legend-engine-executionPlan-execution/src/main/java/org/finos/legend/engine/plan/execution/result/freemarker/PlanDateParameter.java +++ b/legend-engine-core/legend-engine-core-base/legend-engine-core-executionPlan-execution/legend-engine-executionPlan-execution/src/main/java/org/finos/legend/engine/plan/execution/result/freemarker/PlanDateParameter.java @@ -37,9 +37,10 @@ public PlanDateParameter(LocalDateTime dateTime, DateTimeFormatter dateTimeForma public PlanDateParameter(LocalDateTime date, DateTimeFormatter dateTimeFormatter, String targetTz) { - LocalDateTime dateTimeAdjustedForTargetTz = getTargetZonedDateTime(date, targetTz); + String validTargetTz = targetTz.replaceAll("^['\"]+|['\"]+$", ""); // Sanitize the targetTz input by trimming any leading or trailing quotes before processing it + LocalDateTime dateTimeAdjustedForTargetTz = getTargetZonedDateTime(date, validTargetTz); formattedDate = dateTimeAdjustedForTargetTz.format(dateTimeFormatter); - processedDate = Date.from(dateTimeAdjustedForTargetTz.atZone(ZoneId.of(targetTz)).toInstant()); + processedDate = Date.from(dateTimeAdjustedForTargetTz.atZone(ZoneId.of(validTargetTz)).toInstant()); } private LocalDateTime getTargetZonedDateTime(LocalDateTime dateTime, String targetTz) diff --git a/legend-engine-core/legend-engine-core-base/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/core/CoreFragmentGrammar.g4 b/legend-engine-core/legend-engine-core-base/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/core/CoreFragmentGrammar.g4 index 11cedecbc21..65774c06b5b 100644 --- a/legend-engine-core/legend-engine-core-base/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/core/CoreFragmentGrammar.g4 +++ b/legend-engine-core/legend-engine-core-base/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/core/CoreFragmentGrammar.g4 @@ -49,7 +49,7 @@ fragment EscAny: Esc . // -------------------------------------- SPECIFICS -------------------------------------- -fragment TimeZone: (('+' | '-')(Digit)(Digit)(Digit)(Digit)) +fragment TimeZone: String | (('+' | '-')(Digit)(Digit)(Digit)(Digit)) ; fragment ValidString: (Letter | Digit | '_' ) (Letter | Digit | '_' | '$')* ; diff --git a/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-dbExtension/legend-engine-xt-relationalStore-snowflake/legend-engine-xt-relationalStore-snowflake-grammar/src/test/java/org/finos/legend/engine/language/pure/grammar/test/TestSnowflakeConnectionGrammarParser.java b/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-dbExtension/legend-engine-xt-relationalStore-snowflake/legend-engine-xt-relationalStore-snowflake-grammar/src/test/java/org/finos/legend/engine/language/pure/grammar/test/TestSnowflakeConnectionGrammarParser.java index 89be92c926a..8afaed862af 100644 --- a/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-dbExtension/legend-engine-xt-relationalStore-snowflake/legend-engine-xt-relationalStore-snowflake-grammar/src/test/java/org/finos/legend/engine/language/pure/grammar/test/TestSnowflakeConnectionGrammarParser.java +++ b/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-dbExtension/legend-engine-xt-relationalStore-snowflake/legend-engine-xt-relationalStore-snowflake-grammar/src/test/java/org/finos/legend/engine/language/pure/grammar/test/TestSnowflakeConnectionGrammarParser.java @@ -130,6 +130,7 @@ public void testSnowflakePublicAuth() "{\n" + " store: store::Store;\n" + " type: Snowflake;\n" + + " timezone: 'US/Arizona';\n" + " specification: Snowflake\n" + " {\n" + " name: 'test';\n" + @@ -154,6 +155,7 @@ public void testSnowflakePublicAuth() "{\n" + " store: store::Store;\n" + " type: Snowflake;\n" + + " timezone: +3000;\n" + " specification: Snowflake\n" + " {\n" + " name: 'test';\n" + diff --git a/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-execution/legend-engine-xt-relationalStore-executionPlan/src/test/java/org/finos/legend/engine/plan/execution/stores/relational/freemarker/TestFreemarker.java b/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-execution/legend-engine-xt-relationalStore-executionPlan/src/test/java/org/finos/legend/engine/plan/execution/stores/relational/freemarker/TestFreemarker.java index 2bc5f08939d..4448444dcb2 100644 --- a/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-execution/legend-engine-xt-relationalStore-executionPlan/src/test/java/org/finos/legend/engine/plan/execution/stores/relational/freemarker/TestFreemarker.java +++ b/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-execution/legend-engine-xt-relationalStore-executionPlan/src/test/java/org/finos/legend/engine/plan/execution/stores/relational/freemarker/TestFreemarker.java @@ -27,6 +27,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Collections; public class TestFreemarker { @@ -239,12 +240,37 @@ public void testEnumPlaceHolderIfOp() throws Exception Assert.assertEquals("select case when 'EMEA' = 'EMEA' then \"root\".COUNTY else \"root\".COUNTY end as \"county\", \"root\".FIPS as \"fips\" from testTable as \"root\"", result4); } + @Test + public void testTimeZoneOffsetAndCode() + { + Map vars = new HashMap<>(); + vars.put("startTime", new ConstantResult("2024-08-22T14:38:14.684")); + ExecutionState state = new ExecutionState(vars, Collections.singletonList(functionTemplates()), Lists.mutable.empty()); + + String testQueryWithTimeZoneOffset = "select \"root\".FIRSTNAME as \"FIRSTNAME\" from PERSON_SCHEMA.PERSON_TABLE as \"root\" where '${GMTtoTZ( \"[-0700]\" startTime)}'::timestamp is null"; + String result1 = FreeMarkerExecutor.process(testQueryWithTimeZoneOffset, state); + Assert.assertEquals("select \"root\".FIRSTNAME as \"FIRSTNAME\" from PERSON_SCHEMA.PERSON_TABLE as \"root\" where '2024-08-22T07:38:14.684'::timestamp is null", result1); + + String testQueryWithTimeZoneCode = "select \"root\".FIRSTNAME as \"FIRSTNAME\" from PERSON_SCHEMA.PERSON_TABLE as \"root\" where '${GMTtoTZ( \"[US/Arizona]\" startTime)}'::timestamp is null"; + String result2 = FreeMarkerExecutor.process(testQueryWithTimeZoneCode, state); + Assert.assertEquals("select \"root\".FIRSTNAME as \"FIRSTNAME\" from PERSON_SCHEMA.PERSON_TABLE as \"root\" where '2024-08-22T07:38:14.684'::timestamp is null", result2); + } + public static String functionTemplates() { - return collectionTemplate() + "\n" + collectionSizeTemplate() + "\n" + renderCollectionWithDefaultTemplate() + "\n" + enumMap_test_Map_CaseTypeMapping() + "\n" + enumMap_test_Map_CountryMapping() + "\n" + equalEnumOperationSelector(); + return collectionTemplate() + "\n" + GMTtoTZTemplate() + "\n" + collectionSizeTemplate() + "\n" + renderCollectionWithDefaultTemplate() + "\n" + enumMap_test_Map_CaseTypeMapping() + "\n" + enumMap_test_Map_CountryMapping() + "\n" + equalEnumOperationSelector(); } -//corresponds to function templates coming with plan + //corresponds to function templates coming with plan + public static String GMTtoTZTemplate() + { + return "<#function GMTtoTZ tz paramDate>" + + "<#if paramDate?is_enumerable && !paramDate?has_content>" + + "<#return paramDate>" + + "<#else>" + + "<#return (tz+\" \"+paramDate)?date.@alloyDate>" + + ""; + } public static String collectionTemplate() { diff --git a/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-generation/legend-engine-xt-relationalStore-grammar/src/test/java/org/finos/legend/engine/language/pure/grammar/test/TestRelationalConnectionGrammarParser.java b/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-generation/legend-engine-xt-relationalStore-grammar/src/test/java/org/finos/legend/engine/language/pure/grammar/test/TestRelationalConnectionGrammarParser.java index 0decfd55cbf..43f18b7ef00 100644 --- a/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-generation/legend-engine-xt-relationalStore-grammar/src/test/java/org/finos/legend/engine/language/pure/grammar/test/TestRelationalConnectionGrammarParser.java +++ b/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-generation/legend-engine-xt-relationalStore-grammar/src/test/java/org/finos/legend/engine/language/pure/grammar/test/TestRelationalConnectionGrammarParser.java @@ -53,6 +53,29 @@ public String getParserGrammarIdentifierInclusionTestCode(List keywords) "}\n\n"; } + private String getTemplateConnectionWithTz(String offsetOrCode) + { + return "###Connection\n" + + "RelationalDatabaseConnection meta::mySimpleConnection\n" + + "{\n" + + " store: model::firm::Person;\n" + + " timezone: " + offsetOrCode + ";\n" + + " type: H2;\n" + + " specification: LocalH2 { testDataSetupCSV: 'testCSV'; };\n" + + " auth: DefaultH2;\n" + + "}\n\n"; + } + + @Test + public void testTimezoneConfiguration() + { + // With Offset + test(getTemplateConnectionWithTz("+0700"), null); + // With zone id + test(getTemplateConnectionWithTz("'EST'"), null); + test(getTemplateConnectionWithTz("'US/Arizona'"), null); + } + @Test public void testRelationalDatabaseConnection() { diff --git a/legend-engine-xts-service/legend-engine-language-pure-dsl-service-execution/src/test/resources/org/finos/legend/engine/pure/dsl/service/execution/test/simpleRelationalService.pure b/legend-engine-xts-service/legend-engine-language-pure-dsl-service-execution/src/test/resources/org/finos/legend/engine/pure/dsl/service/execution/test/simpleRelationalService.pure index 2fb43cc103f..cdf735d6206 100644 --- a/legend-engine-xts-service/legend-engine-language-pure-dsl-service-execution/src/test/resources/org/finos/legend/engine/pure/dsl/service/execution/test/simpleRelationalService.pure +++ b/legend-engine-xts-service/legend-engine-language-pure-dsl-service-execution/src/test/resources/org/finos/legend/engine/pure/dsl/service/execution/test/simpleRelationalService.pure @@ -296,7 +296,7 @@ Runtime test::Runtime { store: test::DB2; type: H2; - timezone:+1000; + timezone: 'Pacific/Guam'; specification: LocalH2 { testDataSetupCSV: 'default\nemploymentTable\nEMP_ID,FIRSTNAME,LASTNAME,EMPLOYMENT_DATETIME\n1,Peter,Smith,2012-05-20T13:10:52.501\n2,John,Johnson,2005-03-15T18:47:52\n3,Bob,Stevens,\n---';