Skip to content

Commit 1cb678c

Browse files
authored
Apply document filters to DocumentSet<T> at query time (#378)
Motivation ---------- Some forms of querying using `DocumentSet<T>` properties on an inherited `BucketContext` don't apply document filters such as the `DocumentType` attribute. Modifications ------------- - Split `CollectionQueryable<T>` into two types `CollectionQueryable<T>` and a base `CouchbaseQueryable<T>`. The former is used to represent the original extent of a query against a collection. The latter is used to construct new `IQueryable<T>` instances as the query is extended with predicates, etc. This removes unnecessary fields and logic from the simpler case that is repeated for every new LINQ method applied to the query. - Add an additional non-generic `IDocumentSet` interface which is inherited by `IDocumentSet<T>`. This is a SemVer safe change because it only adds `IQueryable` which was already included on `IDocumentSet<T>` via `IQueryable<T>`. - Create DelayedFilterQueryProvider as a wrapper for ClusterQueryProvider that applies filters to `IDocumentSet` at query execution time. - Refactor query timeouts to always be based on a callback to the `BucketContext` rather than a property, which allows a singleton `ClusterQueryExecutor` per `BucketContext` rather than per query. - Refactor `BucketContext` to build a singleton query provider, query parser, and query executor rather than recreating for every query. - Refactor `DocumentSet<T>` to get the query provider from the `BucketContext`. - Reduce interface invocations by caching the bucket, scope, and collection names once when constructing a `DocumentSet<T>` or `CollectionQueryable<T>` instead of for each property read. - Minor perf and nullable ref type improvements to `DocumentFilterSet`. - Fix some integration tests that were refering to data no longer found in the default `beer-sample` bucket. Results ------- Filters are applied consistently to `DocumentSet<T>` based on the filter configuration at the time the query is run. Queries in all cases will have fewer heap allocations and other CPU performance benefits. Resolves #376
1 parent a999bd2 commit 1cb678c

15 files changed

Lines changed: 346 additions & 190 deletions

Src/Couchbase.Linq.IntegrationTests/QueryTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ public void Map2PocoTests_Simple_Projections_StartsWith()
174174
var context = new BucketContext(TestSetup.Bucket);
175175

176176
var beers = from b in context.Query<Beer>()
177-
where b.Type == "beer" && b.Name.StartsWith("563")
177+
where b.Type == "beer" && b.Name.StartsWith("Amendment")
178178
select new { name = b.Name, abv = b.Abv };
179179

180180
var results = beers.Take(1).ToList();
@@ -1000,7 +1000,7 @@ public void SubqueryTests_ArraySubqueryContains()
10001000
var context = new BucketContext(TestSetup.Bucket);
10011001

10021002
var breweries = from brewery in context.Query<Brewery>()
1003-
where brewery.Type == "brewery" && brewery.Address.Contains("563 Second Street")
1003+
where brewery.Type == "brewery" && brewery.Address.Contains("210 Aberdeen Dr.")
10041004
orderby brewery.Name
10051005
select new {name = brewery.Name, addresses = brewery.Address};
10061006

Src/Couchbase.Linq.IntegrationTests/SingleQueryTests.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public void Single_HasResult()
4646
var context = new BucketContext(TestSetup.Bucket);
4747

4848
var beers = from beer in context.Query<Beer>()
49-
where beer.Name == "21A IPA"
49+
where beer.Name == "Amendment Pale Ale"
5050
select new {beer.Name};
5151

5252
Console.WriteLine(beers.Single().Name);
@@ -58,8 +58,8 @@ public async Task SingleAsync_HasResult()
5858
var context = new BucketContext(TestSetup.Bucket);
5959

6060
var beers = from beer in context.Query<Beer>()
61-
where beer.Name == "21A IPA"
62-
select new {beer.Name};
61+
where beer.Name == "Amendment Pale Ale"
62+
select new { beer.Name };
6363

6464
Console.WriteLine((await beers.SingleAsync()).Name);
6565
}
@@ -72,7 +72,7 @@ public async Task SingleAsync_WithPredicate_HasResult()
7272
var beers = from beer in context.Query<Beer>()
7373
select new {beer.Name};
7474

75-
var result = await beers.SingleAsync(p => p.Name == "21A IPA");
75+
var result = await beers.SingleAsync(p => p.Name == "Amendment Pale Ale");
7676

