Skip to content

Commit

Permalink
Merge pull request #41 from PSeON/dev
Browse files Browse the repository at this point in the history
POSIX regular expressions operators
  • Loading branch information
roji authored Jul 8, 2016
2 parents 7689c80 + 4214de4 commit 7c5a096
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 1 deletion.
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);

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)
{
// 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)
{
// 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>());
}
}
}
}

0 comments on commit 7c5a096

Please sign in to comment.