From 71f4a23cde80a8e68b25fb9944e951310f2d4aac Mon Sep 17 00:00:00 2001 From: Matt Freels Date: Tue, 20 Feb 2024 14:50:16 -0700 Subject: [PATCH 1/2] consolidate Execute impl --- Fauna/Linq/QuerySource.cs | 23 ----------- Fauna/Linq/QuerySourceDsl.cs | 79 +++++++++++++++++++----------------- 2 files changed, 42 insertions(+), 60 deletions(-) diff --git a/Fauna/Linq/QuerySource.cs b/Fauna/Linq/QuerySource.cs index c77d0b16..5e1c9761 100644 --- a/Fauna/Linq/QuerySource.cs +++ b/Fauna/Linq/QuerySource.cs @@ -19,12 +19,6 @@ internal void SetQuery(Query query) { Pipeline = new Pipeline(PipelineMode.Query, query, typeof(TElem), false, null, null); } - - // DSL Helpers - - internal abstract TResult Execute(Pipeline pl); - - internal abstract Task ExecuteAsync(Pipeline pl, CancellationToken cancel = default); } public partial class QuerySource : QuerySource, IQuerySource @@ -39,23 +33,6 @@ internal QuerySource(DataContext ctx, Pipeline pl) // constructors, so they use this base one. internal QuerySource() { } - internal override TResult Execute(Pipeline pl) - { - try - { - var res = ExecuteAsync(pl); - res.Wait(); - return res.Result; - } - catch (AggregateException ex) - { - throw ex.InnerExceptions.First(); - } - } - - internal override Task ExecuteAsync(Pipeline pl, CancellationToken cancel = default) => - pl.GetExec(Ctx).Result(queryOptions: null, cancel: cancel); - public IAsyncEnumerable> PaginateAsync(QueryOptions? queryOptions = null, CancellationToken cancel = default) { var pe = Pipeline.GetExec(Ctx); diff --git a/Fauna/Linq/QuerySourceDsl.cs b/Fauna/Linq/QuerySourceDsl.cs index df5fa678..1889f7b5 100644 --- a/Fauna/Linq/QuerySourceDsl.cs +++ b/Fauna/Linq/QuerySourceDsl.cs @@ -103,12 +103,12 @@ private Pipeline CountImpl(Expression>? predicate) => q: QH.MethodCall(MaybeWhereCall(Query, predicate), "count"), ety: typeof(int)); - public T First() => ExecuteMaybeEmpty(FirstImpl(null)); + public T First() => Execute(FirstImpl(null)); public Task FirstAsync(CancellationToken cancel = default) => - ExecuteMaybeEmptyAsync(FirstImpl(null), cancel); - public T First(Expression> predicate) => ExecuteMaybeEmpty(FirstImpl(predicate)); + ExecuteAsync(FirstImpl(null), cancel); + public T First(Expression> predicate) => Execute(FirstImpl(predicate)); public Task FirstAsync(Expression> predicate, CancellationToken cancel = default) => - ExecuteMaybeEmptyAsync(FirstImpl(predicate), cancel); + ExecuteAsync(FirstImpl(predicate), cancel); private Pipeline FirstImpl(Expression>? predicate) => CopyPipeline( mode: PipelineMode.Scalar, @@ -127,12 +127,12 @@ private Pipeline FirstOrDefaultImpl(Expression>? predicate) => ety: typeof(T), enull: true); - public T Last() => ExecuteMaybeEmpty(LastImpl(null)); + public T Last() => Execute(LastImpl(null)); public Task LastAsync(CancellationToken cancel = default) => - ExecuteMaybeEmptyAsync(LastImpl(null), cancel); - public T Last(Expression> predicate) => ExecuteMaybeEmpty(LastImpl(predicate)); + ExecuteAsync(LastImpl(null), cancel); + public T Last(Expression> predicate) => Execute(LastImpl(predicate)); public Task LastAsync(Expression> predicate, CancellationToken cancel = default) => - ExecuteMaybeEmptyAsync(LastImpl(predicate), cancel); + ExecuteAsync(LastImpl(predicate), cancel); private Pipeline LastImpl(Expression>? predicate) => CopyPipeline( mode: PipelineMode.Scalar, @@ -165,15 +165,15 @@ private Pipeline LongCountImpl(Expression>? predicate) => private static readonly Query _maxReducer = QH.Expr("(a, b) => if (a >= b) a else b"); - public T Max() => ExecuteMaybeEmpty(MaxImpl(null)); + public T Max() => Execute(MaxImpl(null)); public Task MaxAsync(CancellationToken cancel = default) => - ExecuteMaybeEmptyAsync(MaxImpl(null), cancel); - public R Max(Expression> selector) => ExecuteMaybeEmpty(MaxImpl(selector)); + ExecuteAsync(MaxImpl(null), cancel); + public R Max(Expression> selector) => Execute(MaxImpl(selector)); public Task MaxAsync(Expression> selector, CancellationToken cancel = default) => - ExecuteMaybeEmptyAsync(MaxImpl(selector), cancel); - private Pipeline MaxImpl(Expression>? selector) + ExecuteAsync(MaxImpl(selector), cancel); + private Pipeline MaxImpl(Expression>? selector, [CallerMemberName] string callerName = "") { - RequireQueryMode("Max"); + RequireQueryMode(callerName); return CopyPipeline( mode: PipelineMode.Scalar, q: QH.MethodCall(MaybeMap(AbortIfEmpty(Query), selector), "reduce", _maxReducer), @@ -182,14 +182,14 @@ private Pipeline MaxImpl(Expression>? selector) private static readonly Query _minReducer = QH.Expr("(a, b) => if (a <= b) a else b"); - public T Min() => ExecuteMaybeEmpty(MinImpl(null)); - public Task MinAsync(CancellationToken cancel = default) => ExecuteMaybeEmptyAsync(MinImpl(null), cancel); - public R Min(Expression> selector) => ExecuteMaybeEmpty(MinImpl(selector)); + public T Min() => Execute(MinImpl(null)); + public Task MinAsync(CancellationToken cancel = default) => ExecuteAsync(MinImpl(null), cancel); + public R Min(Expression> selector) => Execute(MinImpl(selector)); public Task MinAsync(Expression> selector, CancellationToken cancel = default) => - ExecuteMaybeEmptyAsync(MinImpl(selector), cancel); - private Pipeline MinImpl(Expression>? selector) + ExecuteAsync(MinImpl(selector), cancel); + private Pipeline MinImpl(Expression>? selector, [CallerMemberName] string callerName = "") { - RequireQueryMode("Min"); + RequireQueryMode(callerName); return CopyPipeline( mode: PipelineMode.Scalar, q: QH.MethodCall(MaybeMap(AbortIfEmpty(Query), selector), "reduce", _minReducer), @@ -232,34 +232,29 @@ private void RequireQueryMode([CallerMemberName] string callerName = "") } } - private R ExecuteMaybeEmpty(Pipeline pl) + private R Execute(Pipeline pl) { try { - return Execute(pl); + var res = ExecuteAsync(pl); + res.Wait(); + return res.Result; } - catch (AbortException ex) + catch (AggregateException ex) { - throw TranslateEmptySetAbort(ex); + throw TranslateException(ex.InnerExceptions.First()); } } - private async Task ExecuteMaybeEmptyAsync(Pipeline pl, CancellationToken cancel) + private async Task ExecuteAsync(Pipeline pl, CancellationToken cancel = default) { try { - return await ExecuteAsync(pl, cancel); + return await pl.GetExec(Ctx).Result(queryOptions: null, cancel: cancel); } catch (AggregateException ex) { - if (ex.InnerExceptions.First() is AbortException aex) - { - throw TranslateEmptySetAbort(aex); - } - else - { - throw; - } + throw TranslateException(ex.InnerExceptions.First()); } } @@ -300,10 +295,20 @@ private Pipeline CopyPipeline( // value is a string. Work around it by using an array. // FIXME(matt) remove workaround and use a string private Query AbortIfEmpty(Query setq) => - QH.Expr("({ let s = (").Concat(setq).Concat("); if (s.isEmpty()) abort(['empty set']); s })"); + QH.Expr("({ let s = (").Concat(setq).Concat("); if (s.isEmpty()) abort(['empty']); s })"); - private Exception TranslateEmptySetAbort(AbortException ex) => - ex.GetData>()?.First() == "empty set" ? new InvalidOperationException("Empty set") : ex; + private Exception TranslateException(Exception ex) => + ex switch + { + AbortException aex => + aex.GetData>()?.First() switch + { + "empty" => new InvalidOperationException("Empty set"), + "not single" => new InvalidOperationException("Set contains more than one element"), + _ => aex, + }, + _ => ex + }; private Query MaybeWhereCall(Query callee, Expression? predicate, [CallerMemberName] string callerName = "") => predicate is null ? callee : WhereCall(callee, predicate, callerName); From 8d082ab8834d03658cde558eb957089c0c8abfc4 Mon Sep 17 00:00:00 2001 From: Matt Freels Date: Tue, 20 Feb 2024 15:15:49 -0700 Subject: [PATCH 2/2] Single family of methods --- Fauna.Test/Linq/Query.Tests.cs | 47 ++++++++++++++++++++++++++++++++++ Fauna/Linq/IQuerySource.cs | 16 ++++++------ Fauna/Linq/QuerySourceDsl.cs | 34 +++++++++++++++++++++++- 3 files changed, 88 insertions(+), 9 deletions(-) diff --git a/Fauna.Test/Linq/Query.Tests.cs b/Fauna.Test/Linq/Query.Tests.cs index 5b37b9d2..a2bfc005 100644 --- a/Fauna.Test/Linq/Query.Tests.cs +++ b/Fauna.Test/Linq/Query.Tests.cs @@ -475,6 +475,53 @@ public async Task Query_Reverse() Assert.AreEqual(new List { "Bob", "Alice" }, names); } + [Test] + public void Query_Single() + { + try + { + db.Author.Single(); + Assert.Fail(); + } + catch (InvalidOperationException ex) + { + Assert.AreEqual("Set contains more than one element", ex.Message); + } + + var sngPred = db.Author.Single(a => a.Name == "Alice"); + Assert.AreEqual("Alice", sngPred.Name); + + try + { + db.Author.Single(a => a.Name == "No name"); + Assert.Fail(); + } + catch (InvalidOperationException ex) + { + Assert.AreEqual("Empty set", ex.Message); + } + } + + [Test] + public void Query_SingleOrDefault() + { + try + { + db.Author.SingleOrDefault(); + Assert.Fail(); + } + catch (InvalidOperationException ex) + { + Assert.AreEqual("Set contains more than one element", ex.Message); + } + + var sngPred = db.Author.SingleOrDefault(a => a.Name == "Alice"); + Assert.AreEqual("Alice", sngPred?.Name); + + var sngNull = db.Author.SingleOrDefault(a => a.Name == "No name"); + Assert.AreEqual(null, sngNull); + } + [Test] public async Task Query_Skip() { diff --git a/Fauna/Linq/IQuerySource.cs b/Fauna/Linq/IQuerySource.cs index b46ec7e1..7136be84 100644 --- a/Fauna/Linq/IQuerySource.cs +++ b/Fauna/Linq/IQuerySource.cs @@ -110,17 +110,17 @@ public interface IQuerySource : IQuerySource public R Min(Expression> selector); public Task MinAsync(Expression> selector, CancellationToken cancel = default); - // public T Single(); - // public Task SingleAsync(CancellationToken cancel = default); + public T Single(); + public Task SingleAsync(CancellationToken cancel = default); - // public T Single(Expression> predicate); - // public Task SingleAsync(Expression> predicate, CancellationToken cancel = default); + public T Single(Expression> predicate); + public Task SingleAsync(Expression> predicate, CancellationToken cancel = default); - // public T SingleOrDefault(); - // public Task SingleOrDefaultAsync(CancellationToken cancel = default); + public T SingleOrDefault(); + public Task SingleOrDefaultAsync(CancellationToken cancel = default); - // public T SingleOrDefault(Expression> predicate); - // public Task SingleOrDefaultAsync(Expression> predicate, CancellationToken cancel = default); + public T SingleOrDefault(Expression> predicate); + public Task SingleOrDefaultAsync(Expression> predicate, CancellationToken cancel = default); public int Sum(Expression> selector); public Task SumAsync(Expression> selector, CancellationToken cancel = default); diff --git a/Fauna/Linq/QuerySourceDsl.cs b/Fauna/Linq/QuerySourceDsl.cs index 1889f7b5..e2e66b0a 100644 --- a/Fauna/Linq/QuerySourceDsl.cs +++ b/Fauna/Linq/QuerySourceDsl.cs @@ -196,6 +196,28 @@ private Pipeline MinImpl(Expression>? selector, [CallerMemberName] ety: typeof(R)); } + public T Single() => Execute(SingleImpl(null)); + public Task SingleAsync(CancellationToken cancel = default) => ExecuteAsync(SingleImpl(null), cancel); + public T Single(Expression> predicate) => Execute(SingleImpl(predicate)); + public Task SingleAsync(Expression> predicate, CancellationToken cancel = default) => + ExecuteAsync(SingleImpl(predicate), cancel); + private Pipeline SingleImpl(Expression>? predicate) => + CopyPipeline( + mode: PipelineMode.Scalar, + q: QH.MethodCall(AbortIfEmpty(Singularize(MaybeWhereCall(Query, predicate))), "first")); + + public T SingleOrDefault() => Execute(SingleOrDefaultImpl(null)); + public Task SingleOrDefaultAsync(CancellationToken cancel = default) => ExecuteAsync(SingleOrDefaultImpl(null), cancel); + public T SingleOrDefault(Expression> predicate) => Execute(SingleOrDefaultImpl(predicate)); + public Task SingleOrDefaultAsync(Expression> predicate, CancellationToken cancel = default) => + ExecuteAsync(SingleOrDefaultImpl(predicate), cancel); + private Pipeline SingleOrDefaultImpl(Expression>? predicate) => + CopyPipeline( + mode: PipelineMode.Scalar, + q: QH.MethodCall(Singularize(MaybeWhereCall(Query, predicate)), "first"), + ety: typeof(T), + enull: true); + private static readonly Query _sumReducer = QH.Expr("(a, b) => a + b"); public int Sum(Expression> selector) => Execute(SumImpl(selector)); @@ -295,7 +317,17 @@ private Pipeline CopyPipeline( // value is a string. Work around it by using an array. // FIXME(matt) remove workaround and use a string private Query AbortIfEmpty(Query setq) => - QH.Expr("({ let s = (").Concat(setq).Concat("); if (s.isEmpty()) abort(['empty']); s })"); + QH.Expr(@"({ let s = (").Concat(setq).Concat(@") + if (s.isEmpty()) abort(['empty']) + s + })"); + + private Query Singularize(Query setq) => + QH.Expr(@"({ + let s = (").Concat(setq).Concat(@").take(2).toArray() + if (s.length > 1) abort(['not single']) + s.take(1) + })"); private Exception TranslateException(Exception ex) => ex switch