7777
Console.WriteLine(result.Name);
7878
}
@@ -137,7 +137,7 @@ public void SingleOrDefault_HasResult()
137137
var context = new BucketContext(TestSetup.Bucket);
138138

139139
var beers = from beer in context.Query<Beer>()
140-
where beer.Name == "21A IPA"
140+
where beer.Name == "Amendment Pale Ale"
141141
select new {beer.Name};
142142

143143
var aBeer = beers.SingleOrDefault();
@@ -151,7 +151,7 @@ public async Task SingleOrDefaultAsync_HasResult()
151151
var context = new BucketContext(TestSetup.Bucket);
152152

153153
var beers = from beer in context.Query<Beer>()
154-
where beer.Name == "21A IPA"
154+
where beer.Name == "Amendment Pale Ale"
155155
select new {beer.Name};
156156

157157
var aBeer = await beers.SingleOrDefaultAsync();
@@ -167,7 +167,7 @@ public async Task SingleOrDefaultAsync_WithPredicate_HasResult()
167167
var beers = from beer in context.Query<Beer>()
168168
select new {beer.Name};
169169

170-
var aBeer = await beers.SingleOrDefaultAsync(p => p.Name == "21A IPA");
170+
var aBeer = await beers.SingleOrDefaultAsync(p => p.Name == "Amendment Pale Ale");
171171
Assert.IsNotNull(aBeer);
172172
Console.WriteLine(aBeer.Name);
173173
}

Src/Couchbase.Linq.UnitTests/Metadata/ContextMetadataTests.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
using System;
2-
using Couchbase.Linq.Metadata;
1+
using Couchbase.Linq.Metadata;
32
using Couchbase.Linq.UnitTests.Documents;
4-
using Moq;
53
using NUnit.Framework;
64

75
namespace Couchbase.Linq.UnitTests.Metadata
@@ -47,6 +45,10 @@ public void ctor_ValidInitializer()
4745

