Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Microsoft.Azure.Cosmos.Linq
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq.Expressions;
using Microsoft.Azure.Cosmos;
Expand Down Expand Up @@ -49,6 +50,12 @@ public static SqlScalarExpression VisitBuiltinFunctionCall(MethodCallExpression

if (methodCallExpression.Method.DeclaringType.GeUnderlyingSystemType() == typeof(CosmosLinqExtensions))
{
// CosmosLinq Extensions are either RegexMatch or Type check functions (IsString, IsBool, etc.)
if (methodCallExpression.Method.Name == nameof(CosmosLinqExtensions.RegexMatch))
{
return StringBuiltinFunctions.Visit(methodCallExpression, context);
}

return TypeCheckFunctions.Visit(methodCallExpression, context);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,43 @@ protected override SqlScalarExpression VisitExplicit(MethodCallExpression method
}
}

private class RegexMatchVisitor : SqlBuiltinFunctionVisitor
{
public RegexMatchVisitor()
: base(SqlFunctionCallScalarExpression.Names.RegexMatch,
isStatic: true,
new List<Type[]>()
{
new Type[]{ typeof(object), typeof(string)}, // search string, regex pattern
new Type[]{ typeof(object), typeof(string), typeof(string)} // search string, regex pattern, search modifier
})
{
}

protected override SqlScalarExpression VisitImplicit(MethodCallExpression methodCallExpression, TranslationContext context)
{
int argumentCount = methodCallExpression.Arguments.Count;
if (argumentCount == 0 || argumentCount > 3 || (methodCallExpression.Arguments[1].NodeType != ExpressionType.Constant))
{
return null;
}

List<SqlScalarExpression> arguments = new List<SqlScalarExpression>
{
// Argument 0 and the Method object is the same, since Regex is an extension method
ExpressionToSql.VisitNonSubqueryScalarExpression(methodCallExpression.Arguments[0], context),
ExpressionToSql.VisitNonSubqueryScalarExpression(methodCallExpression.Arguments[1], context)
};

if (argumentCount > 2 && (methodCallExpression.Arguments[2].NodeType == ExpressionType.Constant))
{
arguments.Add(ExpressionToSql.VisitNonSubqueryScalarExpression(methodCallExpression.Arguments[2], context));
}

return SqlFunctionCallScalarExpression.CreateBuiltin(SqlFunctionCallScalarExpression.Names.RegexMatch, arguments.ToArray());
}
}

private class StringVisitToString : SqlBuiltinFunctionVisitor
{
public StringVisitToString()
Expand Down Expand Up @@ -432,6 +469,10 @@ static StringBuiltinFunctions()
"TrimStart",
new StringVisitTrimStart()
},
{
nameof(CosmosLinqExtensions.RegexMatch),
new RegexMatchVisitor()
},
{
"Replace",
new SqlBuiltinFunctionVisitor(SqlFunctionCallScalarExpression.Names.Replace,
Expand Down
44 changes: 44 additions & 0 deletions Microsoft.Azure.Cosmos/src/Linq/CosmosLinqExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace Microsoft.Azure.Cosmos.Linq
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos.Diagnostics;
Expand Down Expand Up @@ -175,6 +176,49 @@ public static bool IsString(this object obj)
throw new NotImplementedException(ClientResources.TypeCheckExtensionFunctionsNotImplemented);
}

/// <summary>
/// Returns a Boolean value indicating if the specified expression matches the supplied regex pattern.
/// For more information, see https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/query/regexmatch.
/// This method is to be used in LINQ expressions only and will be evaluated on server.
/// There's no implementation provided in the client library.
/// </summary>
/// <param name="obj"></param>
/// <param name="regularExpression">A string expression with a regular expression defined to use when searching.</param>
/// <returns>Returns true if the string matches the regex expressions; otherwise, false.</returns>
/// <example>
/// <code>
/// <![CDATA[
/// var matched = documents.Where(document => document.Name.RegexMatch(<regex>));
/// ]]>
/// </code>
/// </example>
public static bool RegexMatch(this object obj, string regularExpression)
{
throw new NotImplementedException(ClientResources.TypeCheckExtensionFunctionsNotImplemented);
}

/// <summary>
/// Returns a Boolean value indicating if the specified expression matches the supplied regex pattern.
/// For more information, see https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/query/regexmatch.
/// This method is to be used in LINQ expressions only and will be evaluated on server.
/// There's no implementation provided in the client library.
/// </summary>
/// <param name="obj"></param>
/// <param name="regularExpression">A string expression with a regular expression defined to use when searching.</param>
/// <param name="searchModifier">An optional string expression with the selected modifiers to use with the regular expression.</param>
/// <returns>Returns true if the string matches the regex expressions; otherwise, false.</returns>
/// <example>
/// <code>
/// <![CDATA[
/// var matched = documents.Where(document => document.Name.RegexMatch(<regex>, <search_modifier>));
/// ]]>
/// </code>
/// </example>
public static bool RegexMatch(this object obj, string regularExpression, string searchModifier)
{
throw new NotImplementedException(ClientResources.TypeCheckExtensionFunctionsNotImplemented);
}

/// <summary>
/// This method generate query definition from LINQ query.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ sealed class SqlFunctionCallScalarExpression : SqlScalarExpression
{ Names.Power, Identifiers.Power },
{ Names.Radians, Identifiers.Radians },
{ Names.Rand, Identifiers.Rand },
{ Names.RegexMatch, Identifiers.RegexMatch },
{ Names.Replace, Identifiers.Replace },
{ Names.Replicate, Identifiers.Replicate },
{ Names.Reverse, Identifiers.Reverse },
Expand Down Expand Up @@ -332,6 +333,7 @@ public static class Names
public const string Power = "POWER";
public const string Radians = "RADIANS";
public const string Rand = "RAND";
public const string RegexMatch = "RegexMatch";
public const string Replace = "REPLACE";
public const string Replicate = "REPLICATE";
public const string Reverse = "REVERSE";
Expand Down Expand Up @@ -474,6 +476,7 @@ public static class Identifiers
public static readonly SqlIdentifier Power = SqlIdentifier.Create(Names.Power);
public static readonly SqlIdentifier Radians = SqlIdentifier.Create(Names.Radians);
public static readonly SqlIdentifier Rand = SqlIdentifier.Create(Names.Rand);
public static readonly SqlIdentifier RegexMatch = SqlIdentifier.Create(Names.RegexMatch);
public static readonly SqlIdentifier Replace = SqlIdentifier.Create(Names.Replace);
public static readonly SqlIdentifier Replicate = SqlIdentifier.Create(Names.Replicate);
public static readonly SqlIdentifier Reverse = SqlIdentifier.Create(Names.Reverse);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<Results>
<Result>
<Input>
<Description><![CDATA[RegexMatch with 1 argument]]></Description>
<Expression><![CDATA[query.Where(doc => doc.StringField.RegexMatch("abcd"))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE root
FROM root
WHERE RegexMatch(root["StringField"], "abcd")]]></SqlQuery>
</Output>
</Result>
<Result>
<Input>
<Description><![CDATA[RegexMatch with 2 argument]]></Description>
<Expression><![CDATA[query.Where(doc => doc.StringField.RegexMatch("abcd", "i"))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE root
FROM root
WHERE RegexMatch(root["StringField"], "abcd", "i")]]></SqlQuery>
</Output>
</Result>
<Result>
<Input>
<Description><![CDATA[RegexMatch with 1st argument member expression]]></Description>
<Expression><![CDATA[query.Where(doc => doc.StringField.RegexMatch(doc.StringField2))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE root
FROM root
WHERE RegexMatch(root["StringField"], root["StringField2"])]]></SqlQuery>
</Output>
</Result>
<Result>
<Input>
<Description><![CDATA[RegexMatch with ToString]]></Description>
<Expression><![CDATA[query.Where(doc => doc.StringField.RegexMatch(doc.IntField.ToString()))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE root
FROM root
WHERE RegexMatch(root["StringField"], ToString(root["IntField"]))]]></SqlQuery>
</Output>
</Result>
<Result>
<Input>
<Description><![CDATA[RegexMatch with StringUpper]]></Description>
<Expression><![CDATA[query.Where(doc => doc.StringField.RegexMatch(doc.StringField2.ToUpper()))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE root
FROM root
WHERE RegexMatch(root["StringField"], UPPER(root["StringField2"]))]]></SqlQuery>
</Output>
</Result>
<Result>
<Input>
<Description><![CDATA[RegexMatch with StringLower]]></Description>
<Expression><![CDATA[query.Where(doc => doc.StringField.RegexMatch(doc.StringField2.ToLower()))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE root
FROM root
WHERE RegexMatch(root["StringField"], LOWER(root["StringField2"]))]]></SqlQuery>
</Output>
</Result>
<Result>
<Input>
<Description><![CDATA[RegexMatch with StringConcat]]></Description>
<Expression><![CDATA[query.Where(doc => doc.StringField.RegexMatch(Concat(doc.StringField, "str")))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE root
FROM root
WHERE RegexMatch(root["StringField"], CONCAT(root["StringField"], "str"))]]></SqlQuery>
</Output>
</Result>
<Result>
<Input>
<Description><![CDATA[RegexMatch with string composition]]></Description>
<Expression><![CDATA[query.Where(doc => doc.IntField.ToString().RegexMatch(doc.StringField))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE root
FROM root
WHERE RegexMatch(ToString(root["IntField"]), root["StringField"])]]></SqlQuery>
</Output>
</Result>
<Result>
<Input>
<Description><![CDATA[RegexMatch with string composition 2]]></Description>
<Expression><![CDATA[query.Where(doc => doc.IntField.ToString().RegexMatch(doc.StringField, doc.StringField2.ToString()))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE root
FROM root
WHERE RegexMatch(ToString(root["IntField"]), root["StringField"], root["StringField2"])]]></SqlQuery>
</Output>
</Result>
<Result>
<Input>
<Description><![CDATA[RegexMatch with conditional]]></Description>
<Expression><![CDATA[query.Where(doc => (doc.StringField.RegexMatch("abc") AndAlso doc.StringField2.RegexMatch("def")))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE root
FROM root
WHERE (RegexMatch(root["StringField"], "abc") AND RegexMatch(root["StringField2"], "def"))]]></SqlQuery>
</Output>
</Result>
<Result>
<Input>
<Description><![CDATA[RegexMatch with conditional 2]]></Description>
<Expression><![CDATA[query.Where(doc => (doc.StringField.RegexMatch("abc") OrElse doc.StringField2.RegexMatch("def")))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE root
FROM root
WHERE (RegexMatch(root["StringField"], "abc") OR RegexMatch(root["StringField2"], "def"))]]></SqlQuery>
</Output>
</Result>
<Result>
<Input>
<Description><![CDATA[RegexMatch with conditional 3]]></Description>
<Expression><![CDATA[query.Where(doc => doc.StringField.RegexMatch("abc")).Where(doc => doc.StringField2.RegexMatch("abc"))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE root
FROM root
WHERE (RegexMatch(root["StringField"], "abc") AND RegexMatch(root["StringField2"], "abc"))]]></SqlQuery>
</Output>
</Result>
<Result>
<Input>
<Description><![CDATA[RegexMatch with conditional 4]]></Description>
<Expression><![CDATA[query.Where(doc => doc.StringField.RegexMatch("abc")).Where(doc => Not(doc.StringField2.RegexMatch("abc")))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE root
FROM root
WHERE (RegexMatch(root["StringField"], "abc") AND (NOT RegexMatch(root["StringField2"], "abc")))]]></SqlQuery>
</Output>
</Result>
<Result>
<Input>
<Description><![CDATA[RegexMatch with 2nd argument invalid string options]]></Description>
<Expression><![CDATA[query.Where(doc => doc.StringField.RegexMatch("abcd", "this should error out on the back end"))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE root
FROM root
WHERE RegexMatch(root["StringField"], "abcd", "this should error out on the back end")]]></SqlQuery>
</Output>
</Result>
</Results>
Loading