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
29 changes: 28 additions & 1 deletion src/EntityFramework6.Npgsql/NpgsqlTextFunctions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Data.Entity;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;

namespace Npgsql
{
Expand Down Expand Up @@ -368,5 +369,31 @@ public static string TsRewrite(string query, string target, string substitute)
{
throw new NotSupportedException();
}
}

/// <summary>
/// Matches regular expression. Generates the "~" operator.
/// http://www.postgresql.org/docs/current/static/functions-matching.html#FUNCTIONS-POSIX-REGEXP
/// This method follows the semantics of <see cref="Regex.IsMatch(string, string)"/>
/// and it is translated to the equivalent PostgreSQL expression when executed.
/// </summary>
[DbFunction("Npgsql", "match_regex")]
public static bool MatchRegex(string input, string pattern)
{
throw new NotSupportedException();
}

/// <summary>
/// Matches regular expression. Generates the "~" operator.
/// http://www.postgresql.org/docs/current/static/functions-matching.html#FUNCTIONS-POSIX-REGEXP
/// This method follows the semantics of <see cref="Regex.IsMatch(string, string, RegexOptions)"/>
/// and it is translated to the equivalent PostgreSQL expression when executed.
/// Options <see cref="RegexOptions.RightToLeft"/> and <see cref="RegexOptions.ECMAScript"/>
/// are not supported.
/// </summary>
[DbFunction("Npgsql", "match_regex")]
public static bool MatchRegex(string input, string pattern, RegexOptions options)
{
throw new NotSupportedException();
}
}
}
76 changes: 76 additions & 0 deletions src/EntityFramework6.Npgsql/SqlGenerators/SqlBaseGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
#endif
using System.Linq;
using JetBrains.Annotations;
using System.Text.RegularExpressions;
using System.Text;

namespace Npgsql.SqlGenerators
{
Expand Down Expand Up @@ -1144,6 +1146,10 @@ VisitedExpression VisitFunction(EdmFunction function, IList<DbExpression> args,

return new CastExpression(args[0].Accept(this), "tsquery");
}
else if (functionName == "match_regex")
{
return VisitMatchRegex(function, args, resultType);
}
}

