Skip to content

Commit 8b8b511

Browse files
Merge pull request #38 from matteobortolazzo/in-memory_LINQ
In-memory execution for LINQ methods not supported by CouchDB
2 parents 94f19c3 + 8ba8738 commit 8b8b511

20 files changed

+504
-94
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
# 1.0.2 (2019-05-02)
1+
# 1.1.0 (2019-05-05)
2+
3+
## Features
4+
* **_find:** IQueryable methods that are not supported by CouchDB are evaluated in-memory using the IEnumerable counterpart, if possible.
5+
6+
# 1.0.2 (2019-05-02)
27

38
## Bug Fixes
49
* **_find:** Boolean member expressions converted to binary expressions in Where (Fix [#PR36](https://github.com/matteobortolazzo/couchdb-net/pull/36)).

LATEST_CHANGE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
## Bug Fixes
2-
* **_find:** Boolean member expression converted to binary expression in Where (Fix [#PR36](https://github.com/matteobortolazzo/couchdb-net/pull/36)).
1+
## Features
2+
* **_find:** IQueryable methods that are not supported by CouchDB are evaluated in-memory using the IEnumerable counterpart, if possible.

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,11 @@ If the Where method is not called in the expression, it will at an empty selecto
179179
| execution_stats | IncludeExecutionStats() |
180180
| conflicts | IncludeConflicts() |
181181

182+
### Other IQueryable methods?
183+
184+
IQueryable methods that are not natively supported by CouchDB are evaluated in-memory using the IEnumerable counterpart, if possible.
185+
**WARN** Max and Min are not working now because the mapping from IQueryable to IEnumerable is not 1 to 1.
186+
182187
## Client operations
183188

184189
```csharp

src/CouchDB.Driver/CouchClientAuthentication.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Flurl.Http;
55
using Nito.AsyncEx;
66
using System;
7+
using System.Collections.Generic;
78
using System.Linq;
89
using System.Net.Http;
910
using System.Text.RegularExpressions;
@@ -25,7 +26,7 @@ protected virtual void OnBeforeCall(HttpCall httpCall)
2526
case AuthenticationType.None:
2627
break;
2728
case AuthenticationType.Basic:
28-
httpCall.FlurlRequest.WithBasicAuth(_settings.Username, _settings.Password);
29+
httpCall.FlurlRequest = httpCall.FlurlRequest.WithBasicAuth(_settings.Username, _settings.Password);
2930
break;
3031
case AuthenticationType.Cookie:
3132
var isTokenExpired =
@@ -35,7 +36,7 @@ protected virtual void OnBeforeCall(HttpCall httpCall)
3536
{
3637
AsyncContext.Run(() => LoginAsync());
3738
}
38-
httpCall.FlurlRequest.EnableCookies().WithCookie("AuthSession", _cookieToken);
39+
httpCall.FlurlRequest = httpCall.FlurlRequest.EnableCookies().WithCookie("AuthSession", _cookieToken);
3940
break;
4041
default:
4142
throw new NotSupportedException($"Authentication of type {_settings.AuthenticationType} is not supported.");
@@ -55,7 +56,7 @@ private async Task LoginAsync()
5556

5657
_cookieCreationDate = DateTime.Now;
5758

58-
if (response.Headers.TryGetValues("Set-Cookie", out var values))
59+
if (response.Headers.TryGetValues("Set-Cookie", out IEnumerable<string> values))
5960
{
6061
var dirtyToken = values.First();
6162
var regex = new Regex(@"^AuthSession=(.+); Version=1; .*Path=\/; HttpOnly$");

src/CouchDB.Driver/CouchDB.Driver.csproj

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,13 @@
99
<PackageProjectUrl>https://github.com/matteobortolazzo/couchdb-net</PackageProjectUrl>
1010
<RepositoryUrl>https://github.com/matteobortolazzo/couchdb-ne</RepositoryUrl>
1111
<PackageTags>couchdb,driver,nosql,netstandard,pouchdb,xamarin</PackageTags>
12-
<PackageReleaseNotes>Complete rewrite.
13-
IQueryable support.
14-
All selectors support.</PackageReleaseNotes>
12+
<PackageReleaseNotes></PackageReleaseNotes>
1513
<PackageIconUrl>http://couchdb.apache.org/image/[email protected]</PackageIconUrl>
1614
<ApplicationIcon />
1715
<OutputType>Library</OutputType>
1816
<StartupObject />
1917
<NeutralLanguage>en</NeutralLanguage>
18+
<Version>1.1.0</Version>
2019
</PropertyGroup>
2120

2221
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

src/CouchDB.Driver/CouchQueryProvider.cs

Lines changed: 87 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -32,32 +32,31 @@ public override string GetQueryText(Expression expression)
3232
return Translate(expression);
3333
}
3434

35-
public override object Execute(Expression e, bool completeResponse)
35+
public override object Execute(Expression expression, bool completeResponse)
3636
{
37-
MethodInfo _filterMethodInfo = null;
38-
Expression[] _filteringExpressions = Array.Empty<Expression>();
39-
if (e is MethodCallExpression m)
37+
// Remove from the expressions tree all IQueryable methods not supported by CouchDB and put them into the list
38+
var unsupportedMethodCallExpressions = new List<MethodCallExpression>();
39+
expression = RemoveUnsupportedMethodExpressions(expression, out var hasUnsupportedMethods, unsupportedMethodCallExpressions);
40+
41+
var body = Translate(expression);
42+
Type elementType = TypeSystem.GetElementType(expression.Type);
43+
44+
// Create generic GetCouchList method and invoke it, sending the request to CouchDB
45+
MethodInfo method = typeof(CouchQueryProvider).GetMethod(nameof(CouchQueryProvider.GetCouchList));
46+
MethodInfo generic = method.MakeGenericMethod(elementType);
47+
var result = generic.Invoke(this, new[] { body });
48+
49+
// If no unsupported methods, return the result
50+
if (!hasUnsupportedMethods)
4051
{
41-
if (
42-
m.Method.Name == "First" ||
43-
m.Method.Name == "FirstOrDefault" ||
44-
m.Method.Name == "Last" ||
45-
m.Method.Name == "LastOrDefault" ||
46-
m.Method.Name == "Single" ||
47-
m.Method.Name == "SingleOrDefault")
48-
{
49-
_filterMethodInfo = m.Method;
50-
_filteringExpressions = m.Arguments.Skip(1).ToArray();
51-
e = m.Arguments[0];
52-
}
52+
return result;
5353
}
5454

55-
var body = Translate(e);
56-
Type elementType = TypeSystem.GetElementType(e.Type);
57-
58-
MethodInfo method = typeof(CouchQueryProvider).GetMethod(nameof(CouchQueryProvider.GetCouchListOrFiltered));
59-
MethodInfo generic = method.MakeGenericMethod(elementType);
60-
var result = generic.Invoke(this, new[] { body, (object)_filterMethodInfo, _filteringExpressions });
55+
// For every unsupported method expression, execute it on the result
56+
foreach (MethodCallExpression inMemoryCall in unsupportedMethodCallExpressions)
57+
{
58+
result = InvokeUnsupportedMethodCallExpression(result, inMemoryCall);
59+
}
6160
return result;
6261
}
6362

@@ -69,9 +68,9 @@ private string Translate(Expression e)
6968

7069
return new QueryTranslator(_settings).Translate(e);
7170
}
72-
73-
public object GetCouchListOrFiltered<T>(string body, MethodInfo filteringMethodInfo, Expression[] filteringExpressions)
74-
{
71+
72+
public object GetCouchList<T>(string body)
73+
{
7574
FindResult<T> result = _flurlClient
7675
.Request(_connectionString)
7776
.AppendPathSegments(_db, "_find")
@@ -80,49 +79,84 @@ public object GetCouchListOrFiltered<T>(string body, MethodInfo filteringMethodI
8079
.SendRequest();
8180

8281
var couchList = new CouchList<T>(result.Docs.ToList(), result.Bookmark, result.ExecutionStats);
82+
return couchList;
83+
}
8384

84-
if (filteringMethodInfo == null)
85+
private Expression RemoveUnsupportedMethodExpressions(Expression expression, out bool hasUnsupportedMethods, IList<MethodCallExpression> unsupportedMethodCallExpressions)
86+
{
87+
if (unsupportedMethodCallExpressions == null)
8588
{
86-
return couchList;
89+
throw new ArgumentNullException(nameof(unsupportedMethodCallExpressions));
8790
}
8891

89-
var filteringMethods = typeof(Enumerable).GetMethods()
90-
.Where(m =>
91-
m.Name == filteringMethodInfo.Name &&
92-
m.GetParameters().Length - 1 == filteringExpressions.Length)
93-
.OrderBy(m => m.GetParameters().Length).ToList();
94-
95-
96-
var invokeParameter = new object[filteringExpressions.Length + 1];
97-
invokeParameter[0] = couchList;
98-
99-
bool IsRightOverload(MethodInfo m)
92+
// Search for method calls to run in-memory,
93+
// Once one is found all method calls after that must run in-memory.
94+
// The expression to translate in JSON ends with the last not in-memory call.
95+
bool IsUnsupportedMethodCallExpression(Expression ex)
10096
{
101-
ParameterInfo[] parameters = m.GetParameters();
102-
for (var i = 0; i < filteringExpressions.Length; i++)
97+
if (ex is MethodCallExpression m)
10398
{
104-
var lamdaExpression = filteringExpressions[i] as UnaryExpression;
105-
if (lamdaExpression == null)
99+
Expression nextCall = m.Arguments[0];
100+
// Check if the next expression is unsupported
101+
var isUnsupported = IsUnsupportedMethodCallExpression(nextCall);
102+
if (isUnsupported)
106103
{
107-
return false;
104+
unsupportedMethodCallExpressions.Add(m);
105+
return isUnsupported;
108106
}
109-
110-
if (lamdaExpression.Operand.Type != parameters[i + 1].ParameterType)
107+
// If the next call is supported and the current is not in the supported list
108+
if (!QueryTranslator.NativeQueryableMethods.Contains(m.Method.Name))
111109
{
112-
return false;
110+
unsupportedMethodCallExpressions.Add(m);
111+
expression = nextCall;
112+
return true;
113113
}
114-
invokeParameter[i + 1] = lamdaExpression.Operand;
115114
}
116-
return true;
115+
return false;
117116
}
118117

119-
MethodInfo rightOverload = filteringMethods.Single(IsRightOverload);
120-
121-
MethodInfo enumerableGenericFilteringMethod = rightOverload.MakeGenericMethod(typeof(T));
122-
118+
hasUnsupportedMethods = IsUnsupportedMethodCallExpression(expression);
119+
return expression;
120+
}
123121

124-
var filtered = enumerableGenericFilteringMethod.Invoke(null, invokeParameter);
122+
private object InvokeUnsupportedMethodCallExpression(object result, MethodCallExpression methodCallExpression)
123+
{
124+
MethodInfo queryableMethodInfo = methodCallExpression.Method;
125+
Expression[] queryableMethodArguments = methodCallExpression.Arguments.ToArray();
126+
// Find the equivalent method in Enumerable
127+
MethodInfo enumarableMethodInfo = typeof(Enumerable).GetMethods().Single(enumerableMethodInfo =>
128+
{
129+
return
130+
queryableMethodInfo.Name == enumerableMethodInfo.Name &&
131+
ReflectionComparator.IsCompatible(queryableMethodInfo, enumerableMethodInfo);
132+
});
133+
134+
// Add the list as first parameter of the call
135+
var invokeParameter = new List<object> { result };
136+
// Convert everty other parameter expression to real values
137+
IEnumerable<object> enumerableParameters = queryableMethodArguments.Skip(1).Select(GetArgumentValueFromExpression);
138+
// Add the other parameter to the complete list
139+
invokeParameter.AddRange(enumerableParameters);
140+
141+
Type[] requestedGenericParameters = enumarableMethodInfo.GetGenericMethodDefinition().GetGenericArguments();
142+
Type[] genericParameters = queryableMethodInfo.GetGenericArguments();
143+
Type[] usableParameters = genericParameters.Take(requestedGenericParameters.Length).ToArray();
144+
MethodInfo enumarableGenericMethod = enumarableMethodInfo.MakeGenericMethod(usableParameters);
145+
var filtered = enumarableGenericMethod.Invoke(null, invokeParameter.ToArray());
125146
return filtered;
126147
}
148+
149+
private object GetArgumentValueFromExpression(Expression e)
150+
{
151+
if (e is ConstantExpression c)
152+
{
153+
return c.Value;
154+
}
155+
if (e is UnaryExpression u && u.Operand is LambdaExpression l)
156+
{
157+
return l.Compile();
158+
}
159+
throw new NotImplementedException($"Expression of type {e.NodeType} not supported.");
160+
}
127161
}
128162
}

src/CouchDB.Driver/Helpers/Evaluator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Linq;
55
using System.Linq.Expressions;
66

7+
#pragma warning disable IDE0058 // Expression value is never used
78
namespace CouchDB.Driver.Helpers
89
{
910
internal static class Evaluator
@@ -134,3 +135,4 @@ public override Expression Visit(Expression expression)
134135
}
135136
}
136137
}
138+
#pragma warning restore IDE0058 // Expression value is never used

src/CouchDB.Driver/Helpers/MicrosecondEpochConverter.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
using Newtonsoft.Json;
33
using Newtonsoft.Json.Converters;
44

5+
#pragma warning disable CA1812 // Avoid uninstantiated internal classes
56
namespace CouchDB.Driver.Helpers
67
{
7-
#pragma warning disable CA1812 // Avoid uninstantiated internal classes
88
internal class MicrosecondEpochConverter : DateTimeConverterBase
9-
#pragma warning restore CA1812 // Avoid uninstantiated internal classes
109
{
1110
private static readonly DateTime _epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
1211

@@ -17,8 +16,10 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
1716

1817
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
1918
{
20-
if (reader.Value == null) { return null; }
21-
return _epoch.AddMilliseconds((long)reader.Value / 1000d);
19+
return reader.Value != null ?
20+
(object)_epoch.AddMilliseconds((long)reader.Value / 1000d) :
21+
null;
2222
}
2323
}
24-
}
24+
}
25+
#pragma warning restore CA1812 // Avoid uninstantiated internal classes

0 commit comments

Comments
 (0)