diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/main/antlr4/org/finos/legend/engine/language/sql/grammar/from/antlr4/SqlBaseLexer.g4 b/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/main/antlr4/org/finos/legend/engine/language/sql/grammar/from/antlr4/SqlBaseLexer.g4 index c025f2a5641..2edc285c700 100644 --- a/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/main/antlr4/org/finos/legend/engine/language/sql/grammar/from/antlr4/SqlBaseLexer.g4 +++ b/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/main/antlr4/org/finos/legend/engine/language/sql/grammar/from/antlr4/SqlBaseLexer.g4 @@ -337,6 +337,10 @@ REGEX_MATCH: '~'; REGEX_NO_MATCH: '!~'; REGEX_MATCH_CI: '~*'; REGEX_NO_MATCH_CI: '!~*'; +OP_LIKE: '~~'; +OP_ILIKE: '~~*'; +OP_NOT_LIKE: '!~~'; +OP_NOT_ILIKE: '!~~*'; PLUS: '+'; MINUS: '-'; diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/main/antlr4/org/finos/legend/engine/language/sql/grammar/from/antlr4/SqlBaseParser.g4 b/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/main/antlr4/org/finos/legend/engine/language/sql/grammar/from/antlr4/SqlBaseParser.g4 index 0dc34ffead3..3c5820322f0 100644 --- a/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/main/antlr4/org/finos/legend/engine/language/sql/grammar/from/antlr4/SqlBaseParser.g4 +++ b/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/main/antlr4/org/finos/legend/engine/language/sql/grammar/from/antlr4/SqlBaseParser.g4 @@ -433,7 +433,7 @@ subscriptSafe ; cmpOp - : EQ | NEQ | LT | LTE | GT | GTE | LLT | REGEX_MATCH | REGEX_NO_MATCH | REGEX_MATCH_CI | REGEX_NO_MATCH_CI + : EQ | NEQ | LT | LTE | GT | GTE | LLT | REGEX_MATCH | REGEX_NO_MATCH | REGEX_MATCH_CI | REGEX_NO_MATCH_CI | OP_LIKE | OP_ILIKE | OP_NOT_LIKE | OP_NOT_ILIKE ; setCmpQuantifier diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/main/java/org/finos/legend/engine/language/sql/grammar/from/SqlVisitor.java b/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/main/java/org/finos/legend/engine/language/sql/grammar/from/SqlVisitor.java index 8624267c1ea..ecf0d59e5ad 100644 --- a/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/main/java/org/finos/legend/engine/language/sql/grammar/from/SqlVisitor.java +++ b/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/main/java/org/finos/legend/engine/language/sql/grammar/from/SqlVisitor.java @@ -1896,6 +1896,22 @@ private static ComparisonOperator getComparisonOperator(Token symbol) return ComparisonOperator.GREATER_THAN; case SqlBaseLexer.GTE: return ComparisonOperator.GREATER_THAN_OR_EQUAL; + case SqlBaseLexer.REGEX_MATCH: + return ComparisonOperator.REGEX_MATCH; + case SqlBaseLexer.REGEX_MATCH_CI: + return ComparisonOperator.REGEX_MATCH_CI; + case SqlBaseLexer.REGEX_NO_MATCH: + return ComparisonOperator.REGEX_NO_MATCH; + case SqlBaseLexer.REGEX_NO_MATCH_CI: + return ComparisonOperator.REGEX_NO_MATCH_CI; + case SqlBaseLexer.OP_LIKE: + return ComparisonOperator.LIKE; + case SqlBaseLexer.OP_ILIKE: + return ComparisonOperator.ILIKE; + case SqlBaseLexer.OP_NOT_LIKE: + return ComparisonOperator.NOT_LIKE; + case SqlBaseLexer.OP_NOT_ILIKE: + return ComparisonOperator.NOT_ILIKE; //TODO handle other operators default: throw new UnsupportedOperationException("Unsupported operator: " + symbol.getText()); diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/main/java/org/finos/legend/engine/language/sql/grammar/to/SQLGrammarComposer.java b/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/main/java/org/finos/legend/engine/language/sql/grammar/to/SQLGrammarComposer.java index 520d4a29d7d..67ec87cb7f4 100644 --- a/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/main/java/org/finos/legend/engine/language/sql/grammar/to/SQLGrammarComposer.java +++ b/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/main/java/org/finos/legend/engine/language/sql/grammar/to/SQLGrammarComposer.java @@ -34,7 +34,15 @@ public class SQLGrammarComposer Tuples.pair(ComparisonOperator.LESS_THAN_OR_EQUAL, "<="), Tuples.pair(ComparisonOperator.GREATER_THAN_OR_EQUAL, ">="), Tuples.pair(ComparisonOperator.IS_DISTINCT_FROM, "IS DISTINCT FROM"), - Tuples.pair(ComparisonOperator.IS_NOT_DISTINCT_FROM, "IS NOT DISTINCT FROM") + Tuples.pair(ComparisonOperator.IS_NOT_DISTINCT_FROM, "IS NOT DISTINCT FROM"), + Tuples.pair(ComparisonOperator.REGEX_MATCH, "~"), + Tuples.pair(ComparisonOperator.REGEX_MATCH_CI, "~*"), + Tuples.pair(ComparisonOperator.REGEX_NO_MATCH, "!~"), + Tuples.pair(ComparisonOperator.REGEX_NO_MATCH_CI, "!~*"), + Tuples.pair(ComparisonOperator.LIKE, "~~"), + Tuples.pair(ComparisonOperator.ILIKE, "~~*"), + Tuples.pair(ComparisonOperator.NOT_LIKE, "!~~"), + Tuples.pair(ComparisonOperator.NOT_ILIKE, "!~~*") ); private final MutableMap binaryComparator = UnifiedMap.newMapWith( Tuples.pair(LogicalBinaryType.AND, "AND"), diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/test/java/org/finos/legend/engine/language/sql/grammar/test/roundtrip/TestSQLRoundTrip.java b/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/test/java/org/finos/legend/engine/language/sql/grammar/test/roundtrip/TestSQLRoundTrip.java index 5b048d7be32..581ce7dbb11 100644 --- a/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/test/java/org/finos/legend/engine/language/sql/grammar/test/roundtrip/TestSQLRoundTrip.java +++ b/legend-engine-xts-sql/legend-engine-xt-sql-grammar/src/test/java/org/finos/legend/engine/language/sql/grammar/test/roundtrip/TestSQLRoundTrip.java @@ -39,6 +39,19 @@ public void testSelectStar() check("SELECT myTable.* FROM myTable"); } + @Test + public void testPatternMatching() + { + check("SELECT * FROM myTable where 'abc' ~ 'def'"); + check("SELECT * FROM myTable where 'abc' ~* 'def'"); + check("SELECT * FROM myTable where 'abc' !~ 'def'"); + check("SELECT * FROM myTable where 'abc' !~* 'def'"); + check("SELECT * FROM myTable where 'abc' ~~ 'def'"); + check("SELECT * FROM myTable where 'abc' ~~* 'def'"); + check("SELECT * FROM myTable where 'abc' !~~ 'def'"); + check("SELECT * FROM myTable where 'abc' !~~* 'def'"); + } + @Test public void testParameters() { diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-postgres-server/src/main/java/org/finos/legend/engine/postgres/TableNameExtractor.java b/legend-engine-xts-sql/legend-engine-xt-sql-postgres-server/src/main/java/org/finos/legend/engine/postgres/TableNameExtractor.java index fdc20ff869a..909fd0268f7 100644 --- a/legend-engine-xts-sql/legend-engine-xt-sql-postgres-server/src/main/java/org/finos/legend/engine/postgres/TableNameExtractor.java +++ b/legend-engine-xts-sql/legend-engine-xt-sql-postgres-server/src/main/java/org/finos/legend/engine/postgres/TableNameExtractor.java @@ -27,18 +27,32 @@ public class TableNameExtractor extends SqlBaseParserBaseVisitor> { + private final Boolean extractTables; + private final Boolean extractTableFunctions; + + public TableNameExtractor() + { + this(true, true); + } + + public TableNameExtractor(Boolean extractTables, Boolean extractTableFunctions) + { + this.extractTables = extractTables; + this.extractTableFunctions = extractTableFunctions; + } + @Override public List visitTableName(SqlBaseParser.TableNameContext ctx) { QualifiedName qualifiedName = getQualifiedName(ctx.qname()); - return Lists.fixedSize.with(qualifiedName); + return this.extractTables ? Lists.fixedSize.with(qualifiedName) : Lists.fixedSize.empty(); } @Override public List visitTableFunction(SqlBaseParser.TableFunctionContext ctx) { QualifiedName qualifiedName = getQualifiedName(ctx.qname()); - return Lists.fixedSize.with(qualifiedName); + return this.extractTableFunctions ? Lists.fixedSize.with(qualifiedName) : Lists.fixedSize.empty(); } @Override diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-postgres-server/src/test/java/org/finos/legend/engine/postgres/TableNameExtractorTest.java b/legend-engine-xts-sql/legend-engine-xt-sql-postgres-server/src/test/java/org/finos/legend/engine/postgres/TableNameExtractorTest.java index d29434c45a6..361a650bc24 100644 --- a/legend-engine-xts-sql/legend-engine-xt-sql-postgres-server/src/test/java/org/finos/legend/engine/postgres/TableNameExtractorTest.java +++ b/legend-engine-xts-sql/legend-engine-xt-sql-postgres-server/src/test/java/org/finos/legend/engine/postgres/TableNameExtractorTest.java @@ -15,56 +15,57 @@ package org.finos.legend.engine.postgres; -import com.google.common.collect.Iterables; +import org.apache.commons.lang3.StringUtils; import org.eclipse.collections.api.factory.Lists; +import org.eclipse.collections.impl.list.mutable.FastList; +import org.eclipse.collections.impl.utility.ListIterate; import org.finos.legend.engine.language.sql.grammar.from.SQLGrammarParser; import org.finos.legend.engine.language.sql.grammar.from.antlr4.SqlBaseParser; import org.finos.legend.engine.protocol.sql.metamodel.QualifiedName; import org.junit.Test; import java.util.List; + import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class TableNameExtractorTest { - private static final TableNameExtractor extractor = new TableNameExtractor(); - - @Test public void testGetSchemaAndTable() { - List qualifiedNames = getQualifiedNames("SELECT * FROM schema1.table1"); - assertEquals(1, qualifiedNames.size()); - QualifiedName qualifiedName = Iterables.getOnlyElement(qualifiedNames); - assertEquals(Lists.mutable.of("schema1", "table1"), qualifiedName.parts); + test("SELECT * FROM schema1.table1", Lists.fixedSize.of("schema1.table1"), new TableNameExtractor()); } @Test public void testSetQuery() { - List qualifiedNames = getQualifiedNames("SET A=B"); - assertEquals(0, qualifiedNames.size()); + test("SET A=B", Lists.fixedSize.empty(), new TableNameExtractor()); } @Test public void testSelectWithoutTable() { - List qualifiedNames = getQualifiedNames("SELECT 1"); - assertEquals(0, qualifiedNames.size()); + test("SELECT 1", FastList.newList(), new TableNameExtractor()); } @Test - public void testFunctionCall() + public void testExtractingDifferentTypes() { - List qualifiedNames = getQualifiedNames("SELECT * FROM service('/my/service')"); - assertEquals(1, qualifiedNames.size()); - QualifiedName qualifiedName = Iterables.getOnlyElement(qualifiedNames); - assertEquals(Lists.mutable.of("service"), qualifiedName.parts); + test("SELECT * FROM service('/my/service') UNION SELECT * from myTable", Lists.fixedSize.of("service", "myTable"), new TableNameExtractor()); + test("SELECT * FROM service('/my/service') UNION SELECT * from myTable", Lists.fixedSize.of("myTable"), new TableNameExtractor(true, false)); + test("SELECT * FROM service('/my/service') UNION SELECT * from myTable", Lists.fixedSize.of("service"), new TableNameExtractor(false, true)); + test("SELECT * FROM service('/my/service') UNION SELECT * from myTable", Lists.fixedSize.empty(), new TableNameExtractor(false, false)); } - private static List getQualifiedNames(String query) + private void test(String sql, List expected, TableNameExtractor extractor) { - SqlBaseParser parser = SQLGrammarParser.getSqlBaseParser(query, "query"); - return parser.singleStatement().accept(extractor); + SqlBaseParser parser = SQLGrammarParser.getSqlBaseParser(sql, "query"); + List qualifiedNames = parser.singleStatement().accept(extractor); + + List result = ListIterate.collect(qualifiedNames, q -> StringUtils.join(q.parts, ".")); + + assertEquals(expected.size(), result.size()); + assertTrue(ListIterate.allSatisfy(expected, result::contains)); } } \ No newline at end of file diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-pure-metamodel/src/main/resources/core_external_query_sql_metamodel/metamodel.pure b/legend-engine-xts-sql/legend-engine-xt-sql-pure-metamodel/src/main/resources/core_external_query_sql_metamodel/metamodel.pure index 0650d4a39e8..e1cf3f84970 100644 --- a/legend-engine-xts-sql/legend-engine-xt-sql-pure-metamodel/src/main/resources/core_external_query_sql_metamodel/metamodel.pure +++ b/legend-engine-xts-sql/legend-engine-xt-sql-pure-metamodel/src/main/resources/core_external_query_sql_metamodel/metamodel.pure @@ -435,7 +435,15 @@ Enum meta::external::query::sql::metamodel::ComparisonOperator GREATER_THAN, GREATER_THAN_OR_EQUAL, IS_DISTINCT_FROM, - IS_NOT_DISTINCT_FROM + IS_NOT_DISTINCT_FROM, + REGEX_MATCH, + REGEX_MATCH_CI, + REGEX_NO_MATCH, + REGEX_NO_MATCH_CI, + LIKE, + ILIKE, + NOT_LIKE, + NOT_ILIKE } Enum meta::external::query::sql::metamodel::CurrentTimeType diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-pure/src/main/resources/core_external_query_sql/binding/fromPure/fromPure.pure b/legend-engine-xts-sql/legend-engine-xt-sql-pure/src/main/resources/core_external_query_sql/binding/fromPure/fromPure.pure index 8e3ea786321..7767d7402ea 100644 --- a/legend-engine-xts-sql/legend-engine-xt-sql-pure/src/main/resources/core_external_query_sql/binding/fromPure/fromPure.pure +++ b/legend-engine-xts-sql/legend-engine-xt-sql-pure/src/main/resources/core_external_query_sql/binding/fromPure/fromPure.pure @@ -1401,14 +1401,22 @@ function meta::external::query::sql::transformation::queryToPure::extractNameFro c:Cast[1] | 'CAST(' + $c.expression->extractNameFromExpression($context) + ' AS ' + $c.type->extractNameFromExpression($context) + ')', c:ComparisonExpression[1] | let operator = [ - pair(ComparisonOperator.EQUAL, '='), - pair(ComparisonOperator.NOT_EQUAL, '!='), - pair(ComparisonOperator.LESS_THAN, '<'), - pair(ComparisonOperator.LESS_THAN_OR_EQUAL, '<='), - pair(ComparisonOperator.GREATER_THAN, '>'), - pair(ComparisonOperator.GREATER_THAN_OR_EQUAL, '>='), - pair(ComparisonOperator.IS_DISTINCT_FROM, 'IS DISTINCT FROM'), - pair(ComparisonOperator.IS_NOT_DISTINCT_FROM, 'IS NOT DISTINCT FROM') + pair(ComparisonOperator.EQUAL, '='), + pair(ComparisonOperator.NOT_EQUAL, '!='), + pair(ComparisonOperator.LESS_THAN, '<'), + pair(ComparisonOperator.LESS_THAN_OR_EQUAL, '<='), + pair(ComparisonOperator.GREATER_THAN, '>'), + pair(ComparisonOperator.GREATER_THAN_OR_EQUAL,'>='), + pair(ComparisonOperator.IS_DISTINCT_FROM, 'IS DISTINCT FROM'), + pair(ComparisonOperator.IS_NOT_DISTINCT_FROM, 'IS NOT DISTINCT FROM'), + pair(ComparisonOperator.REGEX_MATCH, '~'), + pair(ComparisonOperator.REGEX_MATCH_CI, '~*'), + pair(ComparisonOperator.REGEX_NO_MATCH, '!~'), + pair(ComparisonOperator.REGEX_NO_MATCH_CI, '!~*'), + pair(ComparisonOperator.LIKE, '~~'), + pair(ComparisonOperator.ILIKE, '~~*'), + pair(ComparisonOperator.NOT_LIKE, '!~~'), + pair(ComparisonOperator.NOT_ILIKE, '!~~*') ]->getValue($c.operator); $c.left->extractNameFromExpression($context) + ' ' + $operator + ' ' + $c.right->extractNameFromExpression($context);, @@ -2185,7 +2193,16 @@ function meta::external::query::sql::transformation::queryToPure::functionProces processor('ltrim', String, {args, fc, ctx | processTrim(ltrim_String_1__String_1_, $args)}), processor('left', left_String_1__Integer_1__String_1_), processor('md5', String, {args, fc, ctx | processHash($args, meta::pure::functions::hash::HashType.MD5)}), - processor('regexp_like', matches_String_1__String_1__Boolean_1_), + processor('regexp_like', Boolean, {args, fc, ctx | + assert($args->size() == 2 || $args->size() == 3, 'incorrect number of args to regexp_like'); + + let caseInsensitive = $args->size() == 3 && $args->at(2)->reactivate()->match([ + s:String[1] | $s == 'i', + a:Any[*] | false + ]); + + createRegexMatch($args->at(0), $args->at(1), $caseInsensitive, false); + }), processor('repeat', repeatString_String_$0_1$__Integer_1__String_$0_1$_), processor('replace', replace_String_1__String_1__String_1__String_1_), processor('reverse', reverseString_String_1__String_1_), @@ -3027,11 +3044,45 @@ function <> meta::external::query::sql::transformation::queryToP function <> meta::external::query::sql::transformation::queryToPure::processStandardComparison(c:ComparisonExpression[1], left:ValueSpecification[1], right:ValueSpecification[1], type:Type[1], expContext:SqlTransformExpressionContext[1], context:SqlTransformContext[1]):ValueSpecification[1] { [ - pair(ComparisonOperator.IS_DISTINCT_FROM, | createIsDistinctFrom($left, $right)), - pair(ComparisonOperator.IS_NOT_DISTINCT_FROM, | createIsNotDistinctFrom($left, $right)) + pair(ComparisonOperator.IS_DISTINCT_FROM, | createIsDistinctFrom($left, $right)), + pair(ComparisonOperator.IS_NOT_DISTINCT_FROM, | createIsNotDistinctFrom($left, $right)), + pair(ComparisonOperator.REGEX_MATCH, | createRegexMatch($left, $right, false, false)), + pair(ComparisonOperator.REGEX_MATCH_CI, | createRegexMatch($left, $right, true, false)), + pair(ComparisonOperator.REGEX_NO_MATCH, | createRegexMatch($left, $right, false, true)), + pair(ComparisonOperator.REGEX_NO_MATCH_CI, | createRegexMatch($left, $right, true, true)), + pair(ComparisonOperator.LIKE, | createLike($c.left, $c.right, false, false, $expContext, $context)), + pair(ComparisonOperator.ILIKE, | createLike($c.left, $c.right, true, false, $expContext, $context)), + pair(ComparisonOperator.NOT_LIKE, | createLike($c.left, $c.right, false, true, $expContext, $context)), + pair(ComparisonOperator.NOT_ILIKE, | createLike($c.left, $c.right, true, true, $expContext, $context)) ]->getValue($c.operator, | createStandardComparison($c, $left, $right, $type))->eval(); } +function <> meta::external::query::sql::transformation::queryToPure::createLike(left:meta::external::query::sql::metamodel::Expression[1], right:meta::external::query::sql::metamodel::Expression[1], caseInsensitive:Boolean[1], not:Boolean[1], expContext:SqlTransformExpressionContext[1], context:SqlTransformContext[1]):ValueSpecification[1] +{ + let like = ^LikePredicate(value = $left, pattern = $right, ignoreCase = $caseInsensitive); + let expression = if ($not, | ^NotExpression(value = $like), | $like); + + processExpression($expression, $expContext, $context); +} + +function <> meta::external::query::sql::transformation::queryToPure::createRegexMatch(left:ValueSpecification[1], right:ValueSpecification[1], caseInsensitive:Boolean[1], not:Boolean[1]):ValueSpecification[1] +{ + assertFalse($caseInsensitive, 'case insensitive regex currently not supported'); + + let pattern = $right->reactivate()->match([ + s:String[1] | $s, + a:Any[*] | fail('regex must be a string literal'); ''; + ]); + + assert($pattern->startsWith('^') && $pattern->endsWith('$'), 'only exact match regex currently supported'); + + let patternIV = $pattern->substring(1, $pattern->length() - 1)->iv(); + + let match = nullOrSfe(matches_String_1__String_1__Boolean_1_, [$left, $patternIV]); + + if ($not, | nullOrSfe(not_Boolean_1__Boolean_1_, $match), | $match); +} + function <> meta::external::query::sql::transformation::queryToPure::processLiteral(literal: Literal[1], expContext:SqlTransformExpressionContext[1], context: SqlTransformContext[1]):ValueSpecification[1] { debug('processLiteral', $context.debug); diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-pure/src/main/resources/core_external_query_sql/binding/fromPure/tests/testTranspile.pure b/legend-engine-xts-sql/legend-engine-xt-sql-pure/src/main/resources/core_external_query_sql/binding/fromPure/tests/testTranspile.pure index 927734c2879..64e6d3eddfb 100644 --- a/legend-engine-xts-sql/legend-engine-xt-sql-pure/src/main/resources/core_external_query_sql/binding/fromPure/tests/testTranspile.pure +++ b/legend-engine-xts-sql/legend-engine-xt-sql-pure/src/main/resources/core_external_query_sql/binding/fromPure/tests/testTranspile.pure @@ -1781,7 +1781,7 @@ function <> meta::external::query::sql::transformation::queryToPure:: function <> meta::external::query::sql::transformation::queryToPure::tests::testStringFunctions():Boolean[1] { test( - 'SELECT ascii(String) AS "ASCII", chr(Integer) AS "CHR", concat(String, \'abc\') AS "CONCAT", String || \'abc\' AS "CONCAT2", regexp_like(String, \'test\') AS "MATCH", ' + + 'SELECT ascii(String) AS "ASCII", chr(Integer) AS "CHR", concat(String, \'abc\') AS "CONCAT", String || \'abc\' AS "CONCAT2", regexp_like(String, \'^test$\') AS "MATCH", ' + 'char_length(String) AS "CHAR_LENGTH", length(String) AS "LENGTH", ltrim(String) AS "LTRIM", ltrim(String, \' \') AS "LTRIM2", md5(String) AS "MD5", upper(String) AS "UPPER", ' + 'lower(String) AS "LOWER", repeat(String, 2) AS "REPEAT", replace(String, \'A\', \'a\') AS "REPLACE", starts_with(String, \'a\') AS "STARTSWITH", strpos(String, \'abc\') AS "STRPOS",' + 'reverse(String) AS "REVERSE", rtrim(String) AS "RTRIM", rtrim(String, \' \') AS "RTRIM2", sha256(String) AS "SHA256", split_part(String, \',\', 1) AS "SPLITPART", ' + @@ -1845,7 +1845,7 @@ function <> meta::external::query::sql::transformation::queryToPure:: function <> meta::external::query::sql::transformation::queryToPure::tests::testStringFunctionsNullable():Boolean[1] { test( - 'SELECT ascii(NULL) AS "ASCII", chr(NULL) AS "CHR", regexp_like(NULL, \'test\') AS "MATCH", char_length(NULL) AS "CHAR_LENGTH", length(NULL) AS "LENGTH", ltrim(NULL) AS "LTRIM", ' + + 'SELECT ascii(NULL) AS "ASCII", chr(NULL) AS "CHR", regexp_like(NULL, \'^test$\') AS "MATCH", char_length(NULL) AS "CHAR_LENGTH", length(NULL) AS "LENGTH", ltrim(NULL) AS "LTRIM", ' + 'ltrim(NULL, \' \') AS "LTRIM2", md5(NULL) AS "MD5", upper(NULL) AS "UPPER", lower(NULL) AS "LOWER", replace(NULL, \'A\', \'a\') AS "REPLACE", starts_with(NULL, \'a\') AS "STARTSWITH", ' + 'strpos(NULL, \'abc\') AS "STRPOS", reverse(NULL) AS "REVERSE", rtrim(NULL) AS "RTRIM", rtrim(NULL, \' \') AS "RTRIM2", sha256(NULL) AS "SHA256", substring(NULL, 1) AS "SUBSTRING", ' + 'substr(NULL, 1, 2) AS "SUBSTR", btrim(NULL) AS "TRIM", btrim(NULL, \' \') AS "TRIM2", lpad(NULL, 2) AS "LPAD", lpad(String, NULL) AS "LPAD2", lpad(String, 2, NULL) AS "LPAD3",' + @@ -2266,6 +2266,31 @@ function <> meta::external::query::sql::transformation::queryToPure:: }) } +//PATTERN MATCHING +function <> meta::external::query::sql::transformation::queryToPure::tests::testPatternMatching():Boolean[1] +{ + test( + 'SELECT ' + + 'String ~~ \'b\' AS "LIKE", ' + + 'String !~~ \'b\' AS "NOT_LIKE", ' + + 'String ~ \'^b$\' AS "REGEX_MATCHES", ' + + 'String !~ \'^b$\' AS "REGEX_NOT_MATCHES" ' + + 'FROM service."/service/service1"', + + {| + FlatInput.all() + ->project( + [ x | $x.booleanIn, x | $x.integerIn, x | $x.floatIn, x | $x.decimalIn, x | $x.strictDateIn, x | $x.dateTimeIn, x | $x.stringIn ], + [ 'Boolean', 'Integer', 'Float', 'Decimal', 'StrictDate', 'DateTime', 'String' ]) + ->project([ + col(row:TDSRow[1] | equal($row.getString('String'), 'b'), 'LIKE'), + col(row:TDSRow[1] | not(equal($row.getString('String'), 'b')), 'NOT_LIKE'), + col(row:TDSRow[1] | matches($row.getString('String'), 'b'), 'REGEX_MATCHES'), + col(row:TDSRow[1] | not(matches($row.getString('String'), 'b')), 'REGEX_NOT_MATCHES') + ]) + }) +} + function <> meta::external::query::sql::transformation::queryToPure::tests::testFromScopingSimple():Boolean[1] { test( @@ -2419,12 +2444,12 @@ function <> meta::external::query::sql::transformation::queryToPure:: function <> meta::external::query::sql::transformation::queryToPure::tests::testPlaceholderParameters():Boolean[1] { test( - 'SELECT * FROM service."/service/service1" where Integer = ? and String = $2', + 'SELECT * FROM service."/service/service1" where Integer = ? and String = $2 and StrictDate = $3', - {_1:Integer[1], _2:String[1] | FlatInput.all()->project( + {_1:Integer[1], _2:String[1], _3:StrictDate[1] | FlatInput.all()->project( [ x | $x.booleanIn, x | $x.integerIn, x | $x.floatIn, x | $x.decimalIn, x | $x.strictDateIn, x | $x.dateTimeIn, x | $x.stringIn ], [ 'Boolean', 'Integer', 'Float', 'Decimal', 'StrictDate', 'DateTime', 'String' ]) - ->filter(row:TDSRow[1] | $row.getInteger('Integer') == $_1 && $row.getString('String') == $_2) + ->filter(row:TDSRow[1] | $row.getInteger('Integer') == $_1 && $row.getString('String') == $_2 && $row.getStrictDate('StrictDate') == $_3) } ) } @@ -2550,6 +2575,14 @@ function <> meta::external::query::sql::transformation::queryToPure:: doNameTest(^ComparisonExpression(left = ^IntegerLiteral(value = 1), operator = ComparisonOperator.LESS_THAN_OR_EQUAL, right = ^IntegerLiteral(value = 2)), '1 <= 2'); doNameTest(^ComparisonExpression(left = ^IntegerLiteral(value = 1), operator = ComparisonOperator.GREATER_THAN, right = ^IntegerLiteral(value = 2)), '1 > 2'); doNameTest(^ComparisonExpression(left = ^IntegerLiteral(value = 1), operator = ComparisonOperator.GREATER_THAN_OR_EQUAL, right = ^IntegerLiteral(value = 2)), '1 >= 2'); + doNameTest(^ComparisonExpression(left = ^StringLiteral(value = 'abc'), operator = ComparisonOperator.REGEX_MATCH, right = ^StringLiteral(value = 'def')), 'abc ~ def'); + doNameTest(^ComparisonExpression(left = ^StringLiteral(value = 'abc'), operator = ComparisonOperator.REGEX_MATCH_CI, right = ^StringLiteral(value = 'def')), 'abc ~* def'); + doNameTest(^ComparisonExpression(left = ^StringLiteral(value = 'abc'), operator = ComparisonOperator.REGEX_NO_MATCH, right = ^StringLiteral(value = 'def')), 'abc !~ def'); + doNameTest(^ComparisonExpression(left = ^StringLiteral(value = 'abc'), operator = ComparisonOperator.REGEX_NO_MATCH_CI, right = ^StringLiteral(value = 'def')), 'abc !~* def'); + doNameTest(^ComparisonExpression(left = ^StringLiteral(value = 'abc'), operator = ComparisonOperator.LIKE, right = ^StringLiteral(value = 'def')), 'abc ~~ def'); + doNameTest(^ComparisonExpression(left = ^StringLiteral(value = 'abc'), operator = ComparisonOperator.ILIKE, right = ^StringLiteral(value = 'def')), 'abc ~~* def'); + doNameTest(^ComparisonExpression(left = ^StringLiteral(value = 'abc'), operator = ComparisonOperator.NOT_LIKE, right = ^StringLiteral(value = 'def')), 'abc !~~ def'); + doNameTest(^ComparisonExpression(left = ^StringLiteral(value = 'abc'), operator = ComparisonOperator.NOT_ILIKE, right = ^StringLiteral(value = 'def')), 'abc !~~* def'); doNameTest(^Extract(field = ExtractField.DOW, expression = ^QualifiedNameReference(name = ^QualifiedName(parts = 'date'))), 'EXTRACT(\'dow\' FROM date)');