Skip to content

Commit

Permalink
Merge pull request #104 from fauna/single
Browse files Browse the repository at this point in the history
Add LINQ Single methods
  • Loading branch information
pnwpedro authored Feb 21, 2024
2 parents b64ef28 + 8d082ab commit c2a4e4e
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 69 deletions.
47 changes: 47 additions & 0 deletions Fauna.Test/Linq/Query.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,53 @@ public async Task Query_Reverse()
Assert.AreEqual(new List<string> { "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()
{
Expand Down
16 changes: 8 additions & 8 deletions Fauna/Linq/IQuerySource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,17 +110,17 @@ public interface IQuerySource<T> : IQuerySource
public R Min<R>(Expression<Func<T, R>> selector);
public Task<R> MinAsync<R>(Expression<Func<T, R>> selector, CancellationToken cancel = default);

// public T Single();
// public Task<T> SingleAsync(CancellationToken cancel = default);
public T Single();
public Task<T> SingleAsync(CancellationToken cancel = default);

// public T Single(Expression<Func<T, bool>> predicate);
// public Task<T> SingleAsync(Expression<Func<T, bool>> predicate, CancellationToken cancel = default);
public T Single(Expression<Func<T, bool>> predicate);
public Task<T> SingleAsync(Expression<Func<T, bool>> predicate, CancellationToken cancel = default);

// public T SingleOrDefault();
// public Task<T> SingleOrDefaultAsync(CancellationToken cancel = default);
public T SingleOrDefault();
public Task<T> SingleOrDefaultAsync(CancellationToken cancel = default);

// public T SingleOrDefault(Expression<Func<T, bool>> predicate);
// public Task<T> SingleOrDefaultAsync(Expression<Func<T, bool>> predicate, CancellationToken cancel = default);
public T SingleOrDefault(Expression<Func<T, bool>> predicate);
public Task<T> SingleOrDefaultAsync(Expression<Func<T, bool>> predicate, CancellationToken cancel = default);

public int Sum(Expression<Func<T, int>> selector);
public Task<int> SumAsync(Expression<Func<T, int>> selector, CancellationToken cancel = default);
Expand Down
23 changes: 0 additions & 23 deletions Fauna/Linq/QuerySource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@ internal void SetQuery<TElem>(Query query)
{
Pipeline = new Pipeline(PipelineMode.Query, query, typeof(TElem), false, null, null);
}

// DSL Helpers

internal abstract TResult Execute<TResult>(Pipeline pl);

internal abstract Task<TResult> ExecuteAsync<TResult>(Pipeline pl, CancellationToken cancel = default);
}

public partial class QuerySource<T> : QuerySource, IQuerySource<T>
Expand All @@ -39,23 +33,6 @@ internal QuerySource(DataContext ctx, Pipeline pl)
// constructors, so they use this base one.
internal QuerySource() { }

internal override TResult Execute<TResult>(Pipeline pl)
{
try
{
var res = ExecuteAsync<TResult>(pl);
res.Wait();
return res.Result;
}
catch (AggregateException ex)
{
throw ex.InnerExceptions.First();
}
}

internal override Task<TResult> ExecuteAsync<TResult>(Pipeline pl, CancellationToken cancel = default) =>
pl.GetExec(Ctx).Result<TResult>(queryOptions: null, cancel: cancel);

public IAsyncEnumerable<Page<T>> PaginateAsync(QueryOptions? queryOptions = null, CancellationToken cancel = default)
{
var pe = Pipeline.GetExec(Ctx);
Expand Down
113 changes: 75 additions & 38 deletions Fauna/Linq/QuerySourceDsl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,12 @@ private Pipeline CountImpl(Expression<Func<T, bool>>? predicate) =>
q: QH.MethodCall(MaybeWhereCall(Query, predicate), "count"),
ety: typeof(int));

public T First() => ExecuteMaybeEmpty<T>(FirstImpl(null));
public T First() => Execute<T>(FirstImpl(null));
public Task<T> FirstAsync(CancellationToken cancel = default) =>
ExecuteMaybeEmptyAsync<T>(FirstImpl(null), cancel);
public T First(Expression<Func<T, bool>> predicate) => ExecuteMaybeEmpty<T>(FirstImpl(predicate));
ExecuteAsync<T>(FirstImpl(null), cancel);
public T First(Expression<Func<T, bool>> predicate) => Execute<T>(FirstImpl(predicate));
public Task<T> FirstAsync(Expression<Func<T, bool>> predicate, CancellationToken cancel = default) =>
ExecuteMaybeEmptyAsync<T>(FirstImpl(predicate), cancel);
ExecuteAsync<T>(FirstImpl(predicate), cancel);
private Pipeline FirstImpl(Expression<Func<T, bool>>? predicate) =>
CopyPipeline(
mode: PipelineMode.Scalar,
Expand All @@ -127,12 +127,12 @@ private Pipeline FirstOrDefaultImpl(Expression<Func<T, bool>>? predicate) =>
ety: typeof(T),
enull: true);

public T Last() => ExecuteMaybeEmpty<T>(LastImpl(null));
public T Last() => Execute<T>(LastImpl(null));
public Task<T> LastAsync(CancellationToken cancel = default) =>
ExecuteMaybeEmptyAsync<T>(LastImpl(null), cancel);
public T Last(Expression<Func<T, bool>> predicate) => ExecuteMaybeEmpty<T>(LastImpl(predicate));
ExecuteAsync<T>(LastImpl(null), cancel);
public T Last(Expression<Func<T, bool>> predicate) => Execute<T>(LastImpl(predicate));
public Task<T> LastAsync(Expression<Func<T, bool>> predicate, CancellationToken cancel = default) =>
ExecuteMaybeEmptyAsync<T>(LastImpl(predicate), cancel);
ExecuteAsync<T>(LastImpl(predicate), cancel);
private Pipeline LastImpl(Expression<Func<T, bool>>? predicate) =>
CopyPipeline(
mode: PipelineMode.Scalar,
Expand Down Expand Up @@ -165,15 +165,15 @@ private Pipeline LongCountImpl(Expression<Func<T, bool>>? predicate) =>

private static readonly Query _maxReducer = QH.Expr("(a, b) => if (a >= b) a else b");

public T Max() => ExecuteMaybeEmpty<T>(MaxImpl<T>(null));
public T Max() => Execute<T>(MaxImpl<T>(null));
public Task<T> MaxAsync(CancellationToken cancel = default) =>
ExecuteMaybeEmptyAsync<T>(MaxImpl<T>(null), cancel);
public R Max<R>(Expression<Func<T, R>> selector) => ExecuteMaybeEmpty<R>(MaxImpl(selector));
ExecuteAsync<T>(MaxImpl<T>(null), cancel);
public R Max<R>(Expression<Func<T, R>> selector) => Execute<R>(MaxImpl(selector));
public Task<R> MaxAsync<R>(Expression<Func<T, R>> selector, CancellationToken cancel = default) =>
ExecuteMaybeEmptyAsync<R>(MaxImpl(selector), cancel);
private Pipeline MaxImpl<R>(Expression<Func<T, R>>? selector)
ExecuteAsync<R>(MaxImpl(selector), cancel);
private Pipeline MaxImpl<R>(Expression<Func<T, R>>? selector, [CallerMemberName] string callerName = "")
{
RequireQueryMode("Max");
RequireQueryMode(callerName);
return CopyPipeline(
mode: PipelineMode.Scalar,
q: QH.MethodCall(MaybeMap(AbortIfEmpty(Query), selector), "reduce", _maxReducer),
Expand All @@ -182,20 +182,42 @@ private Pipeline MaxImpl<R>(Expression<Func<T, R>>? selector)

private static readonly Query _minReducer = QH.Expr("(a, b) => if (a <= b) a else b");

public T Min() => ExecuteMaybeEmpty<T>(MinImpl<T>(null));
public Task<T> MinAsync(CancellationToken cancel = default) => ExecuteMaybeEmptyAsync<T>(MinImpl<T>(null), cancel);
public R Min<R>(Expression<Func<T, R>> selector) => ExecuteMaybeEmpty<R>(MinImpl(selector));
public T Min() => Execute<T>(MinImpl<T>(null));
public Task<T> MinAsync(CancellationToken cancel = default) => ExecuteAsync<T>(MinImpl<T>(null), cancel);
public R Min<R>(Expression<Func<T, R>> selector) => Execute<R>(MinImpl(selector));
public Task<R> MinAsync<R>(Expression<Func<T, R>> selector, CancellationToken cancel = default) =>
ExecuteMaybeEmptyAsync<R>(MinImpl(selector), cancel);
private Pipeline MinImpl<R>(Expression<Func<T, R>>? selector)
ExecuteAsync<R>(MinImpl(selector), cancel);
private Pipeline MinImpl<R>(Expression<Func<T, R>>? selector, [CallerMemberName] string callerName = "")
{
RequireQueryMode("Min");
RequireQueryMode(callerName);
return CopyPipeline(
mode: PipelineMode.Scalar,
q: QH.MethodCall(MaybeMap(AbortIfEmpty(Query), selector), "reduce", _minReducer),
ety: typeof(R));
}

public T Single() => Execute<T>(SingleImpl(null));
public Task<T> SingleAsync(CancellationToken cancel = default) => ExecuteAsync<T>(SingleImpl(null), cancel);
public T Single(Expression<Func<T, bool>> predicate) => Execute<T>(SingleImpl(predicate));
public Task<T> SingleAsync(Expression<Func<T, bool>> predicate, CancellationToken cancel = default) =>
ExecuteAsync<T>(SingleImpl(predicate), cancel);
private Pipeline SingleImpl(Expression<Func<T, bool>>? predicate) =>
CopyPipeline(
mode: PipelineMode.Scalar,
q: QH.MethodCall(AbortIfEmpty(Singularize(MaybeWhereCall(Query, predicate))), "first"));

public T SingleOrDefault() => Execute<T>(SingleOrDefaultImpl(null));
public Task<T> SingleOrDefaultAsync(CancellationToken cancel = default) => ExecuteAsync<T>(SingleOrDefaultImpl(null), cancel);
public T SingleOrDefault(Expression<Func<T, bool>> predicate) => Execute<T>(SingleOrDefaultImpl(predicate));
public Task<T> SingleOrDefaultAsync(Expression<Func<T, bool>> predicate, CancellationToken cancel = default) =>
ExecuteAsync<T>(SingleOrDefaultImpl(predicate), cancel);
private Pipeline SingleOrDefaultImpl(Expression<Func<T, bool>>? 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<Func<T, int>> selector) => Execute<int>(SumImpl<int>(selector));
Expand Down Expand Up @@ -232,34 +254,29 @@ private void RequireQueryMode([CallerMemberName] string callerName = "")
}
}

private R ExecuteMaybeEmpty<R>(Pipeline pl)
private R Execute<R>(Pipeline pl)
{
try
{
return Execute<R>(pl);
var res = ExecuteAsync<R>(pl);
res.Wait();
return res.Result;
}
catch (AbortException ex)
catch (AggregateException ex)
{
throw TranslateEmptySetAbort(ex);
throw TranslateException(ex.InnerExceptions.First());
}
}

private async Task<R> ExecuteMaybeEmptyAsync<R>(Pipeline pl, CancellationToken cancel)
private async Task<R> ExecuteAsync<R>(Pipeline pl, CancellationToken cancel = default)
{
try
{
return await ExecuteAsync<R>(pl, cancel);
return await pl.GetExec(Ctx).Result<R>(queryOptions: null, cancel: cancel);
}
catch (AggregateException ex)
{
if (ex.InnerExceptions.First() is AbortException aex)
{
throw TranslateEmptySetAbort(aex);
}
else
{
throw;
}
throw TranslateException(ex.InnerExceptions.First());
}
}

Expand Down Expand Up @@ -300,10 +317,30 @@ 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 })");

private Exception TranslateEmptySetAbort(AbortException ex) =>
ex.GetData<List<string>>()?.First() == "empty set" ? new InvalidOperationException("Empty set") : ex;
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
{
AbortException aex =>
aex.GetData<List<string>>()?.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);
Expand Down

0 comments on commit c2a4e4e

Please sign in to comment.