var customFuncCall = new FunctionExpression(
Expand All @@ -1160,6 +1166,76 @@ VisitedExpression VisitFunction(EdmFunction function, IList<DbExpression> args,
#endif
}

#if ENTITIES6
VisitedExpression VisitMatchRegex(EdmFunction function, IList<DbExpression> args, TypeUsage resultType)
{
if (args.Count != 2 && args.Count != 3)
throw new ArgumentException("Invalid number of arguments. Expected 2 or 3.", nameof(args));

var options = RegexOptions.None;

if (args.Count == 3)
{
var optionsExpression = args[2] as DbConstantExpression;
if (optionsExpression == null)
throw new NotSupportedException("Options must be constant expression.");

options = (RegexOptions)optionsExpression.Value;
}

if (options.HasFlag(RegexOptions.RightToLeft) || options.HasFlag(RegexOptions.ECMAScript))
{
throw new NotSupportedException("Options RightToLeft and ECMAScript are not supported.");
}

if (options == RegexOptions.Singleline)
{
return OperatorExpression.Build(
Operator.RegexMatch,
_useNewPrecedences,
args[0].Accept(this),
args[1].Accept(this));
}

var flags = new StringBuilder("(?");

if (options.HasFlag(RegexOptions.IgnoreCase))
{
flags.Append('i');
}

if (options.HasFlag(RegexOptions.Multiline))
{
flags.Append('n');
}
else if (!options.HasFlag(RegexOptions.Singleline))
{
// In .NET's default mode, . doesn't match newlines but PostgreSQL it does.
flags.Append('p');
}

if (options.HasFlag(RegexOptions.IgnorePatternWhitespace))
{
flags.Append('x');
}

flags.Append(')');

var primitiveType = PrimitiveType.GetEdmPrimitiveType(PrimitiveTypeKind.String);
var newRegexExpression = OperatorExpression.Build(
Operator.Concat,
_useNewPrecedences,
new ConstantExpression(flags.ToString(), TypeUsage.CreateStringTypeUsage(primitiveType, true, false)),
args[1].Accept(this));

return OperatorExpression.Build(
Operator.RegexMatch,
_useNewPrecedences,
args[0].Accept(this),
newRegexExpression);
}
#endif

VisitedExpression Substring(VisitedExpression source, VisitedExpression start, VisitedExpression count)
{
var substring = new FunctionExpression("substr");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,7 @@ internal enum UnaryTypes {
public static readonly Operator QueryNegate = new Operator("!!", 10, 8, UnaryTypes.Prefix, true);
public static readonly Operator QueryContains = new Operator("@>", 10, 8);
public static readonly Operator QueryIsContained = new Operator("<@", 10, 8);
public static readonly Operator RegexMatch = new Operator("~", 10, 8);

Copy link
Copy Markdown
Contributor

@rwasef1830 rwasef1830 Jul 7, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename RegexMatchCase to just RegexMatch and remove the insensitive one (it is unused).

public static readonly Dictionary<Operator, Operator> NegateDict;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
<ItemGroup>
<Compile Include="EntityFrameworkBasicTests.cs" />
<Compile Include="EntityFrameworkMigrationTests.cs" />
<Compile Include="PatternMatchingTests.cs" />
<Compile Include="Support\EntityFrameworkTestBase.cs" />
<Compile Include="FullTextSearchTests.cs" />
<Compile Include="NLogLoggingProvider.cs" />
Expand Down
164 changes: 164 additions & 0 deletions test/EntityFramework6.Npgsql.Tests/PatternMatchingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#region License
// The PostgreSQL License
//
// Copyright (C) 2016 The Npgsql Development Team
//
// Permission to use, copy, modify, and distribute this software and its
// documentation for any purpose, without fee, and without a written
// agreement is hereby granted, provided that the above copyright notice
// and this paragraph and the following two paragraphs appear in all copies.
//
// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
//
// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS
// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#endregion

using Npgsql;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Entity;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Core.Objects;
using System.Data.Entity.Infrastructure;
using NpgsqlTypes;
using System.Text.RegularExpressions;

namespace EntityFramework6.Npgsql.Tests
{
class PatternMatchingTests : EntityFrameworkTestBase
{
[Test]
[TestCase("blog", "blog", "BLOG", TestName = "Case-sensitive")]
[TestCase("^blog$", "blog", "some \nblog\n name", TestName = "^ and $ match beginning and end")]
[TestCase("some .* name", "some blog name", "some \n name", TestName = ". matches all except \\n")]
[TestCase("some blog name", "some blog name", "someblogname", TestName = "Whitespace not ignored in pattern")]
public void MatchRegex(string pattern, string matchingInput, string mismatchingInput)
Copy link
Copy Markdown
Contributor

@rwasef1830 rwasef1830 Jul 8, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove MatchRegex from TestName property. In the unit test runner the test case names will appear under the test's main method name in a tree structure so no need to mention it again in the value of the TestName property. (eg: Case-sensitive).

{
// Arrange
using (var context = new BloggingContext(ConnectionString))
{
context.Database.Log = Console.Out.WriteLine;

context.Blogs.Add(new Blog() { Name = matchingInput });
context.Blogs.Add(new Blog() { Name = mismatchingInput });
context.SaveChanges();
}

// Act
// Ensure correctness of a test case
var netMatchResult = Regex.IsMatch(matchingInput, pattern);
var netMismatchResult = Regex.IsMatch(mismatchingInput, pattern);

List<string> pgMatchResults;
List<string> pgMismatchResults;
List<string> pgMatchWithOptionsResults;
List<string> pgMismatchWithOptionsResults;
using (var context = new BloggingContext(ConnectionString))
{
pgMatchResults = (from b in context.Blogs
where NpgsqlTextFunctions.MatchRegex(b.Name, pattern)
select b.Name).ToList();

pgMismatchResults = (from b in context.Blogs
where !NpgsqlTextFunctions.MatchRegex(b.Name, pattern)
select b.Name).ToList();

pgMatchWithOptionsResults = (from b in context.Blogs
where NpgsqlTextFunctions.MatchRegex(b.Name, pattern, RegexOptions.None)
select b.Name).ToList();

pgMismatchWithOptionsResults = (from b in context.Blogs
where !NpgsqlTextFunctions.MatchRegex(b.Name, pattern, RegexOptions.None)
select b.Name).ToList();
}

// Assert
Assert.That(netMatchResult, Is.True);
Assert.That(netMismatchResult, Is.False);

Assert.That(pgMatchResults.Count, Is.EqualTo(1));
Assert.That(pgMatchResults[0], Is.EqualTo(matchingInput));
Assert.That(pgMismatchResults.Count, Is.EqualTo(1));
Assert.That(pgMismatchResults[0], Is.EqualTo(mismatchingInput));

Assert.That(pgMatchWithOptionsResults.Count, Is.EqualTo(1));
Assert.That(pgMatchWithOptionsResults[0], Is.EqualTo(matchingInput));
Assert.That(pgMismatchWithOptionsResults.Count, Is.EqualTo(1));
Assert.That(pgMismatchWithOptionsResults[0], Is.EqualTo(mismatchingInput));
}

[Test]
[TestCase(RegexOptions.IgnoreCase, "some", "SOME", "placeholder", TestName = "IgnoreCase")]
[TestCase(RegexOptions.IgnorePatternWhitespace, "s o m e", "some", "s o m e", TestName = "IgnorePatternWhitespace")]
[TestCase(RegexOptions.Multiline, "^blog$", "some \nblog\n name", "placeholder", TestName = "Multiline")]
[TestCase(RegexOptions.Singleline, "some .* name", "some \n name", "placeholder", TestName = "Singleline")]
public void MatchRegexOptions(RegexOptions options, string pattern, string matchingInput, string mismatchingInput)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, remove "MatchRegexOptions" from the TestName property value.

{
// Arrange
using (var context = new BloggingContext(ConnectionString))
{
context.Database.Log = Console.Out.WriteLine;

context.Blogs.Add(new Blog() { Name = matchingInput });
context.Blogs.Add(new Blog() { Name = mismatchingInput });
context.SaveChanges();
}

// Act
// Ensure correctness of a test case
var netMatchResult = Regex.IsMatch(matchingInput, pattern, options);
var netMismatchResult = Regex.IsMatch(mismatchingInput, pattern, options);

List<string> pgMatchResults;
List<string> pgMismatchResults;
using (var context = new BloggingContext(ConnectionString))
{
pgMatchResults = (from b in context.Blogs
where NpgsqlTextFunctions.MatchRegex(b.Name, pattern, options)
select b.Name).ToList();

pgMismatchResults = (from b in context.Blogs
where !NpgsqlTextFunctions.MatchRegex(b.Name, pattern, options)
select b.Name).ToList();
}

// Assert
Assert.That(netMatchResult, Is.True);
Assert.That(netMismatchResult, Is.False);

Assert.That(pgMatchResults.Count, Is.EqualTo(1));
Assert.That(pgMatchResults[0], Is.EqualTo(matchingInput));
Assert.That(pgMismatchResults.Count, Is.EqualTo(1));
Assert.That(pgMismatchResults[0], Is.EqualTo(mismatchingInput));
}

[Test]
[TestCase(RegexOptions.RightToLeft)]
[TestCase(RegexOptions.ECMAScript)]
public void MatchRegex_NotSupportedOption(RegexOptions options)
{
using (var context = new BloggingContext(ConnectionString))
{
Assert.That(() =>
{
var results = (from b in context.Blogs
where NpgsqlTextFunctions.MatchRegex(b.Name, "Some pattern", options)
select b.Name).ToList();
}, Throws.InnerException.TypeOf<NotSupportedException>());
}
}
}
}