4846
private class TestContext : BucketContext
4947
{
48+
public TestContext() : base(QueryFactory.CreateMockBucket("default"))
49+
{
50+
}
51+
5052
public IDocumentSet<Beer> Beers { get; set; }
5153

5254
public IDocumentSet<RouteInCollection> Routes { get; set; }

Src/Couchbase.Linq.UnitTests/N1QLTestBase.cs

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Linq;
33
using System.Linq.Expressions;
4+
using System.Security.Cryptography.X509Certificates;
45
using Couchbase.Core.IO.Serializers;
56
using Couchbase.Core.Version;
67
using Couchbase.KeyValue;
@@ -13,6 +14,7 @@
1314
using Microsoft.Extensions.Logging;
1415
using Moq;
1516
using Newtonsoft.Json.Serialization;
17+
using Remotion.Linq;
1618

1719
namespace Couchbase.Linq.UnitTests
1820
{
@@ -109,30 +111,8 @@ internal string CreateN1QlQuery(IBucket bucket, Expression expression, ClusterVe
109111
return visitor.GetQuery();
110112
}
111113

112-
protected virtual IQueryable<T> CreateQueryable<T>(string bucketName)
113-
{
114-
return CreateQueryable<T>(bucketName, QueryExecutor);
115-
}
116-
117-
internal virtual IQueryable<T> CreateQueryable<T>(string bucketName, IAsyncQueryExecutor queryExecutor)
118-
{
119-
var mockCluster = new Mock<ICluster>();
120-
mockCluster
121-
.Setup(p => p.ClusterServices)
122-
.Returns(ServiceProvider);
123-
124-
var mockBucket = new Mock<IBucket>();
125-
mockBucket.SetupGet(e => e.Name).Returns(bucketName);
126-
mockBucket.SetupGet(e => e.Cluster).Returns(mockCluster.Object);
127-
128-
var mockCollection = new Mock<ICouchbaseCollection>();
129-
mockCollection
130-
.SetupGet(p => p.Scope.Bucket)
131-
.Returns(mockBucket.Object);
132-
133-
return new CollectionQueryable<T>(mockCollection.Object,
134-
QueryParserHelper.CreateQueryParser(mockCluster.Object), queryExecutor);
135-
}
114+
protected virtual IQueryable<T> CreateQueryable<T>(string bucketName) =>
115+
QueryFactory.Queryable<T>(bucketName, N1QlHelpers.DefaultScopeName, N1QlHelpers.DefaultCollectionName, QueryExecutor);
136116

137117
protected void SetContractResolver(IContractResolver contractResolver)
138118
{

Src/Couchbase.Linq.UnitTests/QueryFactory.cs

Lines changed: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,53 @@
22
using Couchbase.Core.IO.Serializers;
33
using Couchbase.Core.Version;
44
using Couchbase.KeyValue;
5+
using Couchbase.Linq.Execution;
6+
using Couchbase.Linq.Filters;
7+
using Couchbase.Linq.QueryGeneration;
58
using Couchbase.Linq.Serialization;
69
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Logging;
11+
using Microsoft.Extensions.Logging.Abstractions;
712
using Moq;
813

914
namespace Couchbase.Linq.UnitTests
1015
{
1116
internal class QueryFactory
1217
{
1318
public static IQueryable<T> Queryable<T>(IBucket bucket) =>
14-
Queryable<T>(bucket.Name, "_default", "_default");
19+
Queryable<T>(bucket.Name, N1QlHelpers.DefaultScopeName, N1QlHelpers.DefaultCollectionName);
1520

1621
public static IQueryable<T> Queryable<T>(IBucket bucket, string scopeName, string collectionName) =>
1722
Queryable<T>(bucket.Name, scopeName, collectionName);
1823

1924
public static IQueryable<T> Queryable<T>(string bucketName) =>
20-
Queryable<T>(bucketName, "_default", "_default");
25+
Queryable<T>(bucketName, N1QlHelpers.DefaultScopeName, N1QlHelpers.DefaultCollectionName);
2126

22-
public static IQueryable<T> Queryable<T>(string bucketName, string scopeName, string collectionName)
27+
public static IQueryable<T> Queryable<T>(string bucketName, string scopeName, string collectionName) =>
28+
Queryable<T>(bucketName, scopeName, collectionName, Mock.Of<IAsyncQueryExecutor>());
29+
30+
public static IQueryable<T> Queryable<T>(string bucketName, string scopeName, string collectionName, IAsyncQueryExecutor queryExecutor)
31+
{
32+
var mockCollection = CreateMockCollection(bucketName, scopeName, collectionName);
33+
34+
return new CollectionQueryable<T>(mockCollection,
35+
new ClusterQueryProvider(
36+
QueryParserHelper.CreateQueryParser(mockCollection.Scope.Bucket.Cluster),
37+
queryExecutor));
38+
}
39+
40+
public static ICouchbaseCollection CreateMockCollection(string bucketName, string scopeName, string collectionName) =>
41+
CreateMockBucket(bucketName).Scope(scopeName).Collection(collectionName);
42+
43+
public static IBucket CreateMockBucket(string bucketName)
2344
{
2445
var serializer = new DefaultSerializer();
2546

26-
var services = new ServiceCollection();
47+
IServiceCollection services = new ServiceCollection();
2748

2849
services.AddSingleton<ITypeSerializer>(serializer);
29-
services.AddLogging();
50+
services.AddSingleton(new DocumentFilterManager());
51+
services.Add(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(NullLogger<>)));
3052
services.AddSingleton(Mock.Of<IClusterVersionProvider>());
3153
services.AddSingleton<ISerializationConverterProvider>(
3254
new DefaultSerializationConverterProvider(serializer,
@@ -38,21 +60,42 @@ public static IQueryable<T> Queryable<T>(string bucketName, string scopeName, st
3860
.Returns(services.BuildServiceProvider());
3961

4062
var mockBucket = new Mock<IBucket>();
41-
mockBucket.SetupGet(e => e.Name).Returns(bucketName);
42-
mockBucket.SetupGet(e => e.Cluster).Returns(mockCluster.Object);
43-
44-
var mockCollection = new Mock<ICouchbaseCollection>();
45-
mockCollection
46-
.SetupGet(p => p.Scope.Bucket)
47-
.Returns(mockBucket.Object);
48-
mockCollection
49-
.SetupGet(p => p.Scope.Name)
50-
.Returns(scopeName);
51-
mockCollection
52-
.SetupGet(p => p.Name)
53-
.Returns(collectionName);
54-
55-
return new CollectionQueryable<T>(mockCollection.Object, default);
63+
mockBucket
64+
.SetupGet(e => e.Name)
65+
.Returns(bucketName);
66+
mockBucket
67+
.SetupGet(e => e.Cluster)
68+
.Returns(mockCluster.Object);
69+
mockBucket
70+
.Setup(e => e.Scope(It.IsAny<string>()))
71+
.Returns((string scopeName) =>
72+
{
73+
var mockScope = new Mock<IScope>();
74+
mockScope
75+
.SetupGet(p => p.Name)
76+
.Returns(scopeName);
77+
mockScope
78+
.SetupGet(p => p.Bucket)
79+
.Returns(mockBucket.Object);
80+
mockScope
81+
.Setup(e => e.Collection(It.IsAny<string>()))
82+
.Returns((string collectionName) =>
83+
{
84+
var mockCollection = new Mock<ICouchbaseCollection>();
85+
mockCollection
86+
.SetupGet(p => p.Name)
87+
.Returns(collectionName);
88+
mockCollection
89+
.SetupGet(p => p.Scope)
90+
.Returns(mockScope.Object);
91+
92+
return mockCollection.Object;
93+
});
94+
95+
return mockScope.Object;
96+
});
97+
98+
return mockBucket.Object;
5699
}
57100
}
58101
}

Src/Couchbase.Linq/BucketContext.cs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Linq;
3+
using Couchbase.Linq.Execution;
34
using Couchbase.Linq.Filters;
45
using Couchbase.Linq.Metadata;
56
using Couchbase.Linq.Utils;
@@ -13,23 +14,17 @@ namespace Couchbase.Linq
1314
public class BucketContext : IBucketContext
1415
{
1516
private readonly DocumentFilterManager _documentFilterManager;
16-
17-
/// <summary>
18-
/// Unit testing seam only, do not use!
19-
/// </summary>
20-
#pragma warning disable 8618
21-
internal BucketContext()
22-
#pragma warning restore 8618
23-
{
24-
}
17+
internal IAsyncQueryProvider QueryProvider { get; }
2518

2619
/// <summary>
2720
/// Creates a new BucketContext for a given Couchbase bucket.
2821
/// </summary>
2922
/// <param name="bucket">Bucket referenced by the new BucketContext.</param>
3023
public BucketContext(IBucket bucket)
3124
{
32-
Bucket = bucket ?? throw new ArgumentNullException(nameof(bucket));
25+
ThrowHelpers.ThrowIfNull(bucket);
26+
27+
Bucket = bucket;
3328

3429
try
3530
{
@@ -42,6 +37,16 @@ public BucketContext(IBucket bucket)
4237
$"{nameof(DocumentFilterManager)} has not been registered with the Couchbase Cluster. Be sure {nameof(LinqClusterOptionsExtensions.AddLinq)} is called on ${nameof(ClusterOptions)} during bootstrap.");
4338
}
4439

40+
var cluster = bucket.Cluster;
41+
var innerQueryProvider = new ClusterQueryProvider(
42+
QueryParserHelper.CreateQueryParser(cluster),
43+
new ClusterQueryExecutor(cluster)
44+
{
45+
QueryTimeoutProvider = () => QueryTimeout
46+
});
47+
48+
QueryProvider = new DelayedFilterQueryProvider(innerQueryProvider, _documentFilterManager);
49+
4550
var myType = GetType();
4651
if (myType != typeof(BucketContext))
4752
{
@@ -70,9 +75,9 @@ public IQueryable<T> Query<T>(BucketQueryOptions options)
7075

7176
internal IQueryable<T> Query<T>(string scope, string collection, BucketQueryOptions options = BucketQueryOptions.None)
7277
{
73-
IQueryable<T> query = new CollectionQueryable<T>(Bucket.Scope(scope).Collection(collection), QueryTimeout);
78+
IQueryable<T> query = new CollectionQueryable<T>(Bucket.Scope(scope).Collection(collection), QueryProvider);
7479

75-
if ((options & BucketQueryOptions.SuppressFilters) == BucketQueryOptions.None)
80+
if (!options.HasFlag(BucketQueryOptions.SuppressFilters))
7681
{
7782
query = _documentFilterManager.ApplyFilters(query);
7883
}

0 commit comments

Comments
 (0)