Skip to content

Commit 7c5a096

Browse files
authored
Merge pull request #41 from PSeON/dev
POSIX regular expressions operators
2 parents 7689c80 + 4214de4 commit 7c5a096

5 files changed

Lines changed: 270 additions & 1 deletion

File tree

src/EntityFramework6.Npgsql/NpgsqlTextFunctions.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Data.Entity;
33
using System.Diagnostics.CodeAnalysis;
4+
using System.Text.RegularExpressions;
45

56
namespace Npgsql
67
{
@@ -368,5 +369,31 @@ public static string TsRewrite(string query, string target, string substitute)
368369
{
369370
throw new NotSupportedException();
370371
}
371-
}
372+
373+
/// <summary>
374+
/// Matches regular expression. Generates the "~" operator.
375+
/// http://www.postgresql.org/docs/current/static/functions-matching.html#FUNCTIONS-POSIX-REGEXP
376+
/// This method follows the semantics of <see cref="Regex.IsMatch(string, string)"/>
377+
/// and it is translated to the equivalent PostgreSQL expression when executed.
378+
/// </summary>
379+
[DbFunction("Npgsql", "match_regex")]
380+
public static bool MatchRegex(string input, string pattern)
381+
{
382+
throw new NotSupportedException();
383+
}
384+
385+
/// <summary>
386+
/// Matches regular expression. Generates the "~" operator.
387+
/// http://www.postgresql.org/docs/current/static/functions-matching.html#FUNCTIONS-POSIX-REGEXP
388+
/// This method follows the semantics of <see cref="Regex.IsMatch(string, string, RegexOptions)"/>
389+
/// and it is translated to the equivalent PostgreSQL expression when executed.
390+
/// Options <see cref="RegexOptions.RightToLeft"/> and <see cref="RegexOptions.ECMAScript"/>
391+
/// are not supported.
392+
/// </summary>
393+
[DbFunction("Npgsql", "match_regex")]
394+
public static bool MatchRegex(string input, string pattern, RegexOptions options)
395+
{
396+
throw new NotSupportedException();
397+
}
398+
}
372399
}

src/EntityFramework6.Npgsql/SqlGenerators/SqlBaseGenerator.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
#endif
3636
using System.Linq;
3737
using JetBrains.Annotations;
38+
using System.Text.RegularExpressions;
39+
using System.Text;
3840

3941
namespace Npgsql.SqlGenerators
4042
{
@@ -1144,6 +1146,10 @@ VisitedExpression VisitFunction(EdmFunction function, IList<DbExpression> args,
11441146

11451147
return new CastExpression(args[0].Accept(this), "tsquery");
11461148
}
1149+
else if (functionName == "match_regex")
1150+
{
1151+
return VisitMatchRegex(function, args, resultType);
1152+
}
11471153
}
11481154

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

