From 6dc66e32d6f2451b905c49297bf92285df3c7baa Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Sun, 13 Sep 2020 21:12:55 +0200 Subject: [PATCH 01/18] #100: Fix booleans in query parameters. --- src/CouchDB.Driver/CouchDatabase.cs | 2 +- .../Extensions/FlurlRequestExtensions.cs | 14 ++++++++++---- tests/CouchDB.Driver.UnitTests/Database_Tests.cs | 2 +- .../Feed/GetChanges_Tests.cs | 2 +- .../Feed/GetContinuousChangesAsync_Tests.cs | 2 +- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/CouchDB.Driver/CouchDatabase.cs b/src/CouchDB.Driver/CouchDatabase.cs index e45e06e..a3c7158 100644 --- a/src/CouchDB.Driver/CouchDatabase.cs +++ b/src/CouchDB.Driver/CouchDatabase.cs @@ -75,7 +75,7 @@ internal CouchDatabase(IFlurlClient flurlClient, CouchOptions options, QueryCont if (withConflicts) { - request = request.SetQueryParam("conflicts", true); + request = request.SetQueryParam("conflicts", "true"); } TSource document = await request diff --git a/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs b/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs index eae6e2a..616fafc 100644 --- a/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs +++ b/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs @@ -1,4 +1,5 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -44,12 +45,17 @@ public static Task PostStringStreamAsync( return request.SendAsync(HttpMethod.Post, capturedStringContent, cancellationToken, completionOption).ReceiveStream(); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "")] public static IFlurlRequest ApplyQueryParametersOptions(this IFlurlRequest request, object options) { - var queryParameters = OptionsHelper.ToQueryParameters(options); - foreach (var (name, value) in queryParameters) + IEnumerable<(string Name, object? Value)> queryParameters = OptionsHelper.ToQueryParameters(options); + foreach ((var name, object? value) in queryParameters) { - request = request.SetQueryParam(name, value); + object? finalValue = value?.GetType() == typeof(bool) + ? value.ToString().ToLowerInvariant() + : value; + + request = request.SetQueryParam(name, finalValue); } return request; diff --git a/tests/CouchDB.Driver.UnitTests/Database_Tests.cs b/tests/CouchDB.Driver.UnitTests/Database_Tests.cs index d24a11e..4a83aba 100644 --- a/tests/CouchDB.Driver.UnitTests/Database_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Database_Tests.cs @@ -58,7 +58,7 @@ public async Task FindWithConflicts() var newR = await _rebels.FindAsync("1", true); httpTest .ShouldHaveCalled("http://localhost/rebels/1") - .WithQueryParamValue("conflicts", true) + .WithQueryParamValue("conflicts", "true") .WithVerb(HttpMethod.Get); } diff --git a/tests/CouchDB.Driver.UnitTests/Feed/GetChanges_Tests.cs b/tests/CouchDB.Driver.UnitTests/Feed/GetChanges_Tests.cs index 90ad1e1..d98db93 100644 --- a/tests/CouchDB.Driver.UnitTests/Feed/GetChanges_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Feed/GetChanges_Tests.cs @@ -61,7 +61,7 @@ public async Task GetChangesAsync_WithOptions() httpTest .ShouldHaveCalled("http://localhost/rebels/_changes") .WithQueryParamValue("feed", "longpoll") - .WithQueryParamValue("attachments", true) + .WithQueryParamValue("attachments", "true") .WithVerb(HttpMethod.Get); } diff --git a/tests/CouchDB.Driver.UnitTests/Feed/GetContinuousChangesAsync_Tests.cs b/tests/CouchDB.Driver.UnitTests/Feed/GetContinuousChangesAsync_Tests.cs index 06573fc..57f8830 100644 --- a/tests/CouchDB.Driver.UnitTests/Feed/GetContinuousChangesAsync_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Feed/GetContinuousChangesAsync_Tests.cs @@ -75,7 +75,7 @@ public async Task GetContinuousChangesAsync_WithOptions() httpTest .ShouldHaveCalled("http://localhost/rebels/_changes") .WithQueryParamValue("feed", "continuous") - .WithQueryParamValue("attachments", true) + .WithQueryParamValue("attachments", "true") .WithVerb(HttpMethod.Get); } From d8c24ecd0f12b0c7f2242a0e86ef4569755b3f18 Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Sun, 13 Sep 2020 21:22:21 +0200 Subject: [PATCH 02/18] #101: New SetNullValueHandling method for the option builder. --- LATEST_CHANGE.md | 5 ++--- README.md | 1 + src/CouchDB.Driver/CouchClient.cs | 3 ++- src/CouchDB.Driver/Options/CouchOptions.cs | 2 ++ src/CouchDB.Driver/Options/CouchOptionsBuilder.cs | 12 ++++++++++++ src/CouchDB.Driver/Options/CouchOptionsBuilder`.cs | 9 +++++++++ 6 files changed, 28 insertions(+), 4 deletions(-) diff --git a/LATEST_CHANGE.md b/LATEST_CHANGE.md index 0ab4830..d5649fe 100644 --- a/LATEST_CHANGE.md +++ b/LATEST_CHANGE.md @@ -1,6 +1,5 @@ ## Features -* **Users"**: New `ChangeUserPassword` mathod for `ICouchDatabase`. +* **Null values"**: New `SetNullValueHandling` method for `CouchOptionsBuilder` to set how to handle null values. ## Bug Fixes -* **IsMatch**: Back to public instead of internal; -* **AddOrUpdate**: Added `Async` postfix. \ No newline at end of file +* **Conflicts**: Fix the query parameter value to get conflicts. \ No newline at end of file diff --git a/README.md b/README.md index eab7424..88d6d16 100644 --- a/README.md +++ b/README.md @@ -331,6 +331,7 @@ var client = new CouchClient("http://localhost:5984", builder => builder | DisableDocumentPluralization | Disables documents pluralization in requests. | | SetDocumentCase | Sets the format case for documents. | | SetPropertyCase | Sets the format case for properties. | +| SetNullValueHandling | Sets how to handle null values. | | DisableLogOutOnDispose | Disables log out on client dispose. | - **DocumentCaseTypes**: None, UnderscoreCase *(default)*, DashCase, KebabCase. diff --git a/src/CouchDB.Driver/CouchClient.cs b/src/CouchDB.Driver/CouchClient.cs index afe054a..e5357cd 100644 --- a/src/CouchDB.Driver/CouchClient.cs +++ b/src/CouchDB.Driver/CouchClient.cs @@ -90,7 +90,8 @@ private IFlurlClient GetConfiguredClient() => { s.JsonSerializer = new NewtonsoftJsonSerializer(new JsonSerializerSettings { - ContractResolver = new CouchContractResolver(_options.PropertiesCase) + ContractResolver = new CouchContractResolver(_options.PropertiesCase), + NullValueHandling = _options.NullValueHandling ?? NullValueHandling.Include }); s.BeforeCallAsync = OnBeforeCallAsync; if (_options.ServerCertificateCustomValidationCallback != null) diff --git a/src/CouchDB.Driver/Options/CouchOptions.cs b/src/CouchDB.Driver/Options/CouchOptions.cs index b5669c3..d777643 100644 --- a/src/CouchDB.Driver/Options/CouchOptions.cs +++ b/src/CouchDB.Driver/Options/CouchOptions.cs @@ -5,6 +5,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Flurl.Http.Configuration; +using Newtonsoft.Json; namespace CouchDB.Driver.Options { @@ -25,6 +26,7 @@ public abstract class CouchOptions internal bool PluralizeEntities { get; set; } internal DocumentCaseType DocumentsCaseType { get; set; } internal PropertyCaseType PropertiesCase { get; set; } + internal NullValueHandling? NullValueHandling { get; set; } internal bool LogOutOnDispose { get; set; } internal Func? ServerCertificateCustomValidationCallback { get; set; } diff --git a/src/CouchDB.Driver/Options/CouchOptionsBuilder.cs b/src/CouchDB.Driver/Options/CouchOptionsBuilder.cs index 7ff392b..b591ec3 100644 --- a/src/CouchDB.Driver/Options/CouchOptionsBuilder.cs +++ b/src/CouchDB.Driver/Options/CouchOptionsBuilder.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using CouchDB.Driver.Helpers; using Flurl.Http.Configuration; +using Newtonsoft.Json; namespace CouchDB.Driver.Options { @@ -181,6 +182,17 @@ public virtual CouchOptionsBuilder SetPropertyCase(PropertyCaseType type) return this; } + /// + /// Sets how to handle null values during serialization. + /// + /// The type of null value handling. + /// Return the current instance to chain calls. + public virtual CouchOptionsBuilder SetJsonNullValueHandling(NullValueHandling nullValueHandling) + { + Options.NullValueHandling = nullValueHandling; + return this; + } + /// /// Disables log out on client dispose. /// diff --git a/src/CouchDB.Driver/Options/CouchOptionsBuilder`.cs b/src/CouchDB.Driver/Options/CouchOptionsBuilder`.cs index 25506f2..5f2cbec 100644 --- a/src/CouchDB.Driver/Options/CouchOptionsBuilder`.cs +++ b/src/CouchDB.Driver/Options/CouchOptionsBuilder`.cs @@ -5,6 +5,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Flurl.Http.Configuration; +using Newtonsoft.Json; namespace CouchDB.Driver.Options { @@ -121,6 +122,14 @@ public CouchOptionsBuilder(CouchOptions options) : base(options) { } public new virtual CouchOptionsBuilder SetPropertyCase(PropertyCaseType type) => (CouchOptionsBuilder)base.SetPropertyCase(type); + /// + /// Sets how to handle null values during serialization. + /// + /// The type of null value handling. + /// Return the current instance to chain calls. + public new virtual CouchOptionsBuilder SetJsonNullValueHandling(NullValueHandling nullValueHandling) + => (CouchOptionsBuilder)base.SetJsonNullValueHandling(nullValueHandling); + /// /// Disables log out on client dispose. /// From 0c65adfe573054f8edcb72282b6d6d9dcd039a37 Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Sun, 13 Sep 2020 21:25:17 +0200 Subject: [PATCH 03/18] #101: Add NullValueHandling tests --- README.md | 2 + .../Settings_Tests.cs | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/README.md b/README.md index 88d6d16..7ae1d5b 100644 --- a/README.md +++ b/README.md @@ -549,3 +549,5 @@ Also, the configurator has `ConfigureFlurlClient` to set custom HTTP client opti Thanks to [Ben Origas](https://github.com/borigas) for features, ideas and tests like SSL custom validation, multi queryable, async deadlock, cookie authenication and many others. Thanks to [n9](https://github.com/n9) for proxy authentication, some bug fixes, suggestions and the great feedback on the changes feed feature! + +Thanks to [Marc](https://github.com/bender-ristone) for NullValueHandling, bug fixes and suggestions! diff --git a/tests/CouchDB.Driver.UnitTests/Settings_Tests.cs b/tests/CouchDB.Driver.UnitTests/Settings_Tests.cs index a037983..3a50d59 100644 --- a/tests/CouchDB.Driver.UnitTests/Settings_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Settings_Tests.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using CouchDB.Driver.Extensions; using CouchDB.Driver.Options; +using Newtonsoft.Json; using Xunit; namespace CouchDB.Driver.UnitTests @@ -189,5 +190,57 @@ public async Task DocumentName_JsonObject() } #endregion + + #region Null Value Handling + + [Fact] + public async Task PropertyNullValueHandling_NotSet() + { + using var httpTest = new HttpTest(); + // Logout + httpTest.RespondWithJson(new { ok = true }); + + await using var client = new CouchClient("http://localhost"); + var rebels = client.GetDatabase(); + await rebels.AddAsync(new Rebel()); + + var call = httpTest.CallLog.First(); + Assert.NotNull(call); + Assert.Equal(@"{""_conflicts"":[],""name"":null,""surname"":null,""age"":0,""isJedi"":false,""species"":0,""guid"":""00000000-0000-0000-0000-000000000000"",""skills"":null,""battles"":null}", call.RequestBody); + } + + [Fact] + public async Task PropertyNullValueHandling_Includes() + { + using var httpTest = new HttpTest(); + // Logout + httpTest.RespondWithJson(new { ok = true }); + + await using var client = new CouchClient("http://localhost", x => x.SetJsonNullValueHandling(NullValueHandling.Include)); + var rebels = client.GetDatabase(); + await rebels.AddAsync(new Rebel()); + + var call = httpTest.CallLog.First(); + Assert.NotNull(call); + Assert.Equal(@"{""_conflicts"":[],""name"":null,""surname"":null,""age"":0,""isJedi"":false,""species"":0,""guid"":""00000000-0000-0000-0000-000000000000"",""skills"":null,""battles"":null}", call.RequestBody); + } + + [Fact] + public async Task PropertyNullValueHandling_Ignore() + { + using var httpTest = new HttpTest(); + // Logout + httpTest.RespondWithJson(new { ok = true }); + + await using var client = new CouchClient("http://localhost", x => x.SetJsonNullValueHandling(NullValueHandling.Ignore)); + var rebels = client.GetDatabase(); + await rebels.AddAsync(new Rebel()); + + var call = httpTest.CallLog.First(); + Assert.NotNull(call); + Assert.Equal(@"{""_conflicts"":[],""age"":0,""isJedi"":false,""species"":0,""guid"":""00000000-0000-0000-0000-000000000000""}", call.RequestBody); + } + + #endregion } } From 644fc437b2f9bec0565b6fb3f2f79db6fc27214e Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Mon, 14 Sep 2020 14:01:59 +0200 Subject: [PATCH 04/18] #102: Ability to create an index --- LATEST_CHANGE.md | 1 + src/.editorconfig | 3 + .../ChangesFeedFilterExtensions.cs | 20 +-- src/CouchDB.Driver/CouchDatabase.cs | 50 +++++- .../Extensions/ExpressionExtensions.cs | 39 +++++ .../Extensions/FlurlRequestExtensions.cs | 1 - src/CouchDB.Driver/ICouchDatabase.cs | 16 +- src/CouchDB.Driver/Indexes/IIndexBuilder.cs | 35 ++++ src/CouchDB.Driver/Indexes/IndexBuilder.cs | 157 ++++++++++++++++++ src/CouchDB.Driver/Indexes/IndexOptions.cs | 8 + .../Options/DocumentCaseType.cs | 2 - .../Translators/MemberExpressionTranslator.cs | 31 +--- tests/CouchDB.Driver.UnitTests/Index_Tests.cs | 55 ++++++ 13 files changed, 367 insertions(+), 51 deletions(-) create mode 100644 src/CouchDB.Driver/Indexes/IIndexBuilder.cs create mode 100644 src/CouchDB.Driver/Indexes/IndexBuilder.cs create mode 100644 src/CouchDB.Driver/Indexes/IndexOptions.cs create mode 100644 tests/CouchDB.Driver.UnitTests/Index_Tests.cs diff --git a/LATEST_CHANGE.md b/LATEST_CHANGE.md index d5649fe..0242b64 100644 --- a/LATEST_CHANGE.md +++ b/LATEST_CHANGE.md @@ -1,4 +1,5 @@ ## Features +* **Indexes"**: Ability to create indexes. * **Null values"**: New `SetNullValueHandling` method for `CouchOptionsBuilder` to set how to handle null values. ## Bug Fixes diff --git a/src/.editorconfig b/src/.editorconfig index 671c781..d2a67dc 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -147,3 +147,6 @@ csharp_preserve_single_line_statements = true # CA1303: Do not pass literals as localized parameters dotnet_diagnostic.CA1303.severity = silent + +# CA1308: Normalize strings to uppercase +dotnet_diagnostic.CA1308.severity = suggestion \ No newline at end of file diff --git a/src/CouchDB.Driver/ChangesFeed/ChangesFeedFilterExtensions.cs b/src/CouchDB.Driver/ChangesFeed/ChangesFeedFilterExtensions.cs index 31671b4..0d006fc 100644 --- a/src/CouchDB.Driver/ChangesFeed/ChangesFeedFilterExtensions.cs +++ b/src/CouchDB.Driver/ChangesFeed/ChangesFeedFilterExtensions.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Linq; using System.Linq.Expressions; using System.Net.Http; using System.Threading; @@ -8,7 +7,6 @@ using CouchDB.Driver.ChangesFeed.Filters; using CouchDB.Driver.ChangesFeed.Responses; using CouchDB.Driver.Extensions; -using CouchDB.Driver.Options; using CouchDB.Driver.Query; using CouchDB.Driver.Types; using Flurl.Http; @@ -17,7 +15,7 @@ namespace CouchDB.Driver.ChangesFeed { internal static class ChangesFeedFilterExtensions { - public static async Task> QueryWithFilterAsync(this IFlurlRequest request, CouchOptions options, ChangesFeedFilter filter, + public static async Task> QueryWithFilterAsync(this IFlurlRequest request, IAsyncQueryProvider queryProvider, ChangesFeedFilter filter, CancellationToken cancellationToken) where TSource : CouchDocument { @@ -32,12 +30,9 @@ public static async Task> QueryWithFilterAsync selectorFilter) { - MethodCallExpression whereExpression = Expression.Call(typeof(Queryable), nameof(Queryable.Where), - new[] { typeof(TSource) }, Expression.Constant(Array.Empty().AsQueryable()), selectorFilter.Value); + MethodCallExpression whereExpression = selectorFilter.Value.WrapInWhereExpression(); + var jsonSelector = queryProvider.ToString(whereExpression); - var optimizer = new QueryOptimizer(); - Expression optimizedQuery = optimizer.Optimize(whereExpression); - var jsonSelector = new QueryTranslator(options).Translate(optimizedQuery); return await request .WithHeader("Content-Type", "application/json") .SetQueryParam("filter", "_selector") @@ -65,7 +60,7 @@ public static async Task> QueryWithFilterAsync QueryContinuousWithFilterAsync(this IFlurlRequest request, CouchOptions options, ChangesFeedFilter filter, CancellationToken cancellationToken) + public static async Task QueryContinuousWithFilterAsync(this IFlurlRequest request, IAsyncQueryProvider queryProvider, ChangesFeedFilter filter, CancellationToken cancellationToken) where TSource: CouchDocument { if (filter is DocumentIdsChangesFeedFilter documentIdsFilter) @@ -78,12 +73,9 @@ public static async Task QueryContinuousWithFilterAsync(this IF if (filter is SelectorChangesFeedFilter selectorFilter) { - MethodCallExpression whereExpression = Expression.Call(typeof(Queryable), nameof(Queryable.Where), - new[] { typeof(TSource) }, Expression.Constant(Array.Empty().AsQueryable()), selectorFilter.Value); + MethodCallExpression whereExpression = selectorFilter.Value.WrapInWhereExpression(); + var jsonSelector = queryProvider.ToString(whereExpression); - var optimizer = new QueryOptimizer(); - Expression optimizedQuery = optimizer.Optimize(whereExpression); - var jsonSelector = new QueryTranslator(options).Translate(optimizedQuery); return await request .WithHeader("Content-Type", "application/json") .SetQueryParam("filter", "_selector") diff --git a/src/CouchDB.Driver/CouchDatabase.cs b/src/CouchDB.Driver/CouchDatabase.cs index a3c7158..1a890e8 100644 --- a/src/CouchDB.Driver/CouchDatabase.cs +++ b/src/CouchDB.Driver/CouchDatabase.cs @@ -8,19 +8,22 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Linq.Expressions; -using System.Net; using System.Net.Http; using System.Runtime.CompilerServices; +using System.Text; using System.Threading; using System.Threading.Tasks; using CouchDB.Driver.ChangesFeed; using CouchDB.Driver.ChangesFeed.Responses; +using CouchDB.Driver.Indexes; using CouchDB.Driver.Local; using CouchDB.Driver.Options; using CouchDB.Driver.Query; +using Flurl.Util; using Newtonsoft.Json; namespace CouchDB.Driver @@ -374,7 +377,7 @@ public async Task> GetChangesAsync(ChangesFeedOptio return filter == null ? await request.GetJsonAsync>(cancellationToken) .ConfigureAwait(false) - : await request.QueryWithFilterAsync(_options, filter, cancellationToken) + : await request.QueryWithFilterAsync(_queryProvider, filter, cancellationToken) .ConfigureAwait(false); } @@ -396,7 +399,7 @@ public async IAsyncEnumerable> GetContinuousC await using Stream stream = filter == null ? await request.GetStreamAsync(cancellationToken, HttpCompletionOption.ResponseHeadersRead) .ConfigureAwait(false) - : await request.QueryContinuousWithFilterAsync(_options, filter, cancellationToken) + : await request.QueryContinuousWithFilterAsync(_queryProvider, filter, cancellationToken) .ConfigureAwait(false); await foreach (var line in stream.ReadLinesAsync(cancellationToken)) @@ -413,6 +416,45 @@ public async IAsyncEnumerable> GetContinuousC #endregion + #region Index + + public async Task CreateIndexAsync(string name, Action> indexBuilderAction, IndexOptions? options = null) + { + Check.NotNull(name, nameof(name)); + Check.NotNull(indexBuilderAction, nameof(indexBuilderAction)); + + var builder = new IndexBuilder(_options, _queryProvider); + indexBuilderAction(builder); + + var indexJson = builder.ToString(); + + var sb = new StringBuilder(); + sb.Append("{") + .Append($"\"index\":{indexJson},") + .Append($"\"name\":\"{name}\",") + .Append("\"type\":\"json\""); + + if (options?.DesignDocument != null) + { + sb.Append($",\"ddoc\":\"{options.DesignDocument}\""); + } + if (options?.Partitioned != null) + { + sb.Append($",\"partitioned\":{options.Partitioned.ToString().ToLowerInvariant()}"); + } + + sb.Append("}"); + + var request = sb.ToString(); + + await NewRequest().AppendPathSegment("_index") + .PostStringAsync(request) + .SendRequestAsync() + .ConfigureAwait(false); + } + + #endregion + #region Utils /// @@ -495,4 +537,4 @@ internal CouchQueryable AsQueryable() #endregion } -} +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Extensions/ExpressionExtensions.cs b/src/CouchDB.Driver/Extensions/ExpressionExtensions.cs index 5e3cd08..0a21209 100644 --- a/src/CouchDB.Driver/Extensions/ExpressionExtensions.cs +++ b/src/CouchDB.Driver/Extensions/ExpressionExtensions.cs @@ -1,11 +1,43 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; +using CouchDB.Driver.Options; namespace CouchDB.Driver.Extensions { internal static class ExpressionExtensions { + public static MemberExpression ToMemberExpression(this Expression selector) + { + if (!(selector is LambdaExpression l) || !(l.Body is MemberExpression m)) + { + throw new ArgumentException("The given expression does not select a property.", nameof(selector)); + } + + return m; + } + + public static string GetPropertyName(this MemberExpression m, CouchOptions options) + { + PropertyCaseType caseType = options.PropertiesCase; + + var members = new List { m.Member.GetCouchPropertyName(caseType) }; + + Expression currentExpression = m.Expression; + + while (currentExpression is MemberExpression cm) + { + members.Add(cm.Member.GetCouchPropertyName(caseType)); + currentExpression = cm.Expression; + } + + members.Reverse(); + var propName = string.Join(".", members.ToArray()); + + return propName; + } + public static bool ContainsSelector(this Expression expression) => expression is MethodCallExpression m && m.Arguments.Count == 2 && m.Arguments[1].IsSelectorExpression(); @@ -39,6 +71,13 @@ public static Expression WrapInLambda(this Expression body, IReadOnlyCollection< return Expression.Quote(lambdaExpression); } + public static MethodCallExpression WrapInWhereExpression(this Expression> selector) + { + MethodCallExpression whereExpression = Expression.Call(typeof(Queryable), nameof(Queryable.Where), + new[] { typeof(TSource) }, Expression.Constant(Array.Empty().AsQueryable()), selector); + return whereExpression; + } + private static Expression StripQuotes(this Expression e) { while (e.NodeType == ExpressionType.Quote) diff --git a/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs b/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs index 616fafc..f83a5a4 100644 --- a/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs +++ b/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs @@ -45,7 +45,6 @@ public static Task PostStringStreamAsync( return request.SendAsync(HttpMethod.Post, capturedStringContent, cancellationToken, completionOption).ReceiveStream(); } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "")] public static IFlurlRequest ApplyQueryParametersOptions(this IFlurlRequest request, object options) { IEnumerable<(string Name, object? Value)> queryParameters = OptionsHelper.ToQueryParameters(options); diff --git a/src/CouchDB.Driver/ICouchDatabase.cs b/src/CouchDB.Driver/ICouchDatabase.cs index 7c08239..82970fc 100644 --- a/src/CouchDB.Driver/ICouchDatabase.cs +++ b/src/CouchDB.Driver/ICouchDatabase.cs @@ -1,9 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using CouchDB.Driver.ChangesFeed; using CouchDB.Driver.ChangesFeed.Responses; +using CouchDB.Driver.Indexes; using CouchDB.Driver.Local; using CouchDB.Driver.Security; using CouchDB.Driver.Types; @@ -124,7 +126,17 @@ IAsyncEnumerable> GetContinuousChangesAsync( CancellationToken cancellationToken); /// - /// Asynchronously downloads a specific attachment. + /// Creates an index for the current database with the given configuration. + /// + /// The name of the index. + /// The action to configure the index builder. + /// The index options. + /// A task that represents the asynchronous operation. + Task CreateIndexAsync(string name, Action> indexBuilderAction, + IndexOptions? options = null); + + /// + /// Asynchronously downloads a specific attachment. /// /// The attachment to download. /// Path of local folder where file is to be downloaded. diff --git a/src/CouchDB.Driver/Indexes/IIndexBuilder.cs b/src/CouchDB.Driver/Indexes/IIndexBuilder.cs new file mode 100644 index 0000000..d0467ff --- /dev/null +++ b/src/CouchDB.Driver/Indexes/IIndexBuilder.cs @@ -0,0 +1,35 @@ +using System; +using System.Linq.Expressions; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Indexes +{ + public interface IIndexBuilder + where TSource : CouchDocument + { + IMultiFieldIndexBuilder IndexBy(Expression> selector); + } + + public interface IMultiFieldIndexBuilder : IIndexBuilder + where TSource : CouchDocument + { + IMultiFieldIndexBuilder AlsoBy(Expression> selector); + IMultiFieldIndexBuilder Where(Expression> selector); + IOrderedIndexBuilder OrderBy(Expression> selector); + IOrderedDescendingIndexBuilder OrderByDescending(Expression> selector); + IMultiFieldIndexBuilder Take(int take); + IMultiFieldIndexBuilder Skip(int skip); + } + + public interface IOrderedIndexBuilder : IMultiFieldIndexBuilder + where TSource : CouchDocument + { + IOrderedIndexBuilder ThenBy(Expression> selector); + } + + public interface IOrderedDescendingIndexBuilder : IMultiFieldIndexBuilder + where TSource : CouchDocument + { + IOrderedDescendingIndexBuilder ThenByDescending(Expression> selector); + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Indexes/IndexBuilder.cs b/src/CouchDB.Driver/Indexes/IndexBuilder.cs new file mode 100644 index 0000000..9b8a60e --- /dev/null +++ b/src/CouchDB.Driver/Indexes/IndexBuilder.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using CouchDB.Driver.Extensions; +using CouchDB.Driver.Options; +using CouchDB.Driver.Query; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Indexes +{ + internal class IndexBuilder: IOrderedIndexBuilder, IOrderedDescendingIndexBuilder + where TSource : CouchDocument + { + private readonly CouchOptions _options; + private readonly IAsyncQueryProvider _queryProvider; + + private bool _ascending = true; + private readonly List _fields; + private readonly List _fieldsOrder; + private int? _toTake; + private int? _toSkip; + private string? _selector; + + public IndexBuilder(CouchOptions options, IAsyncQueryProvider queryProvider) + { + _options = options; + _queryProvider = queryProvider; + _fields = new List(); + _fieldsOrder = new List(); + } + + public IMultiFieldIndexBuilder IndexBy(Expression> selector) + { + var m = selector.ToMemberExpression(); + _fields.Clear(); + _fields.Add(m.GetPropertyName(_options)); + return this; + } + + public IMultiFieldIndexBuilder Where(Expression> selector) + { + MethodCallExpression whereExpression = selector.WrapInWhereExpression(); + var jsonSelector = _queryProvider.ToString(whereExpression); + _selector = jsonSelector.Substring(1, jsonSelector.Length - 2); + return this; + } + + public IOrderedIndexBuilder OrderBy(Expression> selector) + { + var m = selector.ToMemberExpression(); + _ascending = true; + _fieldsOrder.Clear(); + _fieldsOrder.Add(m.GetPropertyName(_options)); + return this; + } + + public IOrderedDescendingIndexBuilder OrderByDescending(Expression> selector) + { + var m = selector.ToMemberExpression(); + _ascending = false; + _fieldsOrder.Clear(); + _fieldsOrder.Add(m.GetPropertyName(_options)); + return this; + } + + public IMultiFieldIndexBuilder Take(int take) + { + _toTake = take; + return this; + } + + public IMultiFieldIndexBuilder Skip(int skip) + { + _toSkip = skip; + return this; + } + + public IMultiFieldIndexBuilder AlsoBy(Expression> selector) + { + var m = selector.ToMemberExpression(); + _fields.Add(m.GetPropertyName(_options)); + return this; + } + + IOrderedIndexBuilder IOrderedIndexBuilder.ThenBy(Expression> selector) + { + var m = selector.ToMemberExpression(); + _fieldsOrder.Add(m.GetPropertyName(_options)); + return this; + } + + IOrderedDescendingIndexBuilder IOrderedDescendingIndexBuilder.ThenByDescending(Expression> selector) + { + var m = selector.ToMemberExpression(); + _fieldsOrder.Add(m.GetPropertyName(_options)); + return this; + } + + public override string ToString() + { + var sb = new StringBuilder(); + + sb.Append("{"); + + // Selector + if (_selector != null) + { + sb.Append(_selector); + sb.Append(","); + } + + // Fields + sb.Append("\"fields\":["); + foreach (var field in _fields) + { + sb.Append($"\"{field}\","); + } + + sb.Length--; + sb.Append("],"); + + // Sort + if (_fieldsOrder.Any()) + { + sb.Append("\"sort\":["); + var order = _ascending ? "asc" : "desc"; + + foreach (var field in _fieldsOrder) + { + sb.Append($"{{\"{field}\":\"{order}\"}},"); + } + + sb.Length--; + sb.Append("],"); + } + + // Limit + if (_toTake != null) + { + sb.Append($"\"limit\":{_toTake},"); + } + + // Skip + if (_toSkip != null) + { + sb.Append($"\"skip\":{_toSkip},"); + } + + sb.Length--; + sb.Append("}"); + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Indexes/IndexOptions.cs b/src/CouchDB.Driver/Indexes/IndexOptions.cs new file mode 100644 index 0000000..84a0217 --- /dev/null +++ b/src/CouchDB.Driver/Indexes/IndexOptions.cs @@ -0,0 +1,8 @@ +namespace CouchDB.Driver.Indexes +{ + public class IndexOptions + { + public string? DesignDocument { get; set; } + public bool? Partitioned { get; set; } + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Options/DocumentCaseType.cs b/src/CouchDB.Driver/Options/DocumentCaseType.cs index 5e5ef39..eea4bc6 100644 --- a/src/CouchDB.Driver/Options/DocumentCaseType.cs +++ b/src/CouchDB.Driver/Options/DocumentCaseType.cs @@ -24,7 +24,6 @@ private DocumentCaseType(string value) : base(value) { } /// public static readonly DocumentCaseType KebabCase = new DocumentCaseType("KebabCase"); -#pragma warning disable CA1308 // Normalize strings to uppercase internal override string Convert(string str) { if (Equals(this, None)) @@ -41,6 +40,5 @@ internal override string Convert(string str) } throw new NotSupportedException($"Value {Value} not supported."); } -#pragma warning restore CA1308 // Normalize strings to uppercase } } \ No newline at end of file diff --git a/src/CouchDB.Driver/Query/Translators/MemberExpressionTranslator.cs b/src/CouchDB.Driver/Query/Translators/MemberExpressionTranslator.cs index 257761b..4d7e7b7 100644 --- a/src/CouchDB.Driver/Query/Translators/MemberExpressionTranslator.cs +++ b/src/CouchDB.Driver/Query/Translators/MemberExpressionTranslator.cs @@ -1,7 +1,5 @@ -using Newtonsoft.Json; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Reflection; +using System.Linq.Expressions; +using CouchDB.Driver.Extensions; namespace CouchDB.Driver.Query { @@ -9,30 +7,7 @@ internal partial class QueryTranslator { protected override Expression VisitMember(MemberExpression m) { - string GetPropertyName(MemberInfo memberInfo) - { - var jsonPropertyAttributes = memberInfo.GetCustomAttributes(typeof(JsonPropertyAttribute), true); - JsonPropertyAttribute? jsonProperty = jsonPropertyAttributes.Length > 0 - ? jsonPropertyAttributes[0] as JsonPropertyAttribute - : null; - - return jsonProperty != null - ? jsonProperty.PropertyName - : _options.PropertiesCase.Convert(memberInfo.Name); - } - - var members = new List { GetPropertyName(m.Member) }; - - Expression currentExpression = m.Expression; - - while (currentExpression is MemberExpression cm) - { - members.Add(GetPropertyName(cm.Member)); - currentExpression = cm.Expression; - } - - members.Reverse(); - var propName = string.Join(".", members.ToArray()); + var propName = m.GetPropertyName(_options); _sb.Append($"\"{propName}\""); return m; diff --git a/tests/CouchDB.Driver.UnitTests/Index_Tests.cs b/tests/CouchDB.Driver.UnitTests/Index_Tests.cs new file mode 100644 index 0000000..d4c6fc8 --- /dev/null +++ b/tests/CouchDB.Driver.UnitTests/Index_Tests.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using CouchDB.Driver.Indexes; +using CouchDB.Driver.UnitTests.Models; +using Flurl.Http.Testing; +using Xunit; + +namespace CouchDB.Driver.UnitTests +{ + public class Index_Tests + { + private readonly ICouchClient _client; + private readonly ICouchDatabase _rebels; + + public Index_Tests() + { + _client = new CouchClient("http://localhost"); + _rebels = _client.GetDatabase(); + } + + [Fact] + public async Task CreateIndexAsync() + { + using var httpTest = new HttpTest(); + httpTest.RespondWithJson(new + { + result = "created" + }); + + await _rebels.CreateIndexAsync("skywalkers", b => b + .IndexBy(r => r.Surname) + .AlsoBy(r => r.Name) + .Where(r => r.Surname == "Skywalker" && r.Age > 3) + .OrderByDescending(r => r.Surname) + .ThenByDescending(r => r.Name) + .Take(100) + .Skip(1), + new IndexOptions() + { + DesignDocument = "skywalkers_ddoc", + Partitioned = true + }); + + + var expectedBody = + "{\"index\":{\"selector\":{\"$and\":[{\"surname\":\"Skywalker\"},{\"age\":{\"$gt\":3}}]},\"fields\":[\"surname\",\"name\"],\"sort\":[{\"surname\":\"desc\"},{\"name\":\"desc\"}],\"limit\":100,\"skip\":1},\"name\":\"skywalkers\",\"type\":\"json\",\"ddoc\":\"skywalkers_ddoc\",\"partitioned\":true}"; + httpTest + .ShouldHaveCalled("http://localhost/rebels/_index") + .WithRequestBody(expectedBody) + .WithVerb(HttpMethod.Post); + } + } +} From 409954ff95cb6892e9d9722bbe7852aba1864f68 Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Mon, 14 Sep 2020 14:20:35 +0200 Subject: [PATCH 05/18] #102: Implements partial indexes. --- src/CouchDB.Driver/CouchDatabase.cs | 2 - src/CouchDB.Driver/Indexes/IIndexBuilder.cs | 1 + src/CouchDB.Driver/Indexes/IndexBuilder.cs | 18 ++++++ tests/CouchDB.Driver.UnitTests/Index_Tests.cs | 60 +++++++++++++++++-- 4 files changed, 73 insertions(+), 8 deletions(-) diff --git a/src/CouchDB.Driver/CouchDatabase.cs b/src/CouchDB.Driver/CouchDatabase.cs index 1a890e8..d4b7c33 100644 --- a/src/CouchDB.Driver/CouchDatabase.cs +++ b/src/CouchDB.Driver/CouchDatabase.cs @@ -8,7 +8,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Linq.Expressions; @@ -23,7 +22,6 @@ using CouchDB.Driver.Local; using CouchDB.Driver.Options; using CouchDB.Driver.Query; -using Flurl.Util; using Newtonsoft.Json; namespace CouchDB.Driver diff --git a/src/CouchDB.Driver/Indexes/IIndexBuilder.cs b/src/CouchDB.Driver/Indexes/IIndexBuilder.cs index d0467ff..ccf19a2 100644 --- a/src/CouchDB.Driver/Indexes/IIndexBuilder.cs +++ b/src/CouchDB.Driver/Indexes/IIndexBuilder.cs @@ -19,6 +19,7 @@ public interface IMultiFieldIndexBuilder : IIndexBuilder IOrderedDescendingIndexBuilder OrderByDescending(Expression> selector); IMultiFieldIndexBuilder Take(int take); IMultiFieldIndexBuilder Skip(int skip); + IMultiFieldIndexBuilder ExcludeWhere(Expression> selector); } public interface IOrderedIndexBuilder : IMultiFieldIndexBuilder diff --git a/src/CouchDB.Driver/Indexes/IndexBuilder.cs b/src/CouchDB.Driver/Indexes/IndexBuilder.cs index 9b8a60e..bdc6015 100644 --- a/src/CouchDB.Driver/Indexes/IndexBuilder.cs +++ b/src/CouchDB.Driver/Indexes/IndexBuilder.cs @@ -22,6 +22,7 @@ internal class IndexBuilder: IOrderedIndexBuilder, IOrderedDes private int? _toTake; private int? _toSkip; private string? _selector; + private string? _partialSelector; public IndexBuilder(CouchOptions options, IAsyncQueryProvider queryProvider) { @@ -77,6 +78,16 @@ public IMultiFieldIndexBuilder Skip(int skip) return this; } + public IMultiFieldIndexBuilder ExcludeWhere(Expression> selector) + { + MethodCallExpression whereExpression = selector.WrapInWhereExpression(); + var jsonSelector = _queryProvider.ToString(whereExpression); + _partialSelector = jsonSelector + .Substring(1, jsonSelector.Length - 2) + .Replace("selector", "partial_filter_selector", StringComparison.CurrentCultureIgnoreCase); + return this; + } + public IMultiFieldIndexBuilder AlsoBy(Expression> selector) { var m = selector.ToMemberExpression(); @@ -111,6 +122,13 @@ public override string ToString() sb.Append(","); } + // Partial Selector + if (_partialSelector != null) + { + sb.Append(_partialSelector); + sb.Append(","); + } + // Fields sb.Append("\"fields\":["); foreach (var field in _fields) diff --git a/tests/CouchDB.Driver.UnitTests/Index_Tests.cs b/tests/CouchDB.Driver.UnitTests/Index_Tests.cs index d4c6fc8..59862ce 100644 --- a/tests/CouchDB.Driver.UnitTests/Index_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Index_Tests.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; +using System.Net.Http; using System.Threading.Tasks; using CouchDB.Driver.Indexes; using CouchDB.Driver.UnitTests.Models; @@ -21,7 +19,56 @@ public Index_Tests() } [Fact] - public async Task CreateIndexAsync() + public async Task CreateIndex_Basic() + { + using var httpTest = new HttpTest(); + httpTest.RespondWithJson(new + { + result = "created" + }); + + await _rebels.CreateIndexAsync("skywalkers", b => b + .IndexBy(r => r.Surname)); + + + var expectedBody = + "{\"index\":{\"fields\":[\"surname\"]},\"name\":\"skywalkers\",\"type\":\"json\"}"; + httpTest + .ShouldHaveCalled("http://localhost/rebels/_index") + .WithRequestBody(expectedBody) + .WithVerb(HttpMethod.Post); + } + + [Fact] + public async Task CreateIndex_Simple_WithOptions() + { + using var httpTest = new HttpTest(); + httpTest.RespondWithJson(new + { + result = "created" + }); + + await _rebels.CreateIndexAsync("skywalkers", b => b + .IndexBy(r => r.Surname) + .OrderBy(r => r.Surname) + .ThenBy(r => r.Name), + new IndexOptions() + { + DesignDocument = "skywalkers_ddoc", + Partitioned = true + }); + + + var expectedBody = + "{\"index\":{\"fields\":[\"surname\"],\"sort\":[{\"surname\":\"asc\"},{\"name\":\"asc\"}]},\"name\":\"skywalkers\",\"type\":\"json\",\"ddoc\":\"skywalkers_ddoc\",\"partitioned\":true}"; + httpTest + .ShouldHaveCalled("http://localhost/rebels/_index") + .WithRequestBody(expectedBody) + .WithVerb(HttpMethod.Post); + } + + [Fact] + public async Task CreateIndex_Full() { using var httpTest = new HttpTest(); httpTest.RespondWithJson(new @@ -36,7 +83,8 @@ await _rebels.CreateIndexAsync("skywalkers", b => b .OrderByDescending(r => r.Surname) .ThenByDescending(r => r.Name) .Take(100) - .Skip(1), + .Skip(1) + .ExcludeWhere(r => r.Age <= 3), new IndexOptions() { DesignDocument = "skywalkers_ddoc", @@ -45,7 +93,7 @@ await _rebels.CreateIndexAsync("skywalkers", b => b var expectedBody = - "{\"index\":{\"selector\":{\"$and\":[{\"surname\":\"Skywalker\"},{\"age\":{\"$gt\":3}}]},\"fields\":[\"surname\",\"name\"],\"sort\":[{\"surname\":\"desc\"},{\"name\":\"desc\"}],\"limit\":100,\"skip\":1},\"name\":\"skywalkers\",\"type\":\"json\",\"ddoc\":\"skywalkers_ddoc\",\"partitioned\":true}"; + "{\"index\":{\"selector\":{\"$and\":[{\"surname\":\"Skywalker\"},{\"age\":{\"$gt\":3}}]},\"partial_filter_selector\":{\"age\":{\"$lte\":3}},\"fields\":[\"surname\",\"name\"],\"sort\":[{\"surname\":\"desc\"},{\"name\":\"desc\"}],\"limit\":100,\"skip\":1},\"name\":\"skywalkers\",\"type\":\"json\",\"ddoc\":\"skywalkers_ddoc\",\"partitioned\":true}"; httpTest .ShouldHaveCalled("http://localhost/rebels/_index") .WithRequestBody(expectedBody) From 30a099692ca29cfbf03c2beeb2fe38b4fb6cf65d Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Mon, 14 Sep 2020 14:48:25 +0200 Subject: [PATCH 06/18] #102: Adds documentation. --- README.md | 45 +++++++++++++ src/CouchDB.Driver/Indexes/IIndexBuilder.cs | 34 +++------- .../Indexes/IMultiFieldIndexBuilder.cs | 66 +++++++++++++++++++ .../Indexes/IOrderedDescendingIndexBuilder.cs | 22 +++++++ .../Indexes/IOrderedIndexBuilder.cs | 22 +++++++ src/CouchDB.Driver/Indexes/IndexBuilder.cs | 16 ++--- 6 files changed, 173 insertions(+), 32 deletions(-) create mode 100644 src/CouchDB.Driver/Indexes/IMultiFieldIndexBuilder.cs create mode 100644 src/CouchDB.Driver/Indexes/IOrderedDescendingIndexBuilder.cs create mode 100644 src/CouchDB.Driver/Indexes/IOrderedIndexBuilder.cs diff --git a/README.md b/README.md index 7ae1d5b..0b1ddd6 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,9 @@ The produced Mango JSON: * [DB Changes Feed](#db-changes-feed) * [Feed Options](#feed-options) * [Feed Filter](#feed-filter) +* [Indexing](#indexing) + * [Index Options](#index-options) + * [Partial Indexes](#partial-indexes) * [Local (non-replicating) Documents](#local-(non-replicating)-documents) * [Bookmark and Execution stats](#bookmark-and-execution-stats) * [Users](#users) @@ -427,6 +430,48 @@ var filter = ChangesFeedFilter.View(view); ChangesFeedResponse changes = await GetChangesAsync(options: null, filter); ``` +## Indexing + +It is possible to create indexes to use when querying. + +```csharp +// Basic index creation +await _rebels.CreateIndexAsync( + name: "surnames", + idxBuilder => idxBuilder.IndexBy(r => r.Surname)); + +// Index creation using all available query parameters +await _rebels.CreateIndexAsync("skywalkers", idxBuilder => idxBuilder + .IndexBy(r => r.Surname) + .AlsoBy(r => r.Name) + .Where(r => r.Surname == "Skywalker") + .OrderBy(r => r.Surname) + .ThenBy(r => r.Name) + .Take(5) + .Skip(1)); +``` + +### Index Options + +```csharp +// Specifies the design document and/or whether a JSON index is partitioned or global +await _rebels.CreateIndexAsync("surnames", idxBuilder => idxBuilder + .IndexBy(r => r.Surname), + new IndexOptions() + { + DesignDocument = "surnames_ddoc", + Partitioned = true + }); +``` + +### Partial Indexes +```csharp +// Create an index which excludes documents at index time +await _rebels.CreateIndexAsync("skywalkers", idxBuilder => idxBuilder + .IndexBy(r => r.Name) + .ExcludeWhere(r => r.Surname != "Skywalker"); +``` + ## Local (non-replicating) Documents The Local (non-replicating) document interface allows you to create local documents that are not replicated to other databases. diff --git a/src/CouchDB.Driver/Indexes/IIndexBuilder.cs b/src/CouchDB.Driver/Indexes/IIndexBuilder.cs index ccf19a2..f3a7151 100644 --- a/src/CouchDB.Driver/Indexes/IIndexBuilder.cs +++ b/src/CouchDB.Driver/Indexes/IIndexBuilder.cs @@ -4,33 +4,19 @@ namespace CouchDB.Driver.Indexes { + /// + /// Builder to configure CouchDB indexes. + /// + /// The type of the document. public interface IIndexBuilder where TSource : CouchDocument { + /// + /// Select a field to use in the index. + /// + /// The type of the selected property. + /// Function to select a property. + /// Returns the current instance to chain calls. IMultiFieldIndexBuilder IndexBy(Expression> selector); } - - public interface IMultiFieldIndexBuilder : IIndexBuilder - where TSource : CouchDocument - { - IMultiFieldIndexBuilder AlsoBy(Expression> selector); - IMultiFieldIndexBuilder Where(Expression> selector); - IOrderedIndexBuilder OrderBy(Expression> selector); - IOrderedDescendingIndexBuilder OrderByDescending(Expression> selector); - IMultiFieldIndexBuilder Take(int take); - IMultiFieldIndexBuilder Skip(int skip); - IMultiFieldIndexBuilder ExcludeWhere(Expression> selector); - } - - public interface IOrderedIndexBuilder : IMultiFieldIndexBuilder - where TSource : CouchDocument - { - IOrderedIndexBuilder ThenBy(Expression> selector); - } - - public interface IOrderedDescendingIndexBuilder : IMultiFieldIndexBuilder - where TSource : CouchDocument - { - IOrderedDescendingIndexBuilder ThenByDescending(Expression> selector); - } } \ No newline at end of file diff --git a/src/CouchDB.Driver/Indexes/IMultiFieldIndexBuilder.cs b/src/CouchDB.Driver/Indexes/IMultiFieldIndexBuilder.cs new file mode 100644 index 0000000..a05c308 --- /dev/null +++ b/src/CouchDB.Driver/Indexes/IMultiFieldIndexBuilder.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq.Expressions; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Indexes +{ + /// + /// Builder to configure CouchDB indexes. + /// + /// The type of the document. + public interface IMultiFieldIndexBuilder : IIndexBuilder + where TSource : CouchDocument + { + /// + /// Select an additional field to use in the index. + /// + /// The type of the selected property. + /// Function to select a property. + /// Returns the current instance to chain calls. + IMultiFieldIndexBuilder AlsoBy(Expression> selector); + + /// + /// Filters the documents based on the predicate. + /// + /// Function to filter documents. + /// Returns the current instance to chain calls. + IMultiFieldIndexBuilder Where(Expression> predicate); + + /// + /// Sort the index in ascending order. + /// + /// The type of the selected property. + /// Function to select a property. + /// Returns the current instance to chain calls. + IOrderedIndexBuilder OrderBy(Expression> selector); + + /// + /// Sort the index in descending order. + /// + /// The type of the selected property. + /// Function to select a property. + /// Returns the current instance to chain calls. + IOrderedDescendingIndexBuilder OrderByDescending(Expression> selector); + + /// + /// Limits the documents to index. + /// + /// The number of document to take. + /// Returns the current instance to chain calls. + IMultiFieldIndexBuilder Take(int count); + + /// + /// Bypasses a specific number of documents. + /// + /// The number of document to skip. + /// Returns the current instance to chain calls. + IMultiFieldIndexBuilder Skip(int count); + + /// + /// Creates partial index which excludes documents based on the predicate at index time. + /// + /// Function to filter documents. + /// Returns the current instance to chain calls. + IMultiFieldIndexBuilder ExcludeWhere(Expression> predicate); + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Indexes/IOrderedDescendingIndexBuilder.cs b/src/CouchDB.Driver/Indexes/IOrderedDescendingIndexBuilder.cs new file mode 100644 index 0000000..7ea31e4 --- /dev/null +++ b/src/CouchDB.Driver/Indexes/IOrderedDescendingIndexBuilder.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq.Expressions; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Indexes +{ + /// + /// Builder to configure CouchDB indexes. + /// + /// The type of the document. + public interface IOrderedDescendingIndexBuilder : IMultiFieldIndexBuilder + where TSource : CouchDocument + { + /// + /// Adds a fields for the index sort in descending order. + /// + /// The type of the selected property. + /// Function to select a property. + /// Returns the current instance to chain calls. + IOrderedDescendingIndexBuilder ThenByDescending(Expression> selector); + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Indexes/IOrderedIndexBuilder.cs b/src/CouchDB.Driver/Indexes/IOrderedIndexBuilder.cs new file mode 100644 index 0000000..f32a4a9 --- /dev/null +++ b/src/CouchDB.Driver/Indexes/IOrderedIndexBuilder.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq.Expressions; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Indexes +{ + /// + /// Builder to configure CouchDB indexes. + /// + /// The type of the document. + public interface IOrderedIndexBuilder : IMultiFieldIndexBuilder + where TSource : CouchDocument + { + /// + /// Adds a fields for the index sort in ascending order. + /// + /// The type of the selected property. + /// Function to select a property. + /// Returns the current instance to chain calls. + IOrderedIndexBuilder ThenBy(Expression> selector); + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Indexes/IndexBuilder.cs b/src/CouchDB.Driver/Indexes/IndexBuilder.cs index bdc6015..e22e401 100644 --- a/src/CouchDB.Driver/Indexes/IndexBuilder.cs +++ b/src/CouchDB.Driver/Indexes/IndexBuilder.cs @@ -40,9 +40,9 @@ public IMultiFieldIndexBuilder IndexBy(Expression Where(Expression> selector) + public IMultiFieldIndexBuilder Where(Expression> predicate) { - MethodCallExpression whereExpression = selector.WrapInWhereExpression(); + MethodCallExpression whereExpression = predicate.WrapInWhereExpression(); var jsonSelector = _queryProvider.ToString(whereExpression); _selector = jsonSelector.Substring(1, jsonSelector.Length - 2); return this; @@ -66,21 +66,21 @@ public IOrderedDescendingIndexBuilder OrderByDescending(Expr return this; } - public IMultiFieldIndexBuilder Take(int take) + public IMultiFieldIndexBuilder Take(int count) { - _toTake = take; + _toTake = count; return this; } - public IMultiFieldIndexBuilder Skip(int skip) + public IMultiFieldIndexBuilder Skip(int count) { - _toSkip = skip; + _toSkip = count; return this; } - public IMultiFieldIndexBuilder ExcludeWhere(Expression> selector) + public IMultiFieldIndexBuilder ExcludeWhere(Expression> predicate) { - MethodCallExpression whereExpression = selector.WrapInWhereExpression(); + MethodCallExpression whereExpression = predicate.WrapInWhereExpression(); var jsonSelector = _queryProvider.ToString(whereExpression); _partialSelector = jsonSelector .Substring(1, jsonSelector.Length - 2) From 65d8bc09630796c958a58ac495057640fa4ff6bb Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Mon, 14 Sep 2020 17:18:34 +0200 Subject: [PATCH 07/18] #102: Create index in Context - Part1 --- src/CouchDB.Driver/CouchContext.cs | 87 ++++++++++++------- src/CouchDB.Driver/CouchDB.Driver.csproj | 1 + .../Options/CouchDatabaseBuilder.cs | 28 ++++++ .../Options/CouchDocumentBuilder.cs | 29 +++++++ .../CouchDbContext_Index_Tests.cs | 55 ++++++++++++ 5 files changed, 167 insertions(+), 33 deletions(-) create mode 100644 src/CouchDB.Driver/Options/CouchDatabaseBuilder.cs create mode 100644 src/CouchDB.Driver/Options/CouchDocumentBuilder.cs create mode 100644 tests/CouchDB.Driver.UnitTests/CouchDbContext_Index_Tests.cs diff --git a/src/CouchDB.Driver/CouchContext.cs b/src/CouchDB.Driver/CouchContext.cs index 417131e..f283083 100644 --- a/src/CouchDB.Driver/CouchContext.cs +++ b/src/CouchDB.Driver/CouchContext.cs @@ -1,10 +1,11 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Threading; using System.Threading.Tasks; using CouchDB.Driver.Helpers; using CouchDB.Driver.Options; +using CouchDB.Driver.Types; namespace CouchDB.Driver { @@ -12,14 +13,15 @@ public abstract class CouchContext : IAsyncDisposable { public ICouchClient Client { get; } protected virtual void OnConfiguring(CouchOptionsBuilder optionsBuilder) { } + protected virtual void OnDatabaseCreating(CouchDatabaseBuilder optionsBuilder) { } - private static readonly MethodInfo GetDatabaseGenericMethod - = typeof(CouchClient).GetMethods(BindingFlags.Public | BindingFlags.Instance) - .Single(mi => mi.Name == nameof(CouchClient.GetDatabase) && mi.GetParameters().Length == 0); + private static readonly MethodInfo InitDatabasesGenericMethod + = typeof(CouchContext).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) + .Single(mi => mi.Name == nameof(InitDatabasesAsync)); - private static readonly MethodInfo GetOrCreateDatabaseAsyncGenericMethod - = typeof(CouchClient).GetMethods(BindingFlags.Public | BindingFlags.Instance) - .Single(mi => mi.Name == nameof(CouchClient.GetOrCreateDatabaseAsync) && mi.GetParameters().Length == 3); + private static readonly MethodInfo ApplyDatabaseChangesGenericMethod + = typeof(CouchContext).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) + .Single(mi => mi.Name == nameof(ApplyDatabaseChangesAsync)); protected CouchContext() : this(new CouchOptions()) { } @@ -27,37 +29,28 @@ protected CouchContext(CouchOptions options) { Check.NotNull(options, nameof(options)); - var builder = new CouchOptionsBuilder(options); + var optionsBuilder = new CouchOptionsBuilder(options); + var databaseBuilder = new CouchDatabaseBuilder(); #pragma warning disable CA2214 // Do not call overridable methods in constructors - OnConfiguring(builder); + OnConfiguring(optionsBuilder); + OnDatabaseCreating(databaseBuilder); #pragma warning restore CA2214 // Do not call overridable methods in constructors Client = new CouchClient(options); + IEnumerable databasePropertyInfos = GetDatabaseProperties(); - PropertyInfo[] databasePropertyInfos = GetDatabaseProperties(); - - foreach (PropertyInfo propertyInfo in databasePropertyInfos) + foreach (PropertyInfo dbProperty in databasePropertyInfos) { - Type documentType = propertyInfo.PropertyType.GetGenericArguments()[0]; - object? database; - if (options.CheckDatabaseExists) - { - MethodInfo getOrCreateDatabaseMethod = GetOrCreateDatabaseAsyncGenericMethod.MakeGenericMethod(documentType); -#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. - var parameters = new[] {(object)null, null, default(CancellationToken)}; -#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. - var task = (Task)getOrCreateDatabaseMethod.Invoke(Client, parameters); - task.ConfigureAwait(false).GetAwaiter().GetResult(); - PropertyInfo resultProperty = task.GetType().GetProperty(nameof(Task.Result)); - database = resultProperty.GetValue(task); - } - else - { - MethodInfo getDatabaseMethod = GetDatabaseGenericMethod.MakeGenericMethod(documentType); - database = getDatabaseMethod.Invoke(Client, Array.Empty()); - } - propertyInfo.SetValue(this, database); + Type documentType = dbProperty.PropertyType.GetGenericArguments()[0]; + + var initDatabasesTask = (Task)InitDatabasesGenericMethod.MakeGenericMethod(documentType) + .Invoke(this, new object[] {dbProperty, options}); + initDatabasesTask.ConfigureAwait(false).GetAwaiter(); + + var applyDatabaseChangesTask = (Task)ApplyDatabaseChangesGenericMethod.MakeGenericMethod(documentType) + .Invoke(this, new object[] { dbProperty, databaseBuilder }); + applyDatabaseChangesTask.ConfigureAwait(false).GetAwaiter(); } } @@ -66,11 +59,39 @@ public ValueTask DisposeAsync() return Client.DisposeAsync(); } - private PropertyInfo[] GetDatabaseProperties() => + private async Task InitDatabasesAsync(PropertyInfo propertyInfo, CouchOptions options) + where TSource : CouchDocument + { + ICouchDatabase database = options.CheckDatabaseExists + ? await Client.GetOrCreateDatabaseAsync().ConfigureAwait(false) + : Client.GetDatabase(); + + propertyInfo.SetValue(this, database); + } + + private async Task ApplyDatabaseChangesAsync(PropertyInfo propertyInfo, CouchDatabaseBuilder databaseBuilder) + where TSource: CouchDocument + { + if (!databaseBuilder.DocumentBuilders.ContainsKey(typeof(TSource))) + { + return; + } + + var database = (CouchDatabase)propertyInfo.GetValue(this); + var documentBuilder = (CouchDocumentBuilder)databaseBuilder.DocumentBuilders[typeof(TSource)]; + + await database.CreateIndexAsync( + documentBuilder.Name, + documentBuilder.IndexBuilderAction, + documentBuilder.Options) + .ConfigureAwait(false); + } + + private IEnumerable GetDatabaseProperties() => GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(CouchDatabase<>)) .ToArray(); } -} +} \ No newline at end of file diff --git a/src/CouchDB.Driver/CouchDB.Driver.csproj b/src/CouchDB.Driver/CouchDB.Driver.csproj index c663495..1ab8f96 100644 --- a/src/CouchDB.Driver/CouchDB.Driver.csproj +++ b/src/CouchDB.Driver/CouchDB.Driver.csproj @@ -35,6 +35,7 @@ + diff --git a/src/CouchDB.Driver/Options/CouchDatabaseBuilder.cs b/src/CouchDB.Driver/Options/CouchDatabaseBuilder.cs new file mode 100644 index 0000000..168344e --- /dev/null +++ b/src/CouchDB.Driver/Options/CouchDatabaseBuilder.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Options +{ + public class CouchDatabaseBuilder + { + internal readonly Dictionary DocumentBuilders; + + internal CouchDatabaseBuilder() + { + DocumentBuilders = new Dictionary(); + } + + public CouchDocumentBuilder Document() + where TSource : CouchDocument + { + Type documentType = typeof(TSource); + if (!DocumentBuilders.ContainsKey(documentType)) + { + DocumentBuilders.Add(documentType, new CouchDocumentBuilder()); + } + + return (CouchDocumentBuilder)DocumentBuilders[documentType]; + } + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Options/CouchDocumentBuilder.cs b/src/CouchDB.Driver/Options/CouchDocumentBuilder.cs new file mode 100644 index 0000000..0f955d4 --- /dev/null +++ b/src/CouchDB.Driver/Options/CouchDocumentBuilder.cs @@ -0,0 +1,29 @@ +using System; +using CouchDB.Driver.Indexes; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Options +{ + public class CouchDocumentBuilder + where TSource : CouchDocument + { + internal string Name { get; set; } + internal Action> IndexBuilderAction { get; set; } + internal IndexOptions? Options { get; set; } + + internal CouchDocumentBuilder() + { + Name = string.Empty; + IndexBuilderAction = builder => {}; + } + + public CouchDocumentBuilder HasIndex(string name, Action> indexBuilderAction, + IndexOptions? options = null) + { + Name = name; + IndexBuilderAction = indexBuilderAction; + Options = options; + return this; + } + } +} \ No newline at end of file diff --git a/tests/CouchDB.Driver.UnitTests/CouchDbContext_Index_Tests.cs b/tests/CouchDB.Driver.UnitTests/CouchDbContext_Index_Tests.cs new file mode 100644 index 0000000..5accbca --- /dev/null +++ b/tests/CouchDB.Driver.UnitTests/CouchDbContext_Index_Tests.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; +using CouchDB.Driver.Extensions; +using CouchDB.Driver.Options; +using CouchDB.Driver.UnitTests.Models; +using Flurl.Http.Testing; +using Xunit; + +namespace CouchDB.Driver.UnitTests +{ + public class CouchDbContext_Index_Tests + { + private class MyDeathStarContext : CouchContext + { + public CouchDatabase Rebels { get; set; } + + protected override void OnConfiguring(CouchOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseEndpoint("http://localhost:5984/") + .UseBasicAuthentication("admin", "admin"); + } + + protected override void OnDatabaseCreating(CouchDatabaseBuilder optionsBuilder) + { + optionsBuilder.Document() + .HasIndex("skywalkers", b => b.IndexBy(b => b.Surname)); + } + } + + [Fact] + public async Task Context_Query() + { + using var httpTest = new HttpTest(); + httpTest.RespondWithJson(new + { + Result = "created" + }); + httpTest.RespondWithJson(new + { + docs = new object[] { + new { + Id = "176694", + Rev = "1-54f8e950cc338d2385d9b0cda2fd918e", + Name = "Luke" + } + } + }); + + await using var context = new MyDeathStarContext(); + var result = await context.Rebels.ToListAsync(); + Assert.NotEmpty(result); + Assert.Equal("Luke", result[0].Name); + } + } +} From 5876419095a499d959d140c9656b785c19c4600e Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Mon, 14 Sep 2020 19:59:11 +0200 Subject: [PATCH 08/18] #102: Adds DeleteIndexAsync and GetIndexesAsync --- src/CouchDB.Driver/CouchDatabase.cs | 43 +++++++++++++++++-- src/CouchDB.Driver/DTOs/BulkGetResultItem.cs | 2 +- src/CouchDB.Driver/DTOs/GetIndexesResult.cs | 14 ++++++ src/CouchDB.Driver/ICouchDatabase.cs | 28 +++++++++++- src/CouchDB.Driver/Types/CouchDocumentInfo.cs | 1 - src/CouchDB.Driver/Types/IndexInfo.cs | 24 +++++++++++ 6 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 src/CouchDB.Driver/DTOs/GetIndexesResult.cs create mode 100644 src/CouchDB.Driver/Types/IndexInfo.cs diff --git a/src/CouchDB.Driver/CouchDatabase.cs b/src/CouchDB.Driver/CouchDatabase.cs index d4b7c33..df85381 100644 --- a/src/CouchDB.Driver/CouchDatabase.cs +++ b/src/CouchDB.Driver/CouchDatabase.cs @@ -415,8 +415,23 @@ public async IAsyncEnumerable> GetContinuousC #endregion #region Index + + /// + public async Task> GetIndexesAsync(CancellationToken cancellationToken = default) + { + GetIndexesResult response = await NewRequest() + .AppendPathSegment("_index") + .GetJsonAsync(cancellationToken) + .SendRequestAsync() + .ConfigureAwait(false); + return response.Indexes; + } - public async Task CreateIndexAsync(string name, Action> indexBuilderAction, IndexOptions? options = null) + /// + public async Task CreateIndexAsync(string name, + Action> indexBuilderAction, + IndexOptions? options = null, + CancellationToken cancellationToken = default) { Check.NotNull(name, nameof(name)); Check.NotNull(indexBuilderAction, nameof(indexBuilderAction)); @@ -445,12 +460,34 @@ public async Task CreateIndexAsync(string name, Action> i var request = sb.ToString(); - await NewRequest().AppendPathSegment("_index") - .PostStringAsync(request) + _ = await NewRequest() + .AppendPathSegment("_index") + .PostStringAsync(request, cancellationToken) + .SendRequestAsync() + .ConfigureAwait(false); + } + + /// + public async Task DeleteIndexAsync(string designDocument, string name, CancellationToken cancellationToken = default) + { + Check.NotNull(designDocument, nameof(designDocument)); + Check.NotNull(name, nameof(name)); + + _ = await NewRequest() + .AppendPathSegments("_index", designDocument, "json", name) + .DeleteAsync(cancellationToken) .SendRequestAsync() .ConfigureAwait(false); } + /// + public Task DeleteIndexAsync(IndexInfo indexInfo, CancellationToken cancellationToken = default) + { + Check.NotNull(indexInfo, nameof(indexInfo)); + + return DeleteIndexAsync(indexInfo.DesignDocument, indexInfo.Name, cancellationToken); + } + #endregion #region Utils diff --git a/src/CouchDB.Driver/DTOs/BulkGetResultItem.cs b/src/CouchDB.Driver/DTOs/BulkGetResultItem.cs index 7f50722..af71871 100644 --- a/src/CouchDB.Driver/DTOs/BulkGetResultItem.cs +++ b/src/CouchDB.Driver/DTOs/BulkGetResultItem.cs @@ -11,4 +11,4 @@ internal class BulkGetResultItem where TSource : CouchDocument public TSource Item { get; set; } } } -#nullable enable \ No newline at end of file +#nullable restore \ No newline at end of file diff --git a/src/CouchDB.Driver/DTOs/GetIndexesResult.cs b/src/CouchDB.Driver/DTOs/GetIndexesResult.cs new file mode 100644 index 0000000..bd04b39 --- /dev/null +++ b/src/CouchDB.Driver/DTOs/GetIndexesResult.cs @@ -0,0 +1,14 @@ +#nullable disable +using System.Collections.Generic; +using CouchDB.Driver.Types; +using Newtonsoft.Json; + +namespace CouchDB.Driver.DTOs +{ + internal class GetIndexesResult + { + [JsonProperty("indexes")] + public List Indexes { get; set; } + } +} +#nullable restore \ No newline at end of file diff --git a/src/CouchDB.Driver/ICouchDatabase.cs b/src/CouchDB.Driver/ICouchDatabase.cs index 82970fc..dd1ad1a 100644 --- a/src/CouchDB.Driver/ICouchDatabase.cs +++ b/src/CouchDB.Driver/ICouchDatabase.cs @@ -125,15 +125,41 @@ IAsyncEnumerable> GetContinuousChangesAsync( ChangesFeedOptions options, ChangesFeedFilter filter, CancellationToken cancellationToken); + /// + /// Gets the list of indexes in the database. + /// + /// A to observe while waiting for the task to complete. + /// A Task that represents the asynchronous operation. The task result contains the list of indexes. + Task> GetIndexesAsync(CancellationToken cancellationToken = default); + /// /// Creates an index for the current database with the given configuration. /// /// The name of the index. /// The action to configure the index builder. /// The index options. + /// A to observe while waiting for the task to complete. /// A task that represents the asynchronous operation. Task CreateIndexAsync(string name, Action> indexBuilderAction, - IndexOptions? options = null); + IndexOptions? options = null, + CancellationToken cancellationToken = default); + + /// + /// Deletes a specific index. + /// + /// The design document name + /// The index name + /// A to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. + Task DeleteIndexAsync(string designDocument, string name, CancellationToken cancellationToken = default); + + /// + /// Deletes a specific index. + /// + /// The index info. + /// A to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. + Task DeleteIndexAsync(IndexInfo indexInfo, CancellationToken cancellationToken = default); /// /// Asynchronously downloads a specific attachment. diff --git a/src/CouchDB.Driver/Types/CouchDocumentInfo.cs b/src/CouchDB.Driver/Types/CouchDocumentInfo.cs index 91bfde3..09b3da7 100644 --- a/src/CouchDB.Driver/Types/CouchDocumentInfo.cs +++ b/src/CouchDB.Driver/Types/CouchDocumentInfo.cs @@ -1,5 +1,4 @@ #nullable disable -using System.Collections.Generic; using System.Runtime.Serialization; using Newtonsoft.Json; diff --git a/src/CouchDB.Driver/Types/IndexInfo.cs b/src/CouchDB.Driver/Types/IndexInfo.cs new file mode 100644 index 0000000..1394ea1 --- /dev/null +++ b/src/CouchDB.Driver/Types/IndexInfo.cs @@ -0,0 +1,24 @@ +#nullable disable +using Newtonsoft.Json; + +namespace CouchDB.Driver.Types +{ + /// + /// Represent info about the index. + /// + public class IndexInfo + { + /// + /// ID of the design document the index belongs to. + /// + [JsonProperty("ddoc")] + public string DesignDocument { get; set; } + + /// + /// The name of the index. + /// + [JsonProperty("name")] + public string Name { get; set; } + } +} +#nullable restore \ No newline at end of file From 35aa1ab3c65491870fb8b61a376f36e8b65249ea Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Tue, 15 Sep 2020 11:55:47 +0200 Subject: [PATCH 09/18] #102: Fixes index syntax --- README.md | 38 +++--- src/CouchDB.Driver/CouchContext.cs | 37 ++++-- src/CouchDB.Driver/CouchDatabase.cs | 1 + src/CouchDB.Driver/Indexes/IIndexBuilder.cs | 12 +- .../Indexes/IIndexBuilderBase.cs | 21 ++++ .../Indexes/IMultiFieldIndexBuilder.cs | 66 ---------- .../Indexes/IOrderedDescendingIndexBuilder.cs | 4 +- .../Indexes/IOrderedIndexBuilder.cs | 4 +- src/CouchDB.Driver/Indexes/IndexBuilder.cs | 117 +++--------------- .../Options/CouchDocumentBuilder.cs | 13 +- src/CouchDB.Driver/Options/CouchOptions.cs | 1 + .../Options/CouchOptionsBuilder.cs | 11 ++ .../Options/CouchOptionsBuilder`.cs | 8 ++ src/CouchDB.Driver/Options/IndexDefinition.cs | 21 ++++ src/CouchDB.Driver/Query/QueryTranslator.cs | 1 + tests/CouchDB.Driver.E2ETests/Client_Tests.cs | 17 +++ .../MyDeathStarContext.cs | 8 ++ .../CouchDbContext_Index_Tests.cs | 4 +- tests/CouchDB.Driver.UnitTests/Index_Tests.cs | 23 ++-- 19 files changed, 187 insertions(+), 220 deletions(-) create mode 100644 src/CouchDB.Driver/Indexes/IIndexBuilderBase.cs delete mode 100644 src/CouchDB.Driver/Indexes/IMultiFieldIndexBuilder.cs create mode 100644 src/CouchDB.Driver/Options/IndexDefinition.cs diff --git a/README.md b/README.md index 0b1ddd6..7ce7eba 100644 --- a/README.md +++ b/README.md @@ -436,26 +436,21 @@ It is possible to create indexes to use when querying. ```csharp // Basic index creation -await _rebels.CreateIndexAsync( - name: "surnames", - idxBuilder => idxBuilder.IndexBy(r => r.Surname)); - -// Index creation using all available query parameters -await _rebels.CreateIndexAsync("skywalkers", idxBuilder => idxBuilder - .IndexBy(r => r.Surname) - .AlsoBy(r => r.Name) - .Where(r => r.Surname == "Skywalker") - .OrderBy(r => r.Surname) - .ThenBy(r => r.Name) - .Take(5) - .Skip(1)); +await _rebels.CreateIndexAsync("rebels_index", b => b + .IndexBy(r => r.Surname)) + .ThenBy(r => r.Name)); + +// Descending index creation +await _rebels.CreateIndexAsync("rebels_index", b => b + .IndexByDescending(r => r.Surname)) + .ThenByDescending(r => r.Name)); ``` ### Index Options ```csharp // Specifies the design document and/or whether a JSON index is partitioned or global -await _rebels.CreateIndexAsync("surnames", idxBuilder => idxBuilder +await _rebels.CreateIndexAsync("rebels_index", b => b .IndexBy(r => r.Surname), new IndexOptions() { @@ -467,9 +462,20 @@ await _rebels.CreateIndexAsync("surnames", idxBuilder => idxBuilder ### Partial Indexes ```csharp // Create an index which excludes documents at index time -await _rebels.CreateIndexAsync("skywalkers", idxBuilder => idxBuilder +await _rebels.CreateIndexAsync("skywalkers_index", b => b .IndexBy(r => r.Name) - .ExcludeWhere(r => r.Surname != "Skywalker"); + .Where(r => r.Surname == "Skywalker"); +``` + +### Indexes operations +```csharp +// Get the list of indexes +var indexes = await _rebels.GetIndexesAsync(); + +// Delete an indexes +await _rebels.DeleteIndexAsync(indexes[0]); +// or +await _rebels.DeleteIndexAsync("surnames_ddoc", name: "surnames"); ``` ## Local (non-replicating) Documents diff --git a/src/CouchDB.Driver/CouchContext.cs b/src/CouchDB.Driver/CouchContext.cs index f283083..d1966f2 100644 --- a/src/CouchDB.Driver/CouchContext.cs +++ b/src/CouchDB.Driver/CouchContext.cs @@ -13,7 +13,7 @@ public abstract class CouchContext : IAsyncDisposable { public ICouchClient Client { get; } protected virtual void OnConfiguring(CouchOptionsBuilder optionsBuilder) { } - protected virtual void OnDatabaseCreating(CouchDatabaseBuilder optionsBuilder) { } + protected virtual void OnDatabaseCreating(CouchDatabaseBuilder databaseBuilder) { } private static readonly MethodInfo InitDatabasesGenericMethod = typeof(CouchContext).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) @@ -46,11 +46,11 @@ protected CouchContext(CouchOptions options) var initDatabasesTask = (Task)InitDatabasesGenericMethod.MakeGenericMethod(documentType) .Invoke(this, new object[] {dbProperty, options}); - initDatabasesTask.ConfigureAwait(false).GetAwaiter(); + initDatabasesTask.ConfigureAwait(false).GetAwaiter().GetResult(); var applyDatabaseChangesTask = (Task)ApplyDatabaseChangesGenericMethod.MakeGenericMethod(documentType) - .Invoke(this, new object[] { dbProperty, databaseBuilder }); - applyDatabaseChangesTask.ConfigureAwait(false).GetAwaiter(); + .Invoke(this, new object[] { dbProperty, options, databaseBuilder }); + applyDatabaseChangesTask.ConfigureAwait(false).GetAwaiter().GetResult(); } } @@ -69,7 +69,7 @@ private async Task InitDatabasesAsync(PropertyInfo propertyInfo, CouchO propertyInfo.SetValue(this, database); } - private async Task ApplyDatabaseChangesAsync(PropertyInfo propertyInfo, CouchDatabaseBuilder databaseBuilder) + private async Task ApplyDatabaseChangesAsync(PropertyInfo propertyInfo, CouchOptions options, CouchDatabaseBuilder databaseBuilder) where TSource: CouchDocument { if (!databaseBuilder.DocumentBuilders.ContainsKey(typeof(TSource))) @@ -80,11 +80,28 @@ private async Task ApplyDatabaseChangesAsync(PropertyInfo propertyInfo, var database = (CouchDatabase)propertyInfo.GetValue(this); var documentBuilder = (CouchDocumentBuilder)databaseBuilder.DocumentBuilders[typeof(TSource)]; - await database.CreateIndexAsync( - documentBuilder.Name, - documentBuilder.IndexBuilderAction, - documentBuilder.Options) - .ConfigureAwait(false); + List indexes = await database.GetIndexesAsync().ConfigureAwait(false); + + foreach (IndexDefinition indexDefinition in documentBuilder.IndexDefinitions) + { + // Delete the index if it already exists + if (options.OverrideExistingIndexes && indexDefinition.Options?.DesignDocument != null) + { + IndexInfo existingIndex = indexes.FirstOrDefault(i => + i.DesignDocument.Equals(indexDefinition.Options.DesignDocument, StringComparison.InvariantCultureIgnoreCase)); + + if (existingIndex != null) + { + await database.DeleteIndexAsync(existingIndex).ConfigureAwait(false); + } + } + + await database.CreateIndexAsync( + indexDefinition.Name, + indexDefinition.IndexBuilderAction, + indexDefinition.Options) + .ConfigureAwait(false); + } } private IEnumerable GetDatabaseProperties() => diff --git a/src/CouchDB.Driver/CouchDatabase.cs b/src/CouchDB.Driver/CouchDatabase.cs index df85381..28a05f3 100644 --- a/src/CouchDB.Driver/CouchDatabase.cs +++ b/src/CouchDB.Driver/CouchDatabase.cs @@ -461,6 +461,7 @@ public async Task CreateIndexAsync(string name, var request = sb.ToString(); _ = await NewRequest() + .WithHeader("Content-Type", "application/json") .AppendPathSegment("_index") .PostStringAsync(request, cancellationToken) .SendRequestAsync() diff --git a/src/CouchDB.Driver/Indexes/IIndexBuilder.cs b/src/CouchDB.Driver/Indexes/IIndexBuilder.cs index f3a7151..c88f565 100644 --- a/src/CouchDB.Driver/Indexes/IIndexBuilder.cs +++ b/src/CouchDB.Driver/Indexes/IIndexBuilder.cs @@ -12,11 +12,19 @@ public interface IIndexBuilder where TSource : CouchDocument { /// - /// Select a field to use in the index. + /// Select a field for the index sort in ascending order. /// /// The type of the selected property. /// Function to select a property. /// Returns the current instance to chain calls. - IMultiFieldIndexBuilder IndexBy(Expression> selector); + IOrderedIndexBuilder IndexBy(Expression> selector); + + /// + /// Select a field for the index sort in descending order. + /// + /// The type of the selected property. + /// Function to select a property. + /// Returns the current instance to chain calls. + IOrderedDescendingIndexBuilder IndexByDescending(Expression> selector); } } \ No newline at end of file diff --git a/src/CouchDB.Driver/Indexes/IIndexBuilderBase.cs b/src/CouchDB.Driver/Indexes/IIndexBuilderBase.cs new file mode 100644 index 0000000..770ecb4 --- /dev/null +++ b/src/CouchDB.Driver/Indexes/IIndexBuilderBase.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq.Expressions; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Indexes +{ + /// + /// Builder to configure CouchDB indexes. + /// + /// The type of the document. + public interface IIndexBuilderBase + where TSource : CouchDocument + { + /// + /// Creates a partial index which excludes documents based on the predicate at index time. + /// + /// Function to filter documents. + /// Returns the current instance to chain calls. + void Where(Expression> predicate); + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Indexes/IMultiFieldIndexBuilder.cs b/src/CouchDB.Driver/Indexes/IMultiFieldIndexBuilder.cs deleted file mode 100644 index a05c308..0000000 --- a/src/CouchDB.Driver/Indexes/IMultiFieldIndexBuilder.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Linq.Expressions; -using CouchDB.Driver.Types; - -namespace CouchDB.Driver.Indexes -{ - /// - /// Builder to configure CouchDB indexes. - /// - /// The type of the document. - public interface IMultiFieldIndexBuilder : IIndexBuilder - where TSource : CouchDocument - { - /// - /// Select an additional field to use in the index. - /// - /// The type of the selected property. - /// Function to select a property. - /// Returns the current instance to chain calls. - IMultiFieldIndexBuilder AlsoBy(Expression> selector); - - /// - /// Filters the documents based on the predicate. - /// - /// Function to filter documents. - /// Returns the current instance to chain calls. - IMultiFieldIndexBuilder Where(Expression> predicate); - - /// - /// Sort the index in ascending order. - /// - /// The type of the selected property. - /// Function to select a property. - /// Returns the current instance to chain calls. - IOrderedIndexBuilder OrderBy(Expression> selector); - - /// - /// Sort the index in descending order. - /// - /// The type of the selected property. - /// Function to select a property. - /// Returns the current instance to chain calls. - IOrderedDescendingIndexBuilder OrderByDescending(Expression> selector); - - /// - /// Limits the documents to index. - /// - /// The number of document to take. - /// Returns the current instance to chain calls. - IMultiFieldIndexBuilder Take(int count); - - /// - /// Bypasses a specific number of documents. - /// - /// The number of document to skip. - /// Returns the current instance to chain calls. - IMultiFieldIndexBuilder Skip(int count); - - /// - /// Creates partial index which excludes documents based on the predicate at index time. - /// - /// Function to filter documents. - /// Returns the current instance to chain calls. - IMultiFieldIndexBuilder ExcludeWhere(Expression> predicate); - } -} \ No newline at end of file diff --git a/src/CouchDB.Driver/Indexes/IOrderedDescendingIndexBuilder.cs b/src/CouchDB.Driver/Indexes/IOrderedDescendingIndexBuilder.cs index 7ea31e4..7a58e8f 100644 --- a/src/CouchDB.Driver/Indexes/IOrderedDescendingIndexBuilder.cs +++ b/src/CouchDB.Driver/Indexes/IOrderedDescendingIndexBuilder.cs @@ -8,11 +8,11 @@ namespace CouchDB.Driver.Indexes /// Builder to configure CouchDB indexes. /// /// The type of the document. - public interface IOrderedDescendingIndexBuilder : IMultiFieldIndexBuilder + public interface IOrderedDescendingIndexBuilder : IIndexBuilderBase where TSource : CouchDocument { /// - /// Adds a fields for the index sort in descending order. + /// Adds a field for the index sort in descending order. /// /// The type of the selected property. /// Function to select a property. diff --git a/src/CouchDB.Driver/Indexes/IOrderedIndexBuilder.cs b/src/CouchDB.Driver/Indexes/IOrderedIndexBuilder.cs index f32a4a9..efd41e7 100644 --- a/src/CouchDB.Driver/Indexes/IOrderedIndexBuilder.cs +++ b/src/CouchDB.Driver/Indexes/IOrderedIndexBuilder.cs @@ -8,11 +8,11 @@ namespace CouchDB.Driver.Indexes /// Builder to configure CouchDB indexes. /// /// The type of the document. - public interface IOrderedIndexBuilder : IMultiFieldIndexBuilder + public interface IOrderedIndexBuilder : IIndexBuilderBase where TSource : CouchDocument { /// - /// Adds a fields for the index sort in ascending order. + /// Adds a field for the index sort in ascending order. /// /// The type of the selected property. /// Function to select a property. diff --git a/src/CouchDB.Driver/Indexes/IndexBuilder.cs b/src/CouchDB.Driver/Indexes/IndexBuilder.cs index e22e401..58e6097 100644 --- a/src/CouchDB.Driver/Indexes/IndexBuilder.cs +++ b/src/CouchDB.Driver/Indexes/IndexBuilder.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Text; using CouchDB.Driver.Extensions; @@ -10,7 +9,7 @@ namespace CouchDB.Driver.Indexes { - internal class IndexBuilder: IOrderedIndexBuilder, IOrderedDescendingIndexBuilder + internal class IndexBuilder: IIndexBuilder, IOrderedIndexBuilder, IOrderedDescendingIndexBuilder where TSource : CouchDocument { private readonly CouchOptions _options; @@ -18,10 +17,6 @@ internal class IndexBuilder: IOrderedIndexBuilder, IOrderedDes private bool _ascending = true; private readonly List _fields; - private readonly List _fieldsOrder; - private int? _toTake; - private int? _toSkip; - private string? _selector; private string? _partialSelector; public IndexBuilder(CouchOptions options, IAsyncQueryProvider queryProvider) @@ -29,84 +24,44 @@ public IndexBuilder(CouchOptions options, IAsyncQueryProvider queryProvider) _options = options; _queryProvider = queryProvider; _fields = new List(); - _fieldsOrder = new List(); } - public IMultiFieldIndexBuilder IndexBy(Expression> selector) + public IOrderedIndexBuilder IndexBy(Expression> selector) { - var m = selector.ToMemberExpression(); - _fields.Clear(); - _fields.Add(m.GetPropertyName(_options)); + AddField(selector); return this; } - public IMultiFieldIndexBuilder Where(Expression> predicate) + public IOrderedDescendingIndexBuilder IndexByDescending(Expression> selector) { - MethodCallExpression whereExpression = predicate.WrapInWhereExpression(); - var jsonSelector = _queryProvider.ToString(whereExpression); - _selector = jsonSelector.Substring(1, jsonSelector.Length - 2); - return this; - } - - public IOrderedIndexBuilder OrderBy(Expression> selector) - { - var m = selector.ToMemberExpression(); - _ascending = true; - _fieldsOrder.Clear(); - _fieldsOrder.Add(m.GetPropertyName(_options)); - return this; - } - - public IOrderedDescendingIndexBuilder OrderByDescending(Expression> selector) - { - var m = selector.ToMemberExpression(); _ascending = false; - _fieldsOrder.Clear(); - _fieldsOrder.Add(m.GetPropertyName(_options)); + AddField(selector); return this; } - public IMultiFieldIndexBuilder Take(int count) + public IOrderedIndexBuilder ThenBy(Expression> selector) { - _toTake = count; - return this; + return IndexBy(selector); } - public IMultiFieldIndexBuilder Skip(int count) + public IOrderedDescendingIndexBuilder ThenByDescending(Expression> selector) { - _toSkip = count; - return this; + return IndexByDescending(selector); } - - public IMultiFieldIndexBuilder ExcludeWhere(Expression> predicate) + + public void Where(Expression> predicate) { MethodCallExpression whereExpression = predicate.WrapInWhereExpression(); var jsonSelector = _queryProvider.ToString(whereExpression); _partialSelector = jsonSelector .Substring(1, jsonSelector.Length - 2) .Replace("selector", "partial_filter_selector", StringComparison.CurrentCultureIgnoreCase); - return this; - } - - public IMultiFieldIndexBuilder AlsoBy(Expression> selector) - { - var m = selector.ToMemberExpression(); - _fields.Add(m.GetPropertyName(_options)); - return this; - } - - IOrderedIndexBuilder IOrderedIndexBuilder.ThenBy(Expression> selector) - { - var m = selector.ToMemberExpression(); - _fieldsOrder.Add(m.GetPropertyName(_options)); - return this; } - IOrderedDescendingIndexBuilder IOrderedDescendingIndexBuilder.ThenByDescending(Expression> selector) + private void AddField(Expression> selector) { - var m = selector.ToMemberExpression(); - _fieldsOrder.Add(m.GetPropertyName(_options)); - return this; + var memberExpression = selector.ToMemberExpression(); + _fields.Add(memberExpression.GetPropertyName(_options)); } public override string ToString() @@ -115,13 +70,6 @@ public override string ToString() sb.Append("{"); - // Selector - if (_selector != null) - { - sb.Append(_selector); - sb.Append(","); - } - // Partial Selector if (_partialSelector != null) { @@ -131,43 +79,18 @@ public override string ToString() // Fields sb.Append("\"fields\":["); - foreach (var field in _fields) - { - sb.Append($"\"{field}\","); - } - sb.Length--; - sb.Append("],"); - - // Sort - if (_fieldsOrder.Any()) - { - sb.Append("\"sort\":["); - var order = _ascending ? "asc" : "desc"; - - foreach (var field in _fieldsOrder) - { - sb.Append($"{{\"{field}\":\"{order}\"}},"); - } - - sb.Length--; - sb.Append("],"); - } - - // Limit - if (_toTake != null) + foreach (var field in _fields) { - sb.Append($"\"limit\":{_toTake},"); - } + var fieldString = _ascending + ? $"\"{field}\"," + : $"{{\"{field}\":\"desc\"}},"; - // Skip - if (_toSkip != null) - { - sb.Append($"\"skip\":{_toSkip},"); + sb.Append(fieldString); } sb.Length--; - sb.Append("}"); + sb.Append("]}"); return sb.ToString(); } diff --git a/src/CouchDB.Driver/Options/CouchDocumentBuilder.cs b/src/CouchDB.Driver/Options/CouchDocumentBuilder.cs index 0f955d4..798f158 100644 --- a/src/CouchDB.Driver/Options/CouchDocumentBuilder.cs +++ b/src/CouchDB.Driver/Options/CouchDocumentBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using CouchDB.Driver.Indexes; using CouchDB.Driver.Types; @@ -7,22 +8,18 @@ namespace CouchDB.Driver.Options public class CouchDocumentBuilder where TSource : CouchDocument { - internal string Name { get; set; } - internal Action> IndexBuilderAction { get; set; } - internal IndexOptions? Options { get; set; } + internal List> IndexDefinitions { get; } internal CouchDocumentBuilder() { - Name = string.Empty; - IndexBuilderAction = builder => {}; + IndexDefinitions = new List>(); } public CouchDocumentBuilder HasIndex(string name, Action> indexBuilderAction, IndexOptions? options = null) { - Name = name; - IndexBuilderAction = indexBuilderAction; - Options = options; + var indexDefinition = new IndexDefinition(name, indexBuilderAction, options); + IndexDefinitions.Add(indexDefinition); return this; } } diff --git a/src/CouchDB.Driver/Options/CouchOptions.cs b/src/CouchDB.Driver/Options/CouchOptions.cs index d777643..8415c31 100644 --- a/src/CouchDB.Driver/Options/CouchOptions.cs +++ b/src/CouchDB.Driver/Options/CouchOptions.cs @@ -15,6 +15,7 @@ public abstract class CouchOptions internal Uri? Endpoint { get; set; } internal bool CheckDatabaseExists { get; set; } + internal bool OverrideExistingIndexes { get; set; } internal AuthenticationType AuthenticationType { get; set; } internal string? Username { get; set; } diff --git a/src/CouchDB.Driver/Options/CouchOptionsBuilder.cs b/src/CouchDB.Driver/Options/CouchOptionsBuilder.cs index b591ec3..e5bb18c 100644 --- a/src/CouchDB.Driver/Options/CouchOptionsBuilder.cs +++ b/src/CouchDB.Driver/Options/CouchOptionsBuilder.cs @@ -66,6 +66,17 @@ public virtual CouchOptionsBuilder EnsureDatabaseExists() return this; } + /// + /// If in a , overrides indexes with same design document and name. + /// Ignore in the . + /// + /// Return the current instance to chain calls. + public virtual CouchOptionsBuilder OverrideExistingIndexes() + { + Options.OverrideExistingIndexes = true; + return this; + } + /// /// Enables basic authentication. /// Basic authentication (RFC 2617) is a quick and simple way to authenticate with CouchDB. The main drawback is the need to send user credentials with each request which may be insecure and could hurt operation performance (since CouchDB must compute the password hash with every request). diff --git a/src/CouchDB.Driver/Options/CouchOptionsBuilder`.cs b/src/CouchDB.Driver/Options/CouchOptionsBuilder`.cs index 5f2cbec..1d3bef0 100644 --- a/src/CouchDB.Driver/Options/CouchOptionsBuilder`.cs +++ b/src/CouchDB.Driver/Options/CouchOptionsBuilder`.cs @@ -52,6 +52,14 @@ public CouchOptionsBuilder(CouchOptions options) : base(options) { } public new virtual CouchOptionsBuilder EnsureDatabaseExists() => (CouchOptionsBuilder)base.EnsureDatabaseExists(); + /// + /// If in a , overrides indexes with same design document and name. + /// Ignore in the . + /// + /// Return the current instance to chain calls. + public new virtual CouchOptionsBuilder OverrideExistingIndexes() + => (CouchOptionsBuilder)base.OverrideExistingIndexes(); + /// /// Enables basic authentication. /// Basic authentication (RFC 2617) is a quick and simple way to authenticate with CouchDB. The main drawback is the need to send user credentials with each request which may be insecure and could hurt operation performance (since CouchDB must compute the password hash with every request). diff --git a/src/CouchDB.Driver/Options/IndexDefinition.cs b/src/CouchDB.Driver/Options/IndexDefinition.cs new file mode 100644 index 0000000..a2ecaf5 --- /dev/null +++ b/src/CouchDB.Driver/Options/IndexDefinition.cs @@ -0,0 +1,21 @@ +using System; +using CouchDB.Driver.Indexes; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Options +{ + internal class IndexDefinition + where TSource : CouchDocument + { + public IndexDefinition(string name, Action> indexBuilderAction, IndexOptions? options) + { + Name = name; + IndexBuilderAction = indexBuilderAction; + Options = options; + } + + public string Name { get; } + public Action> IndexBuilderAction { get; } + public IndexOptions? Options { get; } + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Query/QueryTranslator.cs b/src/CouchDB.Driver/Query/QueryTranslator.cs index 1ee355f..d4794b2 100644 --- a/src/CouchDB.Driver/Query/QueryTranslator.cs +++ b/src/CouchDB.Driver/Query/QueryTranslator.cs @@ -18,6 +18,7 @@ internal QueryTranslator(CouchOptions options) public string Translate(Expression e) { + _isSelectorSet = false; _sb.Clear(); _sb.Append("{"); Visit(e); diff --git a/tests/CouchDB.Driver.E2ETests/Client_Tests.cs b/tests/CouchDB.Driver.E2ETests/Client_Tests.cs index a40090c..16279e9 100644 --- a/tests/CouchDB.Driver.E2ETests/Client_Tests.cs +++ b/tests/CouchDB.Driver.E2ETests/Client_Tests.cs @@ -9,6 +9,7 @@ using CouchDB.Driver.E2ETests._Models; using CouchDB.Driver.Extensions; using CouchDB.Driver.Local; +using CouchDB.Driver.Query.Extensions; using Xunit; namespace CouchDB.Driver.E2E @@ -102,6 +103,22 @@ public async Task Crud_Context() Assert.NotEmpty(result); } + [Fact] + public async Task Crud_Index_Context() + { + await using var context = new MyDeathStarContext(); + await context.Rebels.AddAsync(new Rebel { Name = "Han", Age = 30, Surname = "Solo" }); + await context.Rebels.AddAsync(new Rebel { Name = "Leia", Age = 19, Surname = "Skywalker" }); + await context.Rebels.AddAsync(new Rebel { Name = "Luke", Age = 19, Surname = "Skywalker" }); + + var rebels = await context.Rebels + .OrderBy(r => r.Surname) + .ThenBy(r => r.Name) + .ToListAsync(); + + Assert.NotEmpty(rebels); + } + [Fact] public async Task Crud_SpecialCharacters() { diff --git a/tests/CouchDB.Driver.E2ETests/MyDeathStarContext.cs b/tests/CouchDB.Driver.E2ETests/MyDeathStarContext.cs index 4ba363b..b00f6a1 100644 --- a/tests/CouchDB.Driver.E2ETests/MyDeathStarContext.cs +++ b/tests/CouchDB.Driver.E2ETests/MyDeathStarContext.cs @@ -14,5 +14,13 @@ protected override void OnConfiguring(CouchOptionsBuilder optionsBuilder) .EnsureDatabaseExists() .UseBasicAuthentication(username: "admin", password: "admin"); } + + protected override void OnDatabaseCreating(CouchDatabaseBuilder databaseBuilder) + { + databaseBuilder.Document() + .HasIndex("surnames_index", builder => builder + .IndexBy(r => r.Surname) + .ThenBy(r => r.Name)); + } } } diff --git a/tests/CouchDB.Driver.UnitTests/CouchDbContext_Index_Tests.cs b/tests/CouchDB.Driver.UnitTests/CouchDbContext_Index_Tests.cs index 5accbca..be7de52 100644 --- a/tests/CouchDB.Driver.UnitTests/CouchDbContext_Index_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/CouchDbContext_Index_Tests.cs @@ -20,9 +20,9 @@ protected override void OnConfiguring(CouchOptionsBuilder optionsBuilder) .UseBasicAuthentication("admin", "admin"); } - protected override void OnDatabaseCreating(CouchDatabaseBuilder optionsBuilder) + protected override void OnDatabaseCreating(CouchDatabaseBuilder databaseBuilder) { - optionsBuilder.Document() + databaseBuilder.Document() .HasIndex("skywalkers", b => b.IndexBy(b => b.Surname)); } } diff --git a/tests/CouchDB.Driver.UnitTests/Index_Tests.cs b/tests/CouchDB.Driver.UnitTests/Index_Tests.cs index 59862ce..4267444 100644 --- a/tests/CouchDB.Driver.UnitTests/Index_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Index_Tests.cs @@ -19,7 +19,7 @@ public Index_Tests() } [Fact] - public async Task CreateIndex_Basic() + public async Task CreateIndex() { using var httpTest = new HttpTest(); httpTest.RespondWithJson(new @@ -40,7 +40,7 @@ await _rebels.CreateIndexAsync("skywalkers", b => b } [Fact] - public async Task CreateIndex_Simple_WithOptions() + public async Task CreateIndex_WithOptions() { using var httpTest = new HttpTest(); httpTest.RespondWithJson(new @@ -49,9 +49,8 @@ public async Task CreateIndex_Simple_WithOptions() }); await _rebels.CreateIndexAsync("skywalkers", b => b - .IndexBy(r => r.Surname) - .OrderBy(r => r.Surname) - .ThenBy(r => r.Name), + .IndexByDescending(r => r.Surname) + .ThenByDescending(r => r.Name), new IndexOptions() { DesignDocument = "skywalkers_ddoc", @@ -60,7 +59,7 @@ await _rebels.CreateIndexAsync("skywalkers", b => b var expectedBody = - "{\"index\":{\"fields\":[\"surname\"],\"sort\":[{\"surname\":\"asc\"},{\"name\":\"asc\"}]},\"name\":\"skywalkers\",\"type\":\"json\",\"ddoc\":\"skywalkers_ddoc\",\"partitioned\":true}"; + "{\"index\":{\"fields\":[{\"surname\":\"desc\"},{\"name\":\"desc\"}]},\"name\":\"skywalkers\",\"type\":\"json\",\"ddoc\":\"skywalkers_ddoc\",\"partitioned\":true}"; httpTest .ShouldHaveCalled("http://localhost/rebels/_index") .WithRequestBody(expectedBody) @@ -68,7 +67,7 @@ await _rebels.CreateIndexAsync("skywalkers", b => b } [Fact] - public async Task CreateIndex_Full() + public async Task CreateIndex_Partial() { using var httpTest = new HttpTest(); httpTest.RespondWithJson(new @@ -78,13 +77,7 @@ public async Task CreateIndex_Full() await _rebels.CreateIndexAsync("skywalkers", b => b .IndexBy(r => r.Surname) - .AlsoBy(r => r.Name) - .Where(r => r.Surname == "Skywalker" && r.Age > 3) - .OrderByDescending(r => r.Surname) - .ThenByDescending(r => r.Name) - .Take(100) - .Skip(1) - .ExcludeWhere(r => r.Age <= 3), + .Where(r => r.Surname == "Skywalker"), new IndexOptions() { DesignDocument = "skywalkers_ddoc", @@ -93,7 +86,7 @@ await _rebels.CreateIndexAsync("skywalkers", b => b var expectedBody = - "{\"index\":{\"selector\":{\"$and\":[{\"surname\":\"Skywalker\"},{\"age\":{\"$gt\":3}}]},\"partial_filter_selector\":{\"age\":{\"$lte\":3}},\"fields\":[\"surname\",\"name\"],\"sort\":[{\"surname\":\"desc\"},{\"name\":\"desc\"}],\"limit\":100,\"skip\":1},\"name\":\"skywalkers\",\"type\":\"json\",\"ddoc\":\"skywalkers_ddoc\",\"partitioned\":true}"; + "{\"index\":{\"partial_filter_selector\":{\"surname\":\"Skywalker\"},\"fields\":[\"surname\"]},\"name\":\"skywalkers\",\"type\":\"json\",\"ddoc\":\"skywalkers_ddoc\",\"partitioned\":true}"; httpTest .ShouldHaveCalled("http://localhost/rebels/_index") .WithRequestBody(expectedBody) From b220de284499bff62496c45c31107c74e8c27473 Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Tue, 15 Sep 2020 15:17:51 +0200 Subject: [PATCH 10/18] Impreves get index --- src/CouchDB.Driver/Types/IndexInfo.cs | 46 +++++++++++++++++++ tests/CouchDB.Driver.E2ETests/Client_Tests.cs | 5 ++ 2 files changed, 51 insertions(+) diff --git a/src/CouchDB.Driver/Types/IndexInfo.cs b/src/CouchDB.Driver/Types/IndexInfo.cs index 1394ea1..0ddd8c4 100644 --- a/src/CouchDB.Driver/Types/IndexInfo.cs +++ b/src/CouchDB.Driver/Types/IndexInfo.cs @@ -1,4 +1,6 @@ #nullable disable +using System.Collections.Generic; +using System.Linq; using Newtonsoft.Json; namespace CouchDB.Driver.Types @@ -8,6 +10,11 @@ namespace CouchDB.Driver.Types /// public class IndexInfo { + public IndexInfo() + { + Fields = new Dictionary(); + } + /// /// ID of the design document the index belongs to. /// @@ -19,6 +26,45 @@ public class IndexInfo /// [JsonProperty("name")] public string Name { get; set; } + + /// + /// The fields used in the index + /// + [JsonIgnore] + public Dictionary Fields { get; } + + [JsonProperty("def")] + internal IndexDefinitionInfo Definition + { + set + { + Fields.Clear(); + + foreach (Dictionary definitions in value.Fields) + { + var (name, direction) = definitions.First(); + IndexFieldDirection fieldDirection = direction == "asc" + ? IndexFieldDirection.Ascending + : IndexFieldDirection.Descending; + Fields.Add(name, fieldDirection); + } + } + } + } + + internal class IndexDefinitionInfo + { + [JsonProperty("fields")] + public Dictionary[] Fields { get; set; } + } + + /// + /// Represent the direction of the index. + /// + public enum IndexFieldDirection + { + Ascending = 0, + Descending = 1, } } #nullable restore \ No newline at end of file diff --git a/tests/CouchDB.Driver.E2ETests/Client_Tests.cs b/tests/CouchDB.Driver.E2ETests/Client_Tests.cs index 16279e9..dce7338 100644 --- a/tests/CouchDB.Driver.E2ETests/Client_Tests.cs +++ b/tests/CouchDB.Driver.E2ETests/Client_Tests.cs @@ -107,6 +107,11 @@ public async Task Crud_Context() public async Task Crud_Index_Context() { await using var context = new MyDeathStarContext(); + + var indexes = await context.Rebels.GetIndexesAsync(); + + Assert.Equal(2, indexes.Count); + await context.Rebels.AddAsync(new Rebel { Name = "Han", Age = 30, Surname = "Solo" }); await context.Rebels.AddAsync(new Rebel { Name = "Leia", Age = 19, Surname = "Skywalker" }); await context.Rebels.AddAsync(new Rebel { Name = "Luke", Age = 19, Surname = "Skywalker" }); From 3a5bf8f2e1548273f697cff4d24fdbcc2261348f Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Tue, 15 Sep 2020 15:26:01 +0200 Subject: [PATCH 11/18] Impreved getIndexes --- src/CouchDB.Driver/CouchDatabase.cs | 7 +++++-- src/CouchDB.Driver/DTOs/CreateIndexResult.cs | 18 ++++++++++++++++++ src/CouchDB.Driver/DTOs/IndexDefinitionInfo.cs | 13 +++++++++++++ src/CouchDB.Driver/ICouchDatabase.cs | 8 ++++---- .../Types/IndexFieldDirection.cs | 11 +++++++++++ src/CouchDB.Driver/Types/IndexInfo.cs | 16 +--------------- 6 files changed, 52 insertions(+), 21 deletions(-) create mode 100644 src/CouchDB.Driver/DTOs/CreateIndexResult.cs create mode 100644 src/CouchDB.Driver/DTOs/IndexDefinitionInfo.cs create mode 100644 src/CouchDB.Driver/Types/IndexFieldDirection.cs diff --git a/src/CouchDB.Driver/CouchDatabase.cs b/src/CouchDB.Driver/CouchDatabase.cs index 28a05f3..97566e1 100644 --- a/src/CouchDB.Driver/CouchDatabase.cs +++ b/src/CouchDB.Driver/CouchDatabase.cs @@ -428,7 +428,7 @@ public async Task> GetIndexesAsync(CancellationToken cancellatio } /// - public async Task CreateIndexAsync(string name, + public async Task CreateIndexAsync(string name, Action> indexBuilderAction, IndexOptions? options = null, CancellationToken cancellationToken = default) @@ -460,12 +460,15 @@ public async Task CreateIndexAsync(string name, var request = sb.ToString(); - _ = await NewRequest() + CreateIndexResult result = await NewRequest() .WithHeader("Content-Type", "application/json") .AppendPathSegment("_index") .PostStringAsync(request, cancellationToken) + .ReceiveJson() .SendRequestAsync() .ConfigureAwait(false); + + return result.Id; } /// diff --git a/src/CouchDB.Driver/DTOs/CreateIndexResult.cs b/src/CouchDB.Driver/DTOs/CreateIndexResult.cs new file mode 100644 index 0000000..ba014b5 --- /dev/null +++ b/src/CouchDB.Driver/DTOs/CreateIndexResult.cs @@ -0,0 +1,18 @@ +#nullable disable +using Newtonsoft.Json; + +namespace CouchDB.Driver.DTOs +{ + internal class CreateIndexResult + { + [JsonProperty("result")] + public string Result { get; set; } + + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + } +} +#nullable restore \ No newline at end of file diff --git a/src/CouchDB.Driver/DTOs/IndexDefinitionInfo.cs b/src/CouchDB.Driver/DTOs/IndexDefinitionInfo.cs new file mode 100644 index 0000000..792409b --- /dev/null +++ b/src/CouchDB.Driver/DTOs/IndexDefinitionInfo.cs @@ -0,0 +1,13 @@ +#nullable disable +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace CouchDB.Driver.DTOs +{ + internal class IndexDefinitionInfo + { + [JsonProperty("fields")] + public Dictionary[] Fields { get; set; } + } +} +#nullable enable \ No newline at end of file diff --git a/src/CouchDB.Driver/ICouchDatabase.cs b/src/CouchDB.Driver/ICouchDatabase.cs index dd1ad1a..73379bd 100644 --- a/src/CouchDB.Driver/ICouchDatabase.cs +++ b/src/CouchDB.Driver/ICouchDatabase.cs @@ -139,16 +139,16 @@ IAsyncEnumerable> GetContinuousChangesAsync( /// The action to configure the index builder. /// The index options. /// A to observe while waiting for the task to complete. - /// A task that represents the asynchronous operation. - Task CreateIndexAsync(string name, Action> indexBuilderAction, + /// A task that represents the asynchronous operation. The task result contains the ID of the design document. + Task CreateIndexAsync(string name, Action> indexBuilderAction, IndexOptions? options = null, CancellationToken cancellationToken = default); /// /// Deletes a specific index. /// - /// The design document name - /// The index name + /// The design document name. + /// The index name. /// A to observe while waiting for the task to complete. /// A task that represents the asynchronous operation. Task DeleteIndexAsync(string designDocument, string name, CancellationToken cancellationToken = default); diff --git a/src/CouchDB.Driver/Types/IndexFieldDirection.cs b/src/CouchDB.Driver/Types/IndexFieldDirection.cs new file mode 100644 index 0000000..1ccfdc7 --- /dev/null +++ b/src/CouchDB.Driver/Types/IndexFieldDirection.cs @@ -0,0 +1,11 @@ +namespace CouchDB.Driver.Types +{ + /// + /// Represent the direction of the index. + /// + public enum IndexFieldDirection + { + Ascending = 0, + Descending = 1, + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Types/IndexInfo.cs b/src/CouchDB.Driver/Types/IndexInfo.cs index 0ddd8c4..b44352b 100644 --- a/src/CouchDB.Driver/Types/IndexInfo.cs +++ b/src/CouchDB.Driver/Types/IndexInfo.cs @@ -1,6 +1,7 @@ #nullable disable using System.Collections.Generic; using System.Linq; +using CouchDB.Driver.DTOs; using Newtonsoft.Json; namespace CouchDB.Driver.Types @@ -51,20 +52,5 @@ internal IndexDefinitionInfo Definition } } } - - internal class IndexDefinitionInfo - { - [JsonProperty("fields")] - public Dictionary[] Fields { get; set; } - } - - /// - /// Represent the direction of the index. - /// - public enum IndexFieldDirection - { - Ascending = 0, - Descending = 1, - } } #nullable restore \ No newline at end of file From e561f168d281bf6cdec9d246b5d464e8ada18b0b Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Wed, 16 Sep 2020 20:42:39 +0200 Subject: [PATCH 12/18] Index builder refactor --- src/CouchDB.Driver/CouchContext.cs | 3 +- src/CouchDB.Driver/CouchDatabase.cs | 22 +++++++-- src/CouchDB.Driver/Indexes/IndexBuilder.cs | 35 +++---------- src/CouchDB.Driver/Indexes/IndexDefinition.cs | 49 +++++++++++++++++++ .../Options/CouchDocumentBuilder.cs | 6 +-- ...xDefinition.cs => IndexSetupDefinition.cs} | 4 +- 6 files changed, 80 insertions(+), 39 deletions(-) create mode 100644 src/CouchDB.Driver/Indexes/IndexDefinition.cs rename src/CouchDB.Driver/Options/{IndexDefinition.cs => IndexSetupDefinition.cs} (72%) diff --git a/src/CouchDB.Driver/CouchContext.cs b/src/CouchDB.Driver/CouchContext.cs index d1966f2..17c9df4 100644 --- a/src/CouchDB.Driver/CouchContext.cs +++ b/src/CouchDB.Driver/CouchContext.cs @@ -4,6 +4,7 @@ using System.Reflection; using System.Threading.Tasks; using CouchDB.Driver.Helpers; +using CouchDB.Driver.Indexes; using CouchDB.Driver.Options; using CouchDB.Driver.Types; @@ -82,7 +83,7 @@ private async Task ApplyDatabaseChangesAsync(PropertyInfo propertyInfo, List indexes = await database.GetIndexesAsync().ConfigureAwait(false); - foreach (IndexDefinition indexDefinition in documentBuilder.IndexDefinitions) + foreach (IndexSetupDefinition indexDefinition in documentBuilder.IndexDefinitions) { // Delete the index if it already exists if (options.OverrideExistingIndexes && indexDefinition.Options?.DesignDocument != null) diff --git a/src/CouchDB.Driver/CouchDatabase.cs b/src/CouchDB.Driver/CouchDatabase.cs index 97566e1..3ecb4ac 100644 --- a/src/CouchDB.Driver/CouchDatabase.cs +++ b/src/CouchDB.Driver/CouchDatabase.cs @@ -428,7 +428,7 @@ public async Task> GetIndexesAsync(CancellationToken cancellatio } /// - public async Task CreateIndexAsync(string name, + public Task CreateIndexAsync(string name, Action> indexBuilderAction, IndexOptions? options = null, CancellationToken cancellationToken = default) @@ -436,10 +436,16 @@ public async Task CreateIndexAsync(string name, Check.NotNull(name, nameof(name)); Check.NotNull(indexBuilderAction, nameof(indexBuilderAction)); - var builder = new IndexBuilder(_options, _queryProvider); - indexBuilderAction(builder); + IndexDefinition indexDefinition = NewIndexBuilder(indexBuilderAction).Build(); + return CreateIndexAsync(name, indexDefinition, options, cancellationToken); + } - var indexJson = builder.ToString(); + internal async Task CreateIndexAsync(string name, + IndexDefinition indexDefinition, + IndexOptions? options = null, + CancellationToken cancellationToken = default) + { + var indexJson = indexDefinition.ToString(); var sb = new StringBuilder(); sb.Append("{") @@ -574,6 +580,14 @@ internal CouchQueryable AsQueryable() return new CouchQueryable(_queryProvider); } + internal IndexBuilder NewIndexBuilder( + Action> indexBuilderAction) + { + var builder = new IndexBuilder(_options, _queryProvider); + indexBuilderAction(builder); + return builder; + } + #endregion } } \ No newline at end of file diff --git a/src/CouchDB.Driver/Indexes/IndexBuilder.cs b/src/CouchDB.Driver/Indexes/IndexBuilder.cs index 58e6097..a4ec575 100644 --- a/src/CouchDB.Driver/Indexes/IndexBuilder.cs +++ b/src/CouchDB.Driver/Indexes/IndexBuilder.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; -using System.Text; using CouchDB.Driver.Extensions; using CouchDB.Driver.Options; using CouchDB.Driver.Query; @@ -64,35 +64,12 @@ private void AddField(Expression> selector) _fields.Add(memberExpression.GetPropertyName(_options)); } - public override string ToString() + public IndexDefinition Build() { - var sb = new StringBuilder(); - - sb.Append("{"); - - // Partial Selector - if (_partialSelector != null) - { - sb.Append(_partialSelector); - sb.Append(","); - } - - // Fields - sb.Append("\"fields\":["); - - foreach (var field in _fields) - { - var fieldString = _ascending - ? $"\"{field}\"," - : $"{{\"{field}\":\"desc\"}},"; - - sb.Append(fieldString); - } - - sb.Length--; - sb.Append("]}"); - - return sb.ToString(); + var fields = _fields.ToDictionary( + field => field, + _ => _ascending ? IndexFieldDirection.Ascending : IndexFieldDirection.Descending); + return new IndexDefinition(fields, _partialSelector); } } } \ No newline at end of file diff --git a/src/CouchDB.Driver/Indexes/IndexDefinition.cs b/src/CouchDB.Driver/Indexes/IndexDefinition.cs new file mode 100644 index 0000000..660e5cb --- /dev/null +++ b/src/CouchDB.Driver/Indexes/IndexDefinition.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Text; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Indexes +{ + internal class IndexDefinition + { + public IndexDefinition(Dictionary fields, string? partialSelector) + { + Fields = fields; + PartialSelector = partialSelector; + } + + public Dictionary Fields { get; } + public string? PartialSelector { get; } + + public override string ToString() + { + var sb = new StringBuilder(); + + sb.Append("{"); + + // Partial Selector + if (PartialSelector != null) + { + sb.Append(PartialSelector); + sb.Append(","); + } + + // Fields + sb.Append("\"fields\":["); + + foreach ((var fieldName, IndexFieldDirection fieldDirection) in Fields) + { + var fieldString = fieldDirection == IndexFieldDirection.Ascending + ? $"\"{fieldName}\"," + : $"{{\"{fieldName}\":\"desc\"}},"; + + sb.Append(fieldString); + } + + sb.Length--; + sb.Append("]}"); + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Options/CouchDocumentBuilder.cs b/src/CouchDB.Driver/Options/CouchDocumentBuilder.cs index 798f158..3bc9de9 100644 --- a/src/CouchDB.Driver/Options/CouchDocumentBuilder.cs +++ b/src/CouchDB.Driver/Options/CouchDocumentBuilder.cs @@ -8,17 +8,17 @@ namespace CouchDB.Driver.Options public class CouchDocumentBuilder where TSource : CouchDocument { - internal List> IndexDefinitions { get; } + internal List> IndexDefinitions { get; } internal CouchDocumentBuilder() { - IndexDefinitions = new List>(); + IndexDefinitions = new List>(); } public CouchDocumentBuilder HasIndex(string name, Action> indexBuilderAction, IndexOptions? options = null) { - var indexDefinition = new IndexDefinition(name, indexBuilderAction, options); + var indexDefinition = new IndexSetupDefinition(name, indexBuilderAction, options); IndexDefinitions.Add(indexDefinition); return this; } diff --git a/src/CouchDB.Driver/Options/IndexDefinition.cs b/src/CouchDB.Driver/Options/IndexSetupDefinition.cs similarity index 72% rename from src/CouchDB.Driver/Options/IndexDefinition.cs rename to src/CouchDB.Driver/Options/IndexSetupDefinition.cs index a2ecaf5..fd3475f 100644 --- a/src/CouchDB.Driver/Options/IndexDefinition.cs +++ b/src/CouchDB.Driver/Options/IndexSetupDefinition.cs @@ -4,10 +4,10 @@ namespace CouchDB.Driver.Options { - internal class IndexDefinition + internal class IndexSetupDefinition where TSource : CouchDocument { - public IndexDefinition(string name, Action> indexBuilderAction, IndexOptions? options) + public IndexSetupDefinition(string name, Action> indexBuilderAction, IndexOptions? options) { Name = name; IndexBuilderAction = indexBuilderAction; From 236c999bf847b4796a8a6d5c30ffab0cf80af0cd Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Wed, 16 Sep 2020 21:15:26 +0200 Subject: [PATCH 13/18] Implements index update on context --- src/CouchDB.Driver/CouchContext.cs | 79 +++++++++++++--- tests/CouchDB.Driver.E2ETests/Client_Tests.cs | 3 + .../MyDeathStarContext.cs | 25 +++++ .../CouchDbContext_Index_Tests.cs | 93 ++++++++++++++++++- 4 files changed, 184 insertions(+), 16 deletions(-) diff --git a/src/CouchDB.Driver/CouchContext.cs b/src/CouchDB.Driver/CouchContext.cs index 17c9df4..49cef49 100644 --- a/src/CouchDB.Driver/CouchContext.cs +++ b/src/CouchDB.Driver/CouchContext.cs @@ -83,26 +83,77 @@ private async Task ApplyDatabaseChangesAsync(PropertyInfo propertyInfo, List indexes = await database.GetIndexesAsync().ConfigureAwait(false); - foreach (IndexSetupDefinition indexDefinition in documentBuilder.IndexDefinitions) + foreach (IndexSetupDefinition indexSetup in documentBuilder.IndexDefinitions) { - // Delete the index if it already exists - if (options.OverrideExistingIndexes && indexDefinition.Options?.DesignDocument != null) - { - IndexInfo existingIndex = indexes.FirstOrDefault(i => - i.DesignDocument.Equals(indexDefinition.Options.DesignDocument, StringComparison.InvariantCultureIgnoreCase)); + await TryCreateOrUpdateIndexAsync(options, indexes, indexSetup, database) + .ConfigureAwait(false); + } + } - if (existingIndex != null) - { - await database.DeleteIndexAsync(existingIndex).ConfigureAwait(false); - } - } + private static async Task TryCreateOrUpdateIndexAsync( + CouchOptions options, + IEnumerable indexes, + IndexSetupDefinition indexSetup, + CouchDatabase database) + where TSource : CouchDocument + { + IndexInfo? currentIndex = TryFindIndex( + indexes, + indexSetup.Name, + indexSetup.Options?.DesignDocument); + if (currentIndex == null) + { await database.CreateIndexAsync( - indexDefinition.Name, - indexDefinition.IndexBuilderAction, - indexDefinition.Options) + indexSetup.Name, + indexSetup.IndexBuilderAction, + indexSetup.Options) + .ConfigureAwait(false); + return; + } + + if (!options.OverrideExistingIndexes) + { + return; + } + + IndexDefinition indexDefinition = database.NewIndexBuilder(indexSetup.IndexBuilderAction).Build(); + if (!AreFieldsEqual(currentIndex.Fields, indexDefinition.Fields)) + { + await database.DeleteIndexAsync(currentIndex) .ConfigureAwait(false); + await database.CreateIndexAsync(indexSetup.Name, indexDefinition, indexSetup.Options) + .ConfigureAwait(false); + } + } + + private static IndexInfo? TryFindIndex(IEnumerable indexes, string name, string? designDocument) + { + return indexes.SingleOrDefault(current => + current.Name == name && + (designDocument == null || current.DesignDocument == designDocument)); + } + + private static bool AreFieldsEqual(Dictionary current, + Dictionary requested) + { + if (current.Count != requested.Count) + { + return false; + } + + for (var i = 0; i < current.Count; i++) + { + (var currentField, IndexFieldDirection currentDirection) = current.ElementAt(i); + (var requestedField, IndexFieldDirection requestedDirection) = requested.ElementAt(i); + + if (currentField != requestedField || currentDirection != requestedDirection) + { + return false; + } } + + return true; } private IEnumerable GetDatabaseProperties() => diff --git a/tests/CouchDB.Driver.E2ETests/Client_Tests.cs b/tests/CouchDB.Driver.E2ETests/Client_Tests.cs index dce7338..194963e 100644 --- a/tests/CouchDB.Driver.E2ETests/Client_Tests.cs +++ b/tests/CouchDB.Driver.E2ETests/Client_Tests.cs @@ -106,7 +106,10 @@ public async Task Crud_Context() [Fact] public async Task Crud_Index_Context() { + // Create index await using var context = new MyDeathStarContext(); + // Override index + await using var newContext = new MyDeathStarContext2(); var indexes = await context.Rebels.GetIndexesAsync(); diff --git a/tests/CouchDB.Driver.E2ETests/MyDeathStarContext.cs b/tests/CouchDB.Driver.E2ETests/MyDeathStarContext.cs index b00f6a1..d78ae5d 100644 --- a/tests/CouchDB.Driver.E2ETests/MyDeathStarContext.cs +++ b/tests/CouchDB.Driver.E2ETests/MyDeathStarContext.cs @@ -23,4 +23,29 @@ protected override void OnDatabaseCreating(CouchDatabaseBuilder databaseBuilder) .ThenBy(r => r.Name)); } } + + /// + /// Different indexes + /// + public class MyDeathStarContext2 : CouchContext + { + public CouchDatabase Rebels { get; set; } + + protected override void OnConfiguring(CouchOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseEndpoint("http://localhost:5984/") + .EnsureDatabaseExists() + .OverrideExistingIndexes() + .UseBasicAuthentication(username: "admin", password: "admin"); + } + + protected override void OnDatabaseCreating(CouchDatabaseBuilder databaseBuilder) + { + databaseBuilder.Document() + .HasIndex("surnames_index", builder => builder + .IndexByDescending(r => r.Surname) + .ThenByDescending(r => r.Name)); + } + } } diff --git a/tests/CouchDB.Driver.UnitTests/CouchDbContext_Index_Tests.cs b/tests/CouchDB.Driver.UnitTests/CouchDbContext_Index_Tests.cs index be7de52..70f777f 100644 --- a/tests/CouchDB.Driver.UnitTests/CouchDbContext_Index_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/CouchDbContext_Index_Tests.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; using CouchDB.Driver.Extensions; using CouchDB.Driver.Options; using CouchDB.Driver.UnitTests.Models; @@ -17,6 +19,7 @@ protected override void OnConfiguring(CouchOptionsBuilder optionsBuilder) { optionsBuilder .UseEndpoint("http://localhost:5984/") + .OverrideExistingIndexes() .UseBasicAuthentication("admin", "admin"); } @@ -28,10 +31,14 @@ protected override void OnDatabaseCreating(CouchDatabaseBuilder databaseBuilder) } [Fact] - public async Task Context_Query() + public async Task Context_Index() { using var httpTest = new HttpTest(); httpTest.RespondWithJson(new + { + Indexes = new string[0] + }); + httpTest.RespondWithJson(new { Result = "created" }); @@ -51,5 +58,87 @@ public async Task Context_Query() Assert.NotEmpty(result); Assert.Equal("Luke", result[0].Name); } + + [Fact] + public async Task Context_Index_NotToOverride() + { + using var httpTest = new HttpTest(); + httpTest.RespondWithJson(new + { + Indexes = new [] + { + new { + ddoc = Guid.NewGuid().ToString(), + name = "skywalkers", + def = new { + fields = new[] { + new Dictionary{ { "surname", "asc" } } + } + } + } + } + }); + httpTest.RespondWithJson(new + { + docs = new object[] { + new { + Id = "176694", + Rev = "1-54f8e950cc338d2385d9b0cda2fd918e", + Name = "Luke" + } + } + }); + + await using var context = new MyDeathStarContext(); + var result = await context.Rebels.ToListAsync(); + Assert.NotEmpty(result); + Assert.Equal("Luke", result[0].Name); + } + + [Fact] + public async Task Context_Index_ToOverride() + { + using var httpTest = new HttpTest(); + httpTest.RespondWithJson(new + { + Indexes = new[] + { + new { + ddoc = Guid.NewGuid().ToString(), + name = "skywalkers", + def = new { + fields = new[] { + new Dictionary{ { "surname", "desc" } } + } + } + } + } + }); + // Delete + httpTest.RespondWithJson(new + { + ok = true + }); + // Create new + httpTest.RespondWithJson(new + { + Result = "created" + }); + // Query + httpTest.RespondWithJson(new + { + docs = new object[] { + new { + Id = "176694", + Rev = "1-54f8e950cc338d2385d9b0cda2fd918e", + Name = "Luke" + } + } + }); + await using var context = new MyDeathStarContext(); + var result = await context.Rebels.ToListAsync(); + Assert.NotEmpty(result); + Assert.Equal("Luke", result[0].Name); + } } } From bc5061d454c4432ae30dede84d9667682ea0188d Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Wed, 16 Sep 2020 21:23:33 +0200 Subject: [PATCH 14/18] Documentation update --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 7ce7eba..6378f70 100644 --- a/README.md +++ b/README.md @@ -478,6 +478,33 @@ await _rebels.DeleteIndexAsync(indexes[0]); await _rebels.DeleteIndexAsync("surnames_ddoc", name: "surnames"); ``` +### CouchContext Index Configuration + +Finally it's possible to configure indexes on the `CouchContext`. +```csharp +public class MyDeathStarContext : CouchContext +{ + public CouchDatabase Rebels { get; set; } + + protected override void OnConfiguring(CouchOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseEndpoint("http://localhost:5984/") + .UseBasicAuthentication("admin", "admin") + // If it finds a index with the same name and ddoc (or null) + // but with different fields and/or sort order, + // it will override the index + .OverrideExistingIndexes(); + } + + protected override void OnDatabaseCreating(CouchDatabaseBuilder databaseBuilder) + { + databaseBuilder.Document() + .HasIndex("rebel_surnames_index", b => b.IndexBy(b => b.Surname)); + } +} +``` + ## Local (non-replicating) Documents The Local (non-replicating) document interface allows you to create local documents that are not replicated to other databases. From 99c7e2edb2d9a2f71ba90c9d2194406d6db6e0ed Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Thu, 17 Sep 2020 20:29:01 +0200 Subject: [PATCH 15/18] #104: Fix queries with variable reference. --- src/CouchDB.Driver/Query/QueryCompiler.cs | 7 +++-- .../Find/Find_Selector_Combinations.cs | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/CouchDB.Driver/Query/QueryCompiler.cs b/src/CouchDB.Driver/Query/QueryCompiler.cs index 71ceb6e..a4b21b2 100644 --- a/src/CouchDB.Driver/Query/QueryCompiler.cs +++ b/src/CouchDB.Driver/Query/QueryCompiler.cs @@ -70,7 +70,8 @@ private TResult SendRequest(Expression query, bool async, CancellationT private TResult SendRequestWithoutFilter(Expression query, bool async, CancellationToken cancellationToken) { - var body = _queryTranslator.Translate(query); + Expression optimizedQuery = _queryOptimizer.Optimize(query); + var body = _queryTranslator.Translate(optimizedQuery); return _requestSender.Send(body, async, cancellationToken); } @@ -84,7 +85,7 @@ private TResult SendRequestWithFilter(MethodCallExpression methodCallEx throw new ArgumentException($"Expression of type {optimizedQuery.GetType().Name} is not valid."); } - var body = _queryTranslator.Translate(query); + var body = _queryTranslator.Translate(optimizedQuery); // If no operation must be done on the list return if (!methodCallExpression.Method.IsSupportedByComposition()) @@ -97,7 +98,7 @@ private TResult SendRequestWithFilter(MethodCallExpression methodCallEx Type returnType = GetReturnType(async); // Query database - var couchQueryable = RequestSendMethod + object couchQueryable = RequestSendMethod .MakeGenericMethod(couchListType) .Invoke(_requestSender, new object[]{ body, async, cancellationToken }); diff --git a/tests/CouchDB.Driver.UnitTests/Find/Find_Selector_Combinations.cs b/tests/CouchDB.Driver.UnitTests/Find/Find_Selector_Combinations.cs index d4b6d0b..78b93b3 100644 --- a/tests/CouchDB.Driver.UnitTests/Find/Find_Selector_Combinations.cs +++ b/tests/CouchDB.Driver.UnitTests/Find/Find_Selector_Combinations.cs @@ -4,6 +4,7 @@ using CouchDB.Driver.Extensions; using System.Linq; using CouchDB.Driver.Query.Extensions; +using Flurl.Http.Testing; namespace CouchDB.Driver.UnitTests.Find { @@ -17,6 +18,34 @@ public Find_Selector_Combinations() _rebels = client.GetDatabase(); } + [Fact] + public void And_HttpCall() + { + using var httpTest = new HttpTest(); + httpTest.RespondWithJson(new + { + docs = new[] + { + new Rebel() + } + }); + var surname = "Skywalker"; + var result = _rebels.Where(r => r.Name == "Luke" && r.Surname == surname).ToList(); + Assert.NotEmpty(result); + + var actualBody = httpTest.CallLog[0].RequestBody; + var expectedBody = @"{""selector"":{""$and"":[{""name"":""Luke""},{""surname"":""Skywalker""}]}}"; + Assert.Equal(expectedBody, actualBody); + } + + [Fact] + public void And_WithVariable() + { + var surname = "Skywalker"; + var json = _rebels.Where(r => r.Name == "Luke" && r.Surname == surname).ToString(); + Assert.Equal(@"{""selector"":{""$and"":[{""name"":""Luke""},{""surname"":""Skywalker""}]}}", json); + } + [Fact] public void And() { From 028c3676884d407cba506f5d296ddaa24b217851 Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Thu, 17 Sep 2020 20:35:35 +0200 Subject: [PATCH 16/18] Update latest change --- LATEST_CHANGE.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/LATEST_CHANGE.md b/LATEST_CHANGE.md index 0242b64..61310f8 100644 --- a/LATEST_CHANGE.md +++ b/LATEST_CHANGE.md @@ -1,6 +1,7 @@ ## Features -* **Indexes"**: Ability to create indexes. -* **Null values"**: New `SetNullValueHandling` method for `CouchOptionsBuilder` to set how to handle null values. +* **Indexes"**: Ability to create indexes. ([#102](https://github.com/matteobortolazzo/couchdb-net/issues/102)) +* **Null values"**: New `SetNullValueHandling` method for `CouchOptionsBuilder` to set how to handle null values. ([#101](https://github.com/matteobortolazzo/couchdb-net/issues/101)) ## Bug Fixes -* **Conflicts**: Fix the query parameter value to get conflicts. \ No newline at end of file +* **Conflicts**: Fix the query parameter value to get conflicts. ([#100](https://github.com/matteobortolazzo/couchdb-net/issues/100)) +* **Query**: Fix queries when variables are used. ([#104](https://github.com/matteobortolazzo/couchdb-net/issues/104)) \ No newline at end of file From 3b85f9af9e854d098bc6de80f04e94ba8b453065 Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Sat, 19 Sep 2020 12:21:50 +0200 Subject: [PATCH 17/18] #103: Implements 'Fields' selector properly --- LATEST_CHANGE.md | 1 + README.md | 9 +-- .../Extensions/QueryableQueryExtensions.cs | 44 ++++++++++++++- .../MethodCallExpressionTranslator.cs | 56 +++++++++++++++++++ .../Shared/SupportedMethodsProvider.cs | 2 + .../Shared/SupportedQueryMethods.cs | 9 +-- .../Find/Find_Miscellaneous.cs | 37 +++++++++--- .../Settings_Tests.cs | 4 +- .../CouchDB.Driver.UnitTests/_Models/Rebel.cs | 8 ++- 9 files changed, 150 insertions(+), 20 deletions(-) diff --git a/LATEST_CHANGE.md b/LATEST_CHANGE.md index 61310f8..136c07d 100644 --- a/LATEST_CHANGE.md +++ b/LATEST_CHANGE.md @@ -1,6 +1,7 @@ ## Features * **Indexes"**: Ability to create indexes. ([#102](https://github.com/matteobortolazzo/couchdb-net/issues/102)) * **Null values"**: New `SetNullValueHandling` method for `CouchOptionsBuilder` to set how to handle null values. ([#101](https://github.com/matteobortolazzo/couchdb-net/issues/101)) +* **Query"**: New `Select` and `Convert` methods to select specific fields. ## Bug Fixes * **Conflicts**: Fix the query parameter value to get conflicts. ([#100](https://github.com/matteobortolazzo/couchdb-net/issues/100)) diff --git a/README.md b/README.md index 6378f70..c73b623 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,9 @@ var skywalkers = await context.Rebels .OrderByDescending(r => r.Name) .ThenByDescending(r => r.Age) .Take(2) - .Select(r => new { - r.Name, - r.Age + .Select( + r => r.Name, + r => r.Age }) .ToListAsync(); ``` @@ -195,7 +195,8 @@ If the Where method is not called in the expression, it will at an empty selecto | sort | OrderBy(..).ThenBy() | | sort | OrderByDescending(..) | | sort | OrderByDescending(..).ThenByDescending() | -| fields | Select(x => new { }) | +| fields | Select(x => x.Prop1, x => x.Prop2) | +| fields | Convert() | | use_index | UseIndex("design_document") | | use_index | UseIndex(new [] { "design_document", "index_name" }) | | r | WithReadQuorum(n) | diff --git a/src/CouchDB.Driver/Query/Extensions/QueryableQueryExtensions.cs b/src/CouchDB.Driver/Query/Extensions/QueryableQueryExtensions.cs index 841af2d..810d8c5 100644 --- a/src/CouchDB.Driver/Query/Extensions/QueryableQueryExtensions.cs +++ b/src/CouchDB.Driver/Query/Extensions/QueryableQueryExtensions.cs @@ -3,13 +3,14 @@ using System.Linq.Expressions; using System.Reflection; using CouchDB.Driver.Helpers; +using CouchDB.Driver.Types; namespace CouchDB.Driver.Query.Extensions { public static class QueryableQueryExtensions { #region Helper methods to obtain MethodInfo in a safe way - + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1801:Review unused parameters")] private static MethodInfo GetMethodInfo(Func f, T1 unused1) { @@ -149,5 +150,46 @@ public static IQueryable IncludeConflicts(this IQueryable + /// Select specific fields to return in the result. + /// + /// The source of items. + /// List of functions to select fields. + /// An that contains the request specific fields when requesting elements from the sequence. + public static IQueryable Select(this IQueryable source, params Expression>[] selectFunctions) + { + Check.NotNull(source, nameof(source)); + + foreach (Expression> selectFunction in selectFunctions) + { + Check.NotNull(selectFunction, nameof(selectFunctions)); + } + + return source.Provider.CreateQuery( + Expression.Call( + null, + GetMethodInfo(Select, source, selectFunctions), + new[] { source.Expression, Expression.Constant(selectFunctions) })); + } + + /// + /// Select only the field defined in + /// + /// Type of source. + /// Type of output list. + /// The source of items. + /// An that contains the request specific fields when requesting elements from the sequence. + public static IQueryable Convert(this IQueryable source) + where TResult : CouchDocument + { + Check.NotNull(source, nameof(source)); + + return source.Provider.CreateQuery( + Expression.Call( + null, + GetMethodInfo(Convert, source), + new[] { source.Expression })); + } } } diff --git a/src/CouchDB.Driver/Query/Translators/MethodCallExpressionTranslator.cs b/src/CouchDB.Driver/Query/Translators/MethodCallExpressionTranslator.cs index 66b557a..149245a 100644 --- a/src/CouchDB.Driver/Query/Translators/MethodCallExpressionTranslator.cs +++ b/src/CouchDB.Driver/Query/Translators/MethodCallExpressionTranslator.cs @@ -1,10 +1,12 @@ using CouchDB.Driver.Types; using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Security.Authentication; using CouchDB.Driver.Extensions; +using CouchDB.Driver.Helpers; using CouchDB.Driver.Shared; #pragma warning disable IDE0058 // Expression value is never used @@ -120,6 +122,16 @@ protected override Expression VisitMethodCall(MethodCallExpression m) return VisitIncludeConflictsMethod(m); } + if (genericDefinition == SupportedQueryMethods.Select) + { + return VisitSelectFieldMethod(m); + } + + if (genericDefinition == SupportedQueryMethods.Convert) + { + return VisitConvertMethod(m); + } + // IEnumerable extensions if (genericDefinition == SupportedQueryMethods.EnumerableContains) @@ -376,6 +388,7 @@ public Expression VisitIncludeExecutionStatsMethod(MethodCallExpression m) _sb.Append(","); return m; } + public Expression VisitIncludeConflictsMethod(MethodCallExpression m) { Visit(m.Arguments[0]); @@ -384,6 +397,49 @@ public Expression VisitIncludeConflictsMethod(MethodCallExpression m) return m; } + public Expression VisitSelectFieldMethod(MethodCallExpression m) + { + Visit(m.Arguments[0]); + _sb.Append("\"fields\":["); + + if (!(((ConstantExpression)m.Arguments[1]).Value is Expression[] fieldExpressions)) + { + throw new InvalidOperationException(); + } + + foreach (Expression a in fieldExpressions) + { + Visit(a); + _sb.Append(","); + } + + _sb.Length--; + _sb.Append("],"); + return m; + } + + public Expression VisitConvertMethod(MethodCallExpression m) + { + Visit(m.Arguments[0]); + _sb.Append("\"fields\":["); + + Type returnType = m.Method.GetGenericArguments()[1]; + PropertyInfo[] properties = returnType + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => p.DeclaringType != typeof(CouchDocument)) + .ToArray(); + + foreach (PropertyInfo property in properties) + { + var field = property.GetCouchPropertyName(_options.PropertiesCase); + _sb.Append($"\"{field}\","); + } + + _sb.Length--; + _sb.Append("],"); + return m; + } + #endregion #region EnumerableExtensions diff --git a/src/CouchDB.Driver/Shared/SupportedMethodsProvider.cs b/src/CouchDB.Driver/Shared/SupportedMethodsProvider.cs index 48fd467..59069ba 100644 --- a/src/CouchDB.Driver/Shared/SupportedMethodsProvider.cs +++ b/src/CouchDB.Driver/Shared/SupportedMethodsProvider.cs @@ -50,6 +50,8 @@ static SupportedMethodsProvider() SupportedQueryMethods.UseIndex, SupportedQueryMethods.IncludeExecutionStats, SupportedQueryMethods.IncludeConflicts, + SupportedQueryMethods.Select, + SupportedQueryMethods.Convert, SupportedQueryMethods.EnumerableContains, SupportedQueryMethods.FieldExists, SupportedQueryMethods.IsCouchType, diff --git a/src/CouchDB.Driver/Shared/SupportedQueryMethods.cs b/src/CouchDB.Driver/Shared/SupportedQueryMethods.cs index f1bcada..0a28da7 100644 --- a/src/CouchDB.Driver/Shared/SupportedQueryMethods.cs +++ b/src/CouchDB.Driver/Shared/SupportedQueryMethods.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Reflection; -using CouchDB.Driver.Extensions; using CouchDB.Driver.Query.Extensions; namespace CouchDB.Driver.Shared @@ -18,6 +15,8 @@ internal static class SupportedQueryMethods public static MethodInfo UseIndex { get; } public static MethodInfo IncludeExecutionStats { get; } public static MethodInfo IncludeConflicts { get; } + public static MethodInfo Select { get; } + public static MethodInfo Convert { get; } public static MethodInfo EnumerableContains { get; } public static MethodInfo FieldExists { get; } public static MethodInfo IsCouchType { get; } @@ -52,6 +51,8 @@ static SupportedQueryMethods() mi.Name == nameof(QueryableQueryExtensions.IncludeExecutionStats)); IncludeConflicts = queryableExtensionsMethods.Single(mi => mi.Name == nameof(QueryableQueryExtensions.IncludeConflicts)); + Select = queryableExtensionsMethods.Single(mi => mi.Name == nameof(QueryableQueryExtensions.Select)); + Convert = queryableExtensionsMethods.Single(mi => mi.Name == nameof(QueryableQueryExtensions.Convert)); EnumerableContains = typeof(EnumerableQueryExtensions) .GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) diff --git a/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs b/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs index 47a82fa..46ea435 100644 --- a/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs +++ b/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs @@ -2,7 +2,6 @@ using System; using System.Linq; using Xunit; -using CouchDB.Driver.Extensions; using CouchDB.Driver.Query.Extensions; namespace CouchDB.Driver.UnitTests.Find @@ -37,20 +36,42 @@ public void Field() var json = _rebels.Select(r => r.Age).ToString(); Assert.Equal(@"{""fields"":[""age""],""selector"":{}}", json); } - + [Fact] public void Fields() { - var json = _rebels.Select(r => new - { - r.Name, - r.Age - }).ToString(); + var json = _rebels.Select( + r => r.Name, + r => r.Age, + r => r.Vehicle.CanFly) + .ToString(); + Assert.Equal(@"{""fields"":[""name"",""age"",""vehicle.canFly""],""selector"":{}}", json); + } + + [Fact] + public void Fields_NewObject() + { + var json = _rebels.Select( + r => new + { + r.Name, + r.Age, + r.Vehicle.CanFly + }) + .ToString(); + Assert.Equal(@"{""fields"":[""name"",""age"",""vehicle.canFly""],""selector"":{}}", json); + } + + [Fact] + public void Convert() + { + var json = _rebels.Convert() + .ToString(); Assert.Equal(@"{""fields"":[""name"",""age""],""selector"":{}}", json); } [Fact] - public void Index_Parlial() + public void Index_Partial() { var json = _rebels.UseIndex("design_document").ToString(); Assert.Equal(@"{""use_index"":""design_document"",""selector"":{}}", json); diff --git a/tests/CouchDB.Driver.UnitTests/Settings_Tests.cs b/tests/CouchDB.Driver.UnitTests/Settings_Tests.cs index 3a50d59..eecd192 100644 --- a/tests/CouchDB.Driver.UnitTests/Settings_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Settings_Tests.cs @@ -206,7 +206,7 @@ public async Task PropertyNullValueHandling_NotSet() var call = httpTest.CallLog.First(); Assert.NotNull(call); - Assert.Equal(@"{""_conflicts"":[],""name"":null,""surname"":null,""age"":0,""isJedi"":false,""species"":0,""guid"":""00000000-0000-0000-0000-000000000000"",""skills"":null,""battles"":null}", call.RequestBody); + Assert.Equal(@"{""_conflicts"":[],""name"":null,""surname"":null,""age"":0,""isJedi"":false,""species"":0,""guid"":""00000000-0000-0000-0000-000000000000"",""skills"":null,""battles"":null,""vehicle"":null}", call.RequestBody); } [Fact] @@ -222,7 +222,7 @@ public async Task PropertyNullValueHandling_Includes() var call = httpTest.CallLog.First(); Assert.NotNull(call); - Assert.Equal(@"{""_conflicts"":[],""name"":null,""surname"":null,""age"":0,""isJedi"":false,""species"":0,""guid"":""00000000-0000-0000-0000-000000000000"",""skills"":null,""battles"":null}", call.RequestBody); + Assert.Equal(@"{""_conflicts"":[],""name"":null,""surname"":null,""age"":0,""isJedi"":false,""species"":0,""guid"":""00000000-0000-0000-0000-000000000000"",""skills"":null,""battles"":null,""vehicle"":null}", call.RequestBody); } [Fact] diff --git a/tests/CouchDB.Driver.UnitTests/_Models/Rebel.cs b/tests/CouchDB.Driver.UnitTests/_Models/Rebel.cs index ff97273..1df64ba 100644 --- a/tests/CouchDB.Driver.UnitTests/_Models/Rebel.cs +++ b/tests/CouchDB.Driver.UnitTests/_Models/Rebel.cs @@ -1,10 +1,15 @@ using CouchDB.Driver.Types; using System; using System.Collections.Generic; -using System.Text; namespace CouchDB.Driver.UnitTests.Models { + public class SimpleRebel : CouchDocument + { + public string Name { get; set; } + public int Age { get; set; } + } + public class Rebel : CouchDocument { public string Name { get; set; } @@ -15,6 +20,7 @@ public class Rebel : CouchDocument public Guid Guid { get; set; } public List Skills { get; set; } public List Battles { get; set; } + public Vehicle Vehicle { get; set; } public override bool Equals(object obj) { From e005d68acc7fce29e2d7ada9d817587b9787bac8 Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Sat, 19 Sep 2020 12:22:19 +0200 Subject: [PATCH 18/18] Update YAML --- src/azure-pipelines.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure-pipelines.yaml b/src/azure-pipelines.yaml index 6debb59..f1639b7 100644 --- a/src/azure-pipelines.yaml +++ b/src/azure-pipelines.yaml @@ -1,6 +1,6 @@ variables: BuildConfiguration: Release - PackageVersion: '2.0.2' + PackageVersion: '2.1.0' trigger: branches: