Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ThrowOnQueryWarning option #205

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
10 changes: 5 additions & 5 deletions src/CouchDB.Driver/CouchClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,19 +112,19 @@ private IFlurlClient GetConfiguredClient() =>
public ICouchDatabase<TSource> GetDatabase<TSource>(string database, string? discriminator = null) where TSource : CouchDocument
{
CheckDatabaseName(database);
var queryContext = new QueryContext(Endpoint, database);
var queryContext = new QueryContext(Endpoint, database, _options.ThrowOnQueryWarning);
return new CouchDatabase<TSource>(_flurlClient, _options, queryContext, discriminator);
}

/// <inheritdoc />
public async Task<ICouchDatabase<TSource>> CreateDatabaseAsync<TSource>(string database,
public async Task<ICouchDatabase<TSource>> CreateDatabaseAsync<TSource>(string database,
int? shards = null, int? replicas = null, bool? partitioned = null, string? discriminator = null, CancellationToken cancellationToken = default)
where TSource : CouchDocument
{
QueryContext queryContext = NewQueryContext(database);
IFlurlResponse response = await CreateDatabaseAsync(queryContext, shards, replicas, partitioned, cancellationToken)
.ConfigureAwait(false);

if (response.IsSuccessful())
{
return new CouchDatabase<TSource>(_flurlClient, _options, queryContext, discriminator);
Expand Down Expand Up @@ -167,7 +167,7 @@ public async Task DeleteDatabaseAsync(string database, CancellationToken cancell
.SendRequestAsync()
.ConfigureAwait(false);

if (!result.Ok)
if (!result.Ok)
{
throw new CouchException("Something went wrong during the delete.", null, "S");
}
Expand Down Expand Up @@ -433,7 +433,7 @@ private IFlurlRequest NewRequest()
private QueryContext NewQueryContext(string database)
{
CheckDatabaseName(database);
return new QueryContext(Endpoint, database);
return new QueryContext(Endpoint, database, _options.ThrowOnQueryWarning);
}

private void CheckDatabaseName(string database)
Expand Down
10 changes: 7 additions & 3 deletions src/CouchDB.Driver/CouchDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ internal CouchDatabase(IFlurlClient flurlClient, CouchOptions options, QueryCont
#region Find

/// <inheritdoc />
public Task<TSource?> FindAsync(string docId, bool withConflicts = false, CancellationToken cancellationToken = default)
public Task<TSource?> FindAsync(string docId, bool withConflicts = false, CancellationToken cancellationToken = default)
=> FindAsync(docId, new FindOptions { Conflicts = withConflicts }, cancellationToken);

/// <inheritdoc />
Expand Down Expand Up @@ -161,6 +161,10 @@ private async Task<List<TSource>> SendQueryAsync(Func<IFlurlRequest, Task<IFlurl
.SendRequestAsync()
.ConfigureAwait(false);

if (this._options.ThrowOnQueryWarning && !String.IsNullOrEmpty(findResult.Warning))
{
throw new CouchDBQueryWarningException(findResult.Warning);
}
var documents = findResult.Docs.ToList();

foreach (TSource document in documents)
Expand Down Expand Up @@ -433,7 +437,7 @@ public async Task<ChangesFeedResponse<TSource>> GetChangesAsync(ChangesFeedOptio
.ConfigureAwait(false)
: await request.QueryWithFilterAsync<TSource>(_queryProvider, filter, cancellationToken)
.ConfigureAwait(false);

if (string.IsNullOrWhiteSpace(_discriminator))
{
return response;
Expand Down Expand Up @@ -468,7 +472,7 @@ public async IAsyncEnumerable<ChangesFeedResponseResult<TSource>> GetContinuousC
.ConfigureAwait(false)
: await request.QueryContinuousWithFilterAsync<TSource>(_queryProvider, filter, cancellationToken)
.ConfigureAwait(false);

await foreach (var line in stream.ReadLinesAsync(cancellationToken))
{
if (string.IsNullOrEmpty(line))
Expand Down
3 changes: 3 additions & 0 deletions src/CouchDB.Driver/DTOs/FindResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ internal class FindResult<T>

[JsonProperty("execution_stats")]
public ExecutionStats ExecutionStats { get; internal set; }

[JsonProperty("warning")]
public string Warning { get; internal set; }
}
}
#nullable restore
12 changes: 12 additions & 0 deletions src/CouchDB.Driver/Exceptions/CouchDBQueryWarningException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace CouchDB.Driver.Exceptions
{
/// <summary>
/// The exception that is thrown if the query returns a warning.
/// </summary>
public class CouchDBQueryWarningException : Exception
{
public CouchDBQueryWarningException(string message) : base(message) { }
}
}
4 changes: 3 additions & 1 deletion src/CouchDB.Driver/Options/CouchOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ public abstract class CouchOptions
internal bool PluralizeEntities { get; set; }
internal DocumentCaseType DocumentsCaseType { get; set; }
internal PropertyCaseType PropertiesCase { get; set; }

internal string? DatabaseSplitDiscriminator { get; set; }
internal NullValueHandling? NullValueHandling { get; set; }
internal bool LogOutOnDispose { get; set; }

internal Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool>? ServerCertificateCustomValidationCallback { get; set; }
internal Action<ClientFlurlHttpSettings>? ClientFlurlHttpSettingsAction { get; set; }

internal bool ThrowOnQueryWarning { get; set; }

internal CouchOptions()
{
AuthenticationType = AuthenticationType.None;
Expand Down
6 changes: 6 additions & 0 deletions src/CouchDB.Driver/Options/CouchOptionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ public virtual CouchOptionsBuilder UseBasicAuthentication(string username, strin
return this;
}

public virtual CouchOptionsBuilder ThrowOnQueryWarning()
{
Options.ThrowOnQueryWarning = true;
return this;
}

/// <summary>
/// Enables cookie authentication.
/// For cookie authentication (RFC 2109) CouchDB generates a token that the client can use for the next few requests to CouchDB. Tokens are valid until a timeout.
Expand Down
5 changes: 4 additions & 1 deletion src/CouchDB.Driver/Query/QueryContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ public class QueryContext
public string DatabaseName { get; set; }
public string EscapedDatabaseName { get; set; }

public QueryContext(Uri endpoint, string databaseName)
public bool ThrowOnQueryWarning { get; set; }

public QueryContext(Uri endpoint, string databaseName, bool throwOnQueryWarning)
{
Endpoint = endpoint;
DatabaseName = databaseName;
EscapedDatabaseName = Uri.EscapeDataString(databaseName);
ThrowOnQueryWarning = throwOnQueryWarning;
}
}
}
14 changes: 12 additions & 2 deletions src/CouchDB.Driver/Query/QuerySender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading;
using System.Threading.Tasks;
using CouchDB.Driver.DTOs;
using CouchDB.Driver.Exceptions;
using CouchDB.Driver.Helpers;
using CouchDB.Driver.Types;
using Flurl.Http;
Expand Down Expand Up @@ -57,13 +58,22 @@ private async Task<CouchList<TItem>> ToListAsync<TItem>(string body, Cancellatio
return new CouchList<TItem>(result.Docs.ToList(), result.Bookmark, result.ExecutionStats);
}

private Task<FindResult<TItem>> SendAsync<TItem>(string body, CancellationToken cancellationToken) =>
_client
private async Task<FindResult<TItem>> SendAsync<TItem>(string body, CancellationToken cancellationToken)
{
var findResult = await _client
.Request(_queryContext.Endpoint)
.AppendPathSegments(_queryContext.DatabaseName, "_find")
.WithHeader("Content-Type", "application/json")
.PostStringAsync(body, cancellationToken)
.ReceiveJson<FindResult<TItem>>()
.SendRequestAsync();

if (this._queryContext.ThrowOnQueryWarning && !String.IsNullOrEmpty(findResult.Warning))
{
throw new CouchDBQueryWarningException(findResult.Warning);
}

return findResult;
}
}
}
41 changes: 39 additions & 2 deletions tests/CouchDB.Driver.E2ETests/Client_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
using System.Threading.Tasks;
using CouchDB.Driver.E2ETests;
using CouchDB.Driver.E2ETests.Models;
using CouchDB.Driver.Exceptions;
using CouchDB.Driver.Extensions;
using CouchDB.Driver.Local;
using Xunit;

namespace CouchDB.Driver.E2E
{
[Trait("Category", "Integration")]
public class ClientTests: IAsyncLifetime
public class ClientTests : IAsyncLifetime
{
private ICouchClient _client;
private ICouchDatabase<Rebel> _rebels;
Expand Down Expand Up @@ -152,7 +153,7 @@ public async Task Crud_SpecialCharacters()
public async Task Users()
{
var users = await _client.GetOrCreateUsersDatabaseAsync();

CouchUser luke = await users.AddAsync(new CouchUser(name: "luke", password: "lasersword"));
Assert.Equal("luke", luke.Name);

Expand Down Expand Up @@ -266,5 +267,41 @@ public async Task LocalDocuments()
var containsId = docs.Select(d => d.Id).Contains("_local/" + docId);
Assert.True(containsId);
}

[Fact]
public async Task ThrowOnQueryWarning()
{
await using var context = new MyDeathStarContextWithQueryWarning();
// There is an index for Name and Surname so it should not cause a warning
await context.Rebels.Where(r => r.Name == "Luke" && r.Surname == "Skywalker").ToListAsync();
try
{
// There is no index for Age so it should cause a warning
await context.Rebels.Where(r => r.Age == 19).ToListAsync();
Assert.Fail("Expected exception not thrown");
}
catch (CouchDBQueryWarningException e)
{
Assert.Equal("No matching index found, create an index to optimize query time.", e.Message);
}

var client = new CouchClient("http://localhost:5984", c =>
c.UseBasicAuthentication("admin", "admin")
.ThrowOnQueryWarning());
var crebels = client.GetDatabase<Rebel>();
// There is an index for Name and Surname so it should not cause a warning
await crebels.QueryAsync(@"{""selector"":{""$and"":[{""name"":""Luke""},{""surname"":""Skywalker""}]}}");
try
{
// There is no index for Age so it should cause a warning
await crebels.QueryAsync(@"{""selector"":{""age"":""19""}}");
Assert.Fail("Expected exception not thrown");
}
catch (CouchDBQueryWarningException e)
{
Assert.Equal("No matching index found, create an index to optimize query time.", e.Message);
}

}
}
}
25 changes: 24 additions & 1 deletion tests/CouchDB.Driver.E2ETests/MyDeathStarContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using CouchDB.Driver.E2ETests.Models;
using CouchDB.Driver.E2ETests.Models;
using CouchDB.Driver.Options;

namespace CouchDB.Driver.E2ETests
Expand Down Expand Up @@ -48,4 +48,27 @@ protected override void OnDatabaseCreating(CouchDatabaseBuilder databaseBuilder)
.ThenByDescending(r => r.Name));
}
}

public class MyDeathStarContextWithQueryWarning : CouchContext
{
public CouchDatabase<Rebel> Rebels { get; set; }

protected override void OnConfiguring(CouchOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseEndpoint("http://localhost:5984/")
.EnsureDatabaseExists()
.UseBasicAuthentication(username: "admin", password: "admin")
.ThrowOnQueryWarning();
}

protected override void OnDatabaseCreating(CouchDatabaseBuilder databaseBuilder)
{
databaseBuilder.Document<Rebel>()
.HasIndex("surnames_index", builder => builder
.IndexBy(r => r.Surname)
.ThenBy(r => r.Name));
}
}

}