1169+
#if ENTITIES6
1170+
VisitedExpression VisitMatchRegex(EdmFunction function, IList<DbExpression> args, TypeUsage resultType)
1171+
{
1172+
if (args.Count != 2 && args.Count != 3)
1173+
throw new ArgumentException("Invalid number of arguments. Expected 2 or 3.", nameof(args));
1174+
1175+
var options = RegexOptions.None;
1176+
1177+
if (args.Count == 3)
1178+
{
1179+
var optionsExpression = args[2] as DbConstantExpression;
1180+
if (optionsExpression == null)
1181+
throw new NotSupportedException("Options must be constant expression.");
1182+
1183+
options = (RegexOptions)optionsExpression.Value;
1184+
}
1185+
1186+
if (options.HasFlag(RegexOptions.RightToLeft) || options.HasFlag(RegexOptions.ECMAScript))
1187+
{
1188+
throw new NotSupportedException("Options RightToLeft and ECMAScript are not supported.");
1189+
}
1190+
1191+
if (options == RegexOptions.Singleline)
1192+
{
1193+
return OperatorExpression.Build(
1194+
Operator.RegexMatch,
1195+
_useNewPrecedences,
1196+
args[0].Accept(this),
1197+
args[1].Accept(this));
1198+
}
1199+
1200+
var flags = new StringBuilder("(?");
1201+
1202+
if (options.HasFlag(RegexOptions.IgnoreCase))
1203+
{
1204+
flags.Append('i');
1205+
}
1206+
1207+
if (options.HasFlag(RegexOptions.Multiline))
1208+
{
1209+
flags.Append('n');
1210+
}
1211+
else if (!options.HasFlag(RegexOptions.Singleline))
1212+
{
1213+
// In .NET's default mode, . doesn't match newlines but PostgreSQL it does.
1214+
flags.Append('p');
1215+
}
1216+
1217+
if (options.HasFlag(RegexOptions.IgnorePatternWhitespace))
1218+
{
1219+
flags.Append('x');
1220+
}
1221+
1222+
flags.Append(')');
1223+
1224+
var primitiveType = PrimitiveType.GetEdmPrimitiveType(PrimitiveTypeKind.String);
1225+
var newRegexExpression = OperatorExpression.Build(
1226+
Operator.Concat,
1227+
_useNewPrecedences,
1228+
new ConstantExpression(flags.ToString(), TypeUsage.CreateStringTypeUsage(primitiveType, true, false)),
1229+
args[1].Accept(this));
1230+
1231+
return OperatorExpression.Build(
1232+
Operator.RegexMatch,
1233+
_useNewPrecedences,
1234+
args[0].Accept(this),
1235+
newRegexExpression);
1236+
}
1237+
#endif
1238+
11631239
VisitedExpression Substring(VisitedExpression source, VisitedExpression start, VisitedExpression count)
11641240
{
11651241
var substring = new FunctionExpression("substr");

src/EntityFramework6.Npgsql/SqlGenerators/VisitedExpression.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,7 @@ internal enum UnaryTypes {
844844
public static readonly Operator QueryNegate = new Operator("!!", 10, 8, UnaryTypes.Prefix, true);
845845
public static readonly Operator QueryContains = new Operator("@>", 10, 8);
846846
public static readonly Operator QueryIsContained = new Operator("<@", 10, 8);
847+
public static readonly Operator RegexMatch = new Operator("~", 10, 8);
847848

848849
public static readonly Dictionary<Operator, Operator> NegateDict;
849850

test/EntityFramework6.Npgsql.Tests/EntityFramework6.Npgsql.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
<ItemGroup>
7373
<Compile Include="EntityFrameworkBasicTests.cs" />
7474
<Compile Include="EntityFrameworkMigrationTests.cs" />
75+
<Compile Include="PatternMatchingTests.cs" />
7576
<Compile Include="Support\EntityFrameworkTestBase.cs" />
7677
<Compile Include="FullTextSearchTests.cs" />
7778
<Compile Include="NLogLoggingProvider.cs" />
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#region License
2+
// The PostgreSQL License
3+
//
4+
// Copyright (C) 2016 The Npgsql Development Team
5+
//
6+
// Permission to use, copy, modify, and distribute this software and its
7+
// documentation for any purpose, without fee, and without a written
8+
// agreement is hereby granted, provided that the above copyright notice
9+
// and this paragraph and the following two paragraphs appear in all copies.
10+
//
11+
// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY
12+
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
13+
// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
14+
// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF
15+
// THE POSSIBILITY OF SUCH DAMAGE.
16+
//
17+
// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES,
18+
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
19+
// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
20+
// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS
21+
// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
22+
#endregion
23+
24+
using Npgsql;
25+
using NUnit.Framework;
26+
using System;
27+
using System.Collections.Generic;
28+
using System.Data.Common;
29+
using System.Data.Entity;
30+
using System.Linq;
31+
using System.Text;
32+
using System.ComponentModel.DataAnnotations.Schema;
33+
using System.Data.Entity.Core.Metadata.Edm;
34+
using System.Data.Entity.Core.Objects;
35+
using System.Data.Entity.Infrastructure;
36+
using NpgsqlTypes;
37+
using System.Text.RegularExpressions;
38+
39+
namespace EntityFramework6.Npgsql.Tests
40+
{
41+
class PatternMatchingTests : EntityFrameworkTestBase
42+
{
43+
[Test]
44+
[TestCase("blog", "blog", "BLOG", TestName = "Case-sensitive")]
45+
[TestCase("^blog$", "blog", "some \nblog\n name", TestName = "^ and $ match beginning and end")]
46+
[TestCase("some .* name", "some blog name", "some \n name", TestName = ". matches all except \\n")]
47+
[TestCase("some blog name", "some blog name", "someblogname", TestName = "Whitespace not ignored in pattern")]
48+
public void MatchRegex(string pattern, string matchingInput, string mismatchingInput)
49+
{
50+
// Arrange
51+
using (var context = new BloggingContext(ConnectionString))
52+
{
53+
context.Database.Log = Console.Out.WriteLine;
54+
55+
context.Blogs.Add(new Blog() { Name = matchingInput });
56+
context.Blogs.Add(new Blog() { Name = mismatchingInput });
57+
context.SaveChanges();
58+
}
59+
60+
// Act
61+
// Ensure correctness of a test case
62+
var netMatchResult = Regex.IsMatch(matchingInput, pattern);
63+
var netMismatchResult = Regex.IsMatch(mismatchingInput, pattern);
64+
65+
List<string> pgMatchResults;
66+
List<string> pgMismatchResults;
67+
List<string> pgMatchWithOptionsResults;
68+
List<string> pgMismatchWithOptionsResults;
69+
using (var context = new BloggingContext(ConnectionString))
70+
{
71+
pgMatchResults = (from b in context.Blogs
72+
where NpgsqlTextFunctions.MatchRegex(b.Name, pattern)
73+
select b.Name).ToList();
74+
75+
pgMismatchResults = (from b in context.Blogs
76+
where !NpgsqlTextFunctions.MatchRegex(b.Name, pattern)
77+
select b.Name).ToList();
78+
79+
pgMatchWithOptionsResults = (from b in context.Blogs
80+
where NpgsqlTextFunctions.MatchRegex(b.Name, pattern, RegexOptions.None)
81+
select b.Name).ToList();
82+
83+
pgMismatchWithOptionsResults = (from b in context.Blogs
84+
where !NpgsqlTextFunctions.MatchRegex(b.Name, pattern, RegexOptions.None)
85+
select b.Name).ToList();
86+
}
87+
88+
// Assert
89+
Assert.That(netMatchResult, Is.True);
90+
Assert.That(netMismatchResult, Is.False);
91+
92+
Assert.That(pgMatchResults.Count, Is.EqualTo(1));
93+
Assert.That(pgMatchResults[0], Is.EqualTo(matchingInput));
94+
Assert.That(pgMismatchResults.Count, Is.EqualTo(1));
95+
Assert.That(pgMismatchResults[0], Is.EqualTo(mismatchingInput));
96+
97+
Assert.That(pgMatchWithOptionsResults.Count, Is.EqualTo(1));
98+
Assert.That(pgMatchWithOptionsResults[0], Is.EqualTo(matchingInput));
99+
Assert.That(pgMismatchWithOptionsResults.Count, Is.EqualTo(1));
100+
Assert.That(pgMismatchWithOptionsResults[0], Is.EqualTo(mismatchingInput));
101+
}
102+
103+
[Test]
104+
[TestCase(RegexOptions.IgnoreCase, "some", "SOME", "placeholder", TestName = "IgnoreCase")]
105+
[TestCase(RegexOptions.IgnorePatternWhitespace, "s o m e", "some", "s o m e", TestName = "IgnorePatternWhitespace")]
106+
[TestCase(RegexOptions.Multiline, "^blog$", "some \nblog\n name", "placeholder", TestName = "Multiline")]
107+
[TestCase(RegexOptions.Singleline, "some .* name", "some \n name", "placeholder", TestName = "Singleline")]
108+
public void MatchRegexOptions(RegexOptions options, string pattern, string matchingInput, string mismatchingInput)
109+
{
110+
// Arrange
111+
using (var context = new BloggingContext(ConnectionString))
112+
{
113+
context.Database.Log = Console.Out.WriteLine;
114+
115+
context.Blogs.Add(new Blog() { Name = matchingInput });
116+
context.Blogs.Add(new Blog() { Name = mismatchingInput });
117+
context.SaveChanges();
118+
}
119+
120+
// Act
121+
// Ensure correctness of a test case
122+
var netMatchResult = Regex.IsMatch(matchingInput, pattern, options);
123+
var netMismatchResult = Regex.IsMatch(mismatchingInput, pattern, options);
124+
125+
List<string> pgMatchResults;
126+
List<string> pgMismatchResults;
127+
using (var context = new BloggingContext(ConnectionString))
128+
{
129+
pgMatchResults = (from b in context.Blogs
130+
where NpgsqlTextFunctions.MatchRegex(b.Name, pattern, options)
131+
select b.Name).ToList();
132+
133+
pgMismatchResults = (from b in context.Blogs
134+
where !NpgsqlTextFunctions.MatchRegex(b.Name, pattern, options)
135+
select b.Name).ToList();
136+
}
137+
138+
// Assert
139+
Assert.That(netMatchResult, Is.True);
140+
Assert.That(netMismatchResult, Is.False);
141+
142+
Assert.That(pgMatchResults.Count, Is.EqualTo(1));
143+
Assert.That(pgMatchResults[0], Is.EqualTo(matchingInput));
144+
Assert.That(pgMismatchResults.Count, Is.EqualTo(1));
145+
Assert.That(pgMismatchResults[0], Is.EqualTo(mismatchingInput));
146+
}
147+
148+
[Test]
149+
[TestCase(RegexOptions.RightToLeft)]
150+
[TestCase(RegexOptions.ECMAScript)]
151+
public void MatchRegex_NotSupportedOption(RegexOptions options)
152+
{
153+
using (var context = new BloggingContext(ConnectionString))
154+
{
155+
Assert.That(() =>
156+
{
157+
var results = (from b in context.Blogs
158+
where NpgsqlTextFunctions.MatchRegex(b.Name, "Some pattern", options)
159+
select b.Name).ToList();
160+
}, Throws.InnerException.TypeOf<NotSupportedException>());
161+
}
162+
}
163+
}
164+
}

0 commit comments

Comments
 (0)