From c1eb8063232ed3384c2993d6d21bd768885a6ee9 Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Mon, 11 Mar 2024 14:53:12 -0300 Subject: [PATCH] groups! --- .../Examples/QueryBuilder.cs | 97 +++++++++++ .../Models/DataTypes/Group.cs | 73 +++++---- .../Builders/GroupBuilder.cs | 62 ------- .../Builders/ShapeBuilder.cs | 2 +- .../Compiled/DebugCompiledQuery.cs | 9 +- src/EdgeDB.Net.QueryBuilder/EdgeQL.cs | 6 + .../Extensions/GroupingExtensions.cs | 26 +++ src/EdgeDB.Net.QueryBuilder/Grammar/Terms.cs | 6 +- src/EdgeDB.Net.QueryBuilder/GroupContext.cs | 7 + .../Interfaces/IGroupable.cs | 18 --- .../Interfaces/IQueryBuilder.cs | 24 ++- .../Interfaces/Queries/IDeleteQuery.cs | 6 +- .../Interfaces/Queries/IGroupQuery.cs | 18 ++- .../Interfaces/Queries/IInsertQuery.cs | 4 +- .../Interfaces/Queries/ISelectQuery.cs | 6 +- .../Interfaces/Queries/IUpdateQuery.cs | 2 +- .../InsertChildren/IUnlessConflictOn.cs | 2 +- .../Lexical/QueryWriter.cs | 6 + src/EdgeDB.Net.QueryBuilder/QueryBuilder.cs | 152 +++++++++++------- .../QueryBuilder/QueryBuilder.Delete.cs | 2 +- .../QueryBuilder/QueryBuilder.Group.cs | 75 +++++++-- .../QueryBuilder/QueryBuilder.Insert.cs | 16 +- .../QueryBuilder/QueryBuilder.Select.cs | 16 +- .../QueryBuilder/QueryBuilder.Update.cs | 28 ++-- .../QueryBuilder/QueryBuilder.With.cs | 48 +++--- .../QueryBuilderState.cs | 15 ++ src/EdgeDB.Net.QueryBuilder/QueryContext.cs | 134 ++++++++------- .../QueryNodes/Contexts/GroupContext.cs | 10 +- .../QueryNodes/ForNode.cs | 2 +- .../QueryNodes/GroupNode.cs | 43 ++++- .../QueryNodes/QueryNode.cs | 8 +- .../Expressions/ExpressionContext.cs | 21 ++- .../Expressions/InitializationTranslator.cs | 30 ++-- .../Expressions/MemberExpressionTranslator.cs | 11 +- .../MethodCallExpressionTranslator.cs | 12 +- .../Methods/EnumerableMethodTranslator.cs | 10 +- .../Methods/GroupMethodTranslator.cs | 18 +++ .../Translators/Methods/MethodTranslator.cs | 37 +---- .../Methods/TranslatedParameter.cs | 16 +- .../Methods/TupleMethodTranslators.cs | 22 +++ .../Utils/QueryGenerationUtils.cs | 10 +- 41 files changed, 706 insertions(+), 404 deletions(-) delete mode 100644 src/EdgeDB.Net.QueryBuilder/Builders/GroupBuilder.cs create mode 100644 src/EdgeDB.Net.QueryBuilder/Extensions/GroupingExtensions.cs create mode 100644 src/EdgeDB.Net.QueryBuilder/GroupContext.cs delete mode 100644 src/EdgeDB.Net.QueryBuilder/Interfaces/IGroupable.cs create mode 100644 src/EdgeDB.Net.QueryBuilder/QueryBuilderState.cs create mode 100644 src/EdgeDB.Net.QueryBuilder/Translators/Methods/GroupMethodTranslator.cs create mode 100644 src/EdgeDB.Net.QueryBuilder/Translators/Methods/TupleMethodTranslators.cs diff --git a/examples/EdgeDB.Examples.CSharp/Examples/QueryBuilder.cs b/examples/EdgeDB.Examples.CSharp/Examples/QueryBuilder.cs index 98bf8468..916c6ae2 100644 --- a/examples/EdgeDB.Examples.CSharp/Examples/QueryBuilder.cs +++ b/examples/EdgeDB.Examples.CSharp/Examples/QueryBuilder.cs @@ -156,6 +156,103 @@ private static async Task QueryBuilderDemo(EdgeDBClient client) .Delete() .Filter(x => EdgeQL.ILike(x.Name, "e%")) .Compile(true); + + // grouping + query = QueryBuilder + .Group() + .By(x => x.Name) + .Compile(true); + + // grouping by expressions + query = QueryBuilder + .Group() + .Using(person => new {StartsWithVowel = Regex.Matches(person.Name!, "(?i)^[aeiou]")}) + .By(ctx => ctx.Using.StartsWithVowel) + .Compile(true); + + // grouping by scalars + query = QueryBuilder + .With(ctx => new {People = ctx.SubQuerySingle(QueryBuilder.Select())}) + .Group(ctx => ctx.Variables.People.Name) + .Using(name => new {StartsWithVowel = Regex.IsMatch(name!, "(?i)^[aeiou]")}) + .By(group => group.Using.StartsWithVowel) + .Compile(true); + + // grouping result as select + query = QueryBuilder + .With(ctx => new + { + People = ctx.SubQuerySingle(QueryBuilder.Select()), + Groups = ctx.SubQuerySingle( + QueryBuilder + .Group(ctx => ctx.Local("People")) + .Using(person => new {Vowel = Regex.IsMatch(person.Name!, "(?i)^[aeiou]")}) + .By(ctx => ctx.Using.Vowel) + ) + }) + .SelectExpression(ctx => ctx.Variables.Groups, shape => shape + .Computeds((ctx, group) => new + { + StartsWithVowel = group.Key, + Count = EdgeQL.Count(group.Elements), + NameLength = EdgeQL.Len(ctx.Ref(group.Elements).Name!) + }) + ) + .Compile(true); + + // grouping by more than 1 parameter + query = QueryBuilder + .With(ctx => new + { + People = ctx.SubQuerySingle(QueryBuilder.Select()), + Groups = ctx.SubQuerySingle( + QueryBuilder + .Group(ctx => ctx.Local("People")) + .Using(person => new + { + Vowel = Regex.IsMatch(person.Name!, "(?i)^[aeiou]"), + NameLength = person.Name!.Length + }) + .By(ctx => Tuple.Create(ctx.Using.Vowel, ctx.Using.NameLength)) + ) + }) + .SelectExpression(ctx => ctx.Variables.Groups, shape => shape + .Computeds((ctx, group) => new + { + StartsWithVowel = group.Key, + Count = EdgeQL.Count(group.Elements), + NameLength = EdgeQL.Len(ctx.Ref(group.Elements).Name!) + }) + ) + .Compile(true); + + // grouping by grouping sets + query = QueryBuilder + .With(ctx => new + { + People = ctx.SubQuerySingle(QueryBuilder.Select()), + Groups = ctx.SubQuerySingle( + QueryBuilder + .Group(ctx => ctx.Local("People")) + .Using(person => new + { + Vowel = Regex.IsMatch(person.Name!, "(?i)^[aeiou]"), + NameLength = person.Name!.Length + }) + .By(ctx => EdgeQL.Cube(new { ctx.Using.Vowel, ctx.Using.NameLength })) + ) + }) + .SelectExpression(ctx => ctx.Variables.Groups, shape => shape + .Computeds((ctx, group) => new + { + group.Key, + group.Grouping, + Count = EdgeQL.Count(group.Elements), + NameLength = EdgeQL.Len(ctx.Ref(group.Elements).Name!) + }) + ) + .OrderBy(x => EdgeQL.ArrayAgg(x.Grouping)) + .Compile(true); } } } diff --git a/src/EdgeDB.Net.Driver/Models/DataTypes/Group.cs b/src/EdgeDB.Net.Driver/Models/DataTypes/Group.cs index b54261d7..594ffa09 100644 --- a/src/EdgeDB.Net.Driver/Models/DataTypes/Group.cs +++ b/src/EdgeDB.Net.Driver/Models/DataTypes/Group.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary; using EdgeDB.DataTypes; using System.Collections; using System.Collections.Immutable; @@ -12,6 +13,30 @@ namespace EdgeDB; /// The type of the elements. public sealed class Group : IGrouping { + static Group() + { + // precache the deserializer, in case theres issues with it, this will throw before the query is run. + if(TypeBuilder.IsValidObjectType(typeof(TElement))) + TypeBuilder.GetDeserializationFactory(typeof(TElement)); + } + /// + /// Gets the key used to group the set of . + /// + [EdgeDBIgnore] + public TKey Key { get; } + + /// + /// Gets a collection of all the names of the parameters used as the key for this particular subset. + /// + [EdgeDBProperty("grouping")] + public IReadOnlyCollection Grouping { get; } + + /// + /// Gets a collection of elements that have the same key as . + /// + [EdgeDBProperty("elements")] + public IReadOnlyCollection Elements { get; } + /// /// Constructs a new grouping. /// @@ -34,25 +59,11 @@ internal Group(IDictionary raw) Grouping = ((string[])groupingValue!).ToImmutableArray(); Key = BuildKey((IDictionary)keyValue!); - throw new NotImplementedException("TODO"); - //Elements = ((IDictionary[])elementsValue!).Select(x => (TElement)TypeBuilder.BuildObject(typeof(TElement), x)!).ToImmutableArray(); + Elements = elementsValue is null + ? Array.Empty() + : ((object?[])elementsValue).Cast().ToImmutableArray(); } - /// - /// Gets the name of the property that was grouped by. - /// - public IReadOnlyCollection Grouping { get; } - - /// - /// Gets a collection of elements that have the same key as . - /// - public IReadOnlyCollection Elements { get; } - - /// - /// Gets the key used to group the set of . - /// - public TKey Key { get; } - /// public IEnumerator GetEnumerator() => Elements.GetEnumerator(); @@ -61,26 +72,22 @@ public IEnumerator GetEnumerator() IEnumerator IEnumerable.GetEnumerator() => Elements.GetEnumerator(); - private static TKey BuildKey(IDictionary value) + private static TKey BuildKey(object? value) { - if (typeof(TKey).IsAssignableTo(typeof(ITuple))) + switch (value) { - var types = typeof(TKey).GenericTypeArguments; - var transientTuple = new TransientTuple(types, value.Values.ToArray()); - - switch (typeof(TKey).Name.Split('`')[0]) + case TKey key: + return key; + case IDictionary {Count: 2} dict when dict.ContainsKey("id"): { - case "ValueTuple": - return (TKey)transientTuple.ToValueTuple(); - case "Tuple": - return (TKey)transientTuple.ToReferenceTuple(); - default: - throw new InvalidOperationException($"Cannot build tuple with the type of {typeof(TKey).Name}"); + var keyRaw = dict.FirstOrDefault(x => x.Key != "id"); + if (keyRaw.Value is TKey keyValue) + return keyValue; + + throw new InvalidOperationException($"Key of grouping cannot be extracted from {keyRaw.Value?.GetType()}"); } + default: + throw new InvalidOperationException($"Cannot build key with the type of {typeof(TKey).Name}"); } - - if (value.Count == 1) - return (TKey)value.First().Value!; - throw new InvalidOperationException($"Cannot build key with the type of {typeof(TKey).Name}"); } } diff --git a/src/EdgeDB.Net.QueryBuilder/Builders/GroupBuilder.cs b/src/EdgeDB.Net.QueryBuilder/Builders/GroupBuilder.cs deleted file mode 100644 index 3638bf29..00000000 --- a/src/EdgeDB.Net.QueryBuilder/Builders/GroupBuilder.cs +++ /dev/null @@ -1,62 +0,0 @@ - -/* Unmerged change from project 'EdgeDB.Net.QueryBuilder (net6.0)' -Before: -using System; -After: -using EdgeDB; -using EdgeDB.Builders; -using System; -*/ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Text; -using System.Threading.Tasks; - -namespace EdgeDB.Builders -{ - public abstract class BaseGroupBuilder - { - public Expression? UsingExpression { get; protected set; } - - public Expression? ByExpression { get; protected set; } - - internal BaseGroupBuilder() { } - internal BaseGroupBuilder(Expression @using) { UsingExpression = @using; } - } - public class GroupBuilder : BaseGroupBuilder - { - public GroupBuilder Using(Expression> @using) - => new(@using); - - public KeyedGroupBuilder By(Expression> keySelector) - => new(keySelector); - } - public class GroupBuilder : BaseGroupBuilder - { - public GroupBuilder(Expression @using) : base(@using) { } - - public KeyedContextGroupBuilder By(Expression> keySelector) - => new(keySelector, UsingExpression!); - - public KeyedContextGroupBuilder By(Expression> keySelector) - => new(keySelector, UsingExpression!); - } - public class KeyedGroupBuilder : BaseGroupBuilder - { - public KeyedGroupBuilder(Expression keySelector) - : base() - { - ByExpression = keySelector; - } - } - public class KeyedContextGroupBuilder : KeyedGroupBuilder - { - public KeyedContextGroupBuilder(Expression keySelector, Expression @using) - : base(keySelector) - { - UsingExpression = @using; - } - } -} diff --git a/src/EdgeDB.Net.QueryBuilder/Builders/ShapeBuilder.cs b/src/EdgeDB.Net.QueryBuilder/Builders/ShapeBuilder.cs index eebd74dd..7d4cc30c 100644 --- a/src/EdgeDB.Net.QueryBuilder/Builders/ShapeBuilder.cs +++ b/src/EdgeDB.Net.QueryBuilder/Builders/ShapeBuilder.cs @@ -220,7 +220,7 @@ public ShapeBuilder Computeds(Expression> computedsSele return this; } - public ShapeBuilder Computeds(Expression, T, TAnon>> computedsSelector) + public ShapeBuilder Computeds(Expression, T, TAnon>> computedsSelector) { var computeds = FlattenAnonymousExpression(SelectedType, computedsSelector); diff --git a/src/EdgeDB.Net.QueryBuilder/Compiled/DebugCompiledQuery.cs b/src/EdgeDB.Net.QueryBuilder/Compiled/DebugCompiledQuery.cs index b913fb72..f10798f5 100644 --- a/src/EdgeDB.Net.QueryBuilder/Compiled/DebugCompiledQuery.cs +++ b/src/EdgeDB.Net.QueryBuilder/Compiled/DebugCompiledQuery.cs @@ -100,17 +100,17 @@ private static string CreateDebugText(string query, Dictionary desc += $": {column.Marker.DebugText.Get()}"; - if (desc.Length - 3 > size) + if (desc.Length > size) { desc = $"{icon} [{column.Marker.Type}] {column.Name}"; } - if (desc.Length - 3 > size) + if (desc.Length > size) { desc = $"{icon} {column.Name}"; } - if (desc.Length - 3 >= size) + if (desc.Length >= size) { desc = icon; } @@ -178,8 +178,7 @@ private static string CreateDebugText(string query, Dictionary private static List> CreateMarkerView(LinkedList spans) { - var orderedTemp = spans.OrderBy(x => x.Range.End.Value - x.Range.Start.Value).ToList(); - var ordered = new Queue(orderedTemp); // order by 'size' + var ordered = new Queue(spans.OrderBy(x => x.Range.End.Value - x.Range.Start.Value)); // order by 'size' var result = new List>(); var row = new List(); diff --git a/src/EdgeDB.Net.QueryBuilder/EdgeQL.cs b/src/EdgeDB.Net.QueryBuilder/EdgeQL.cs index 1ec46648..3d7890f9 100644 --- a/src/EdgeDB.Net.QueryBuilder/EdgeQL.cs +++ b/src/EdgeDB.Net.QueryBuilder/EdgeQL.cs @@ -4,6 +4,12 @@ namespace EdgeDB { public sealed partial class EdgeQL { + public static T Rollup(T value) + => default!; + + public static T Cube(T value) + => default!; + public static JsonReferenceVariable AsJson(T value) => new(value); public static long Count(IMultiCardinalityExecutable a) { return default!; } diff --git a/src/EdgeDB.Net.QueryBuilder/Extensions/GroupingExtensions.cs b/src/EdgeDB.Net.QueryBuilder/Extensions/GroupingExtensions.cs new file mode 100644 index 00000000..ad65e8b9 --- /dev/null +++ b/src/EdgeDB.Net.QueryBuilder/Extensions/GroupingExtensions.cs @@ -0,0 +1,26 @@ +using EdgeDB.Interfaces.Queries; +using System.Linq.Expressions; + +namespace EdgeDB; + +public static class QueryBuilderGroupingExtensions +{ + // public static IGroupUsingQuery> Using( + // this IGroupQuery> query, Expression> expression) + // { + // return query.UsingInternal>(expression); + // } + // + // public static IGroupUsingQuery> Using( + // this IGroupQuery> query, Expression> expression) + // { + // return query.UsingInternal>(expression); + // } + + // public static IGroupUsingQuery> Using( + // this IGroupQuery query, Expression> expression + // ) where TOldContext : QueryContext + // { + // + // } +} diff --git a/src/EdgeDB.Net.QueryBuilder/Grammar/Terms.cs b/src/EdgeDB.Net.QueryBuilder/Grammar/Terms.cs index accae253..b1afc4af 100644 --- a/src/EdgeDB.Net.QueryBuilder/Grammar/Terms.cs +++ b/src/EdgeDB.Net.QueryBuilder/Grammar/Terms.cs @@ -9,12 +9,14 @@ public static QueryWriter LabelVerbose(this QueryWriter writer, string name, Def => writer.Marker(MarkerType.Verbose, name, debug, values); #endregion - public static QueryWriter Wrapped(this QueryWriter writer, Value value, string separator = "()") + public static QueryWriter Wrapped(this QueryWriter writer, Value value, string separator = "()", bool spaced = false) { if (separator.Length != 2) throw new ArgumentOutOfRangeException(nameof(separator)); - return writer.Append(separator[0], value, separator[1]); + return spaced ? + writer.Append(separator[0], ' ', value, ' ', separator[1]) : + writer.Append(separator[0], value, separator[1]); } public static QueryWriter Wrapped(this QueryWriter writer, WriterProxy value, string separator = "()") diff --git a/src/EdgeDB.Net.QueryBuilder/GroupContext.cs b/src/EdgeDB.Net.QueryBuilder/GroupContext.cs new file mode 100644 index 00000000..3b40f2d4 --- /dev/null +++ b/src/EdgeDB.Net.QueryBuilder/GroupContext.cs @@ -0,0 +1,7 @@ +namespace EdgeDB; + +public abstract class GroupContext : IQueryContextUsing where TContext : IQueryContext +{ + public abstract TUsing Using { get; } + public abstract TContext Context { get; } +} diff --git a/src/EdgeDB.Net.QueryBuilder/Interfaces/IGroupable.cs b/src/EdgeDB.Net.QueryBuilder/Interfaces/IGroupable.cs deleted file mode 100644 index 638bff99..00000000 --- a/src/EdgeDB.Net.QueryBuilder/Interfaces/IGroupable.cs +++ /dev/null @@ -1,18 +0,0 @@ -using EdgeDB.Builders; -using EdgeDB.Interfaces.Queries; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Text; -using System.Threading.Tasks; - -namespace EdgeDB.Interfaces -{ - public interface IGroupable - { - IGroupQuery> GroupBy(Expression> propertySelector); - - IGroupQuery> Group(Expression>> groupBuilder); - } -} diff --git a/src/EdgeDB.Net.QueryBuilder/Interfaces/IQueryBuilder.cs b/src/EdgeDB.Net.QueryBuilder/Interfaces/IQueryBuilder.cs index 9c7ac397..6052ae8b 100644 --- a/src/EdgeDB.Net.QueryBuilder/Interfaces/IQueryBuilder.cs +++ b/src/EdgeDB.Net.QueryBuilder/Interfaces/IQueryBuilder.cs @@ -20,9 +20,19 @@ public interface IQueryBuilder : IDeleteQuery, IInsertQuery, IUnlessConflictOn, - IGroupQuery, - IGroupable + IGroupQuery, + IGroupUsingQuery + where TContext : IQueryContext { + IGroupQuery Group(); + + IGroupQuery Group(Action> shape); + + IGroupQuery Group(Expression> selector); + IGroupQuery Group(Expression> selector); + IGroupQuery Group(Expression> selector, Action> shape); + IGroupQuery Group(Expression> selector, Action> shape); + /// /// Adds a FOR statement on the with a UNION /// whos inner query is the . @@ -42,7 +52,7 @@ public interface IQueryBuilder : /// /// The current query. /// - IQueryBuilder> With(TVariables variables); + IQueryBuilder> With(TVariables variables); /// /// Adds a WITH statement whos variables are the properties defined in . @@ -54,7 +64,7 @@ public interface IQueryBuilder : /// /// The current query. /// - IQueryBuilder> With(Expression, TVariables>> variables); + IQueryBuilder> With(Expression, TVariables>> variables); /// /// Adds a SELECT statement selecting the current with an @@ -105,10 +115,10 @@ public interface IQueryBuilder : /// /// A . /// - ISelectQuery SelectExpression( - Expression> expression, + ISelectQuery SelectExpression( + Expression> expression, Action>? shape = null - ) where TQuery : ISingleCardinalityExecutable; + ); /// /// Adds a INSERT statement inserting an instance of . diff --git a/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/IDeleteQuery.cs b/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/IDeleteQuery.cs index a827ed93..85514f93 100644 --- a/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/IDeleteQuery.cs +++ b/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/IDeleteQuery.cs @@ -12,7 +12,7 @@ namespace EdgeDB.Interfaces.Queries /// /// The type which this DELETE query is querying against. /// The type of context representing the current builder. - public interface IDeleteQuery : IGroupable, IMultiCardinalityExecutable + public interface IDeleteQuery : IMultiCardinalityExecutable where TContext : IQueryContext { /// /// Filters the current delete query by the given predicate. @@ -31,7 +31,7 @@ public interface IDeleteQuery : IGroupable, IMultiCardin /// The order of which null values should occor. /// The current query. IDeleteQuery OrderBy(Expression> propertySelector, OrderByNullPlacement? nullPlacement = null); - + /// IDeleteQuery OrderBy(Expression> propertySelector, OrderByNullPlacement? nullPlacement = null); @@ -42,7 +42,7 @@ public interface IDeleteQuery : IGroupable, IMultiCardin /// The order of which null values should occor. /// The current query. IDeleteQuery OrderByDesending(Expression> propertySelector, OrderByNullPlacement? nullPlacement = null); - + /// IDeleteQuery OrderByDesending(Expression> propertySelector, OrderByNullPlacement? nullPlacement = null); diff --git a/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/IGroupQuery.cs b/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/IGroupQuery.cs index f741d727..60fc0ef6 100644 --- a/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/IGroupQuery.cs +++ b/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/IGroupQuery.cs @@ -1,13 +1,27 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Text; using System.Threading.Tasks; namespace EdgeDB.Interfaces.Queries { - public interface IGroupQuery : IMultiCardinalityExecutable + public interface IGroupQuery where TContext : IQueryContext { - + IMultiCardinalityExecutable> By(Expression> selector); + + IMultiCardinalityExecutable> By(Expression> selector); + + IGroupUsingQuery> Using( + Expression> expression); + + IGroupUsingQuery> Using( + Expression> expression); + } + + public interface IGroupUsingQuery + { + IMultiCardinalityExecutable> By(Expression> selector); } } diff --git a/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/IInsertQuery.cs b/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/IInsertQuery.cs index 14ae898b..aaa80501 100644 --- a/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/IInsertQuery.cs +++ b/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/IInsertQuery.cs @@ -12,7 +12,7 @@ namespace EdgeDB.Interfaces.Queries /// /// The type which this INSERT query is querying against. /// The type of context representing the current builder. - public interface IInsertQuery : ISingleCardinalityExecutable + public interface IInsertQuery : ISingleCardinalityExecutable where TContext : IQueryContext { /// /// Automatically adds an UNLESS CONFLICT ON ... statement to the current insert @@ -33,7 +33,7 @@ public interface IInsertQuery : ISingleCardinalityExecutable /// The current query. IUnlessConflictOn UnlessConflictOn(Expression> propertySelector); - + /// IUnlessConflictOn UnlessConflictOn(Expression> propertySelector); } diff --git a/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/ISelectQuery.cs b/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/ISelectQuery.cs index b0e683c1..6c0eb2a0 100644 --- a/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/ISelectQuery.cs +++ b/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/ISelectQuery.cs @@ -12,7 +12,7 @@ namespace EdgeDB.Interfaces.Queries /// /// The type which this SELECT query is querying against. /// The type of context representing the current builder. - public interface ISelectQuery : IGroupable, IMultiCardinalityExecutable + public interface ISelectQuery : IMultiCardinalityExecutable where TContext : IQueryContext { /// /// Filters the current select query by the given predicate. @@ -31,7 +31,7 @@ public interface ISelectQuery : IGroupable, IMultiCardin /// The order of which null values should occor. /// The current query. ISelectQuery OrderBy(Expression> propertySelector, OrderByNullPlacement? nullPlacement = null); - + /// ISelectQuery OrderBy(Expression> propertySelector, OrderByNullPlacement? nullPlacement = null); @@ -42,7 +42,7 @@ public interface ISelectQuery : IGroupable, IMultiCardin /// The order of which null values should occor. /// The current query. ISelectQuery OrderByDesending(Expression> propertySelector, OrderByNullPlacement? nullPlacement = null); - + /// ISelectQuery OrderByDesending(Expression> propertySelector, OrderByNullPlacement? nullPlacement = null); diff --git a/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/IUpdateQuery.cs b/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/IUpdateQuery.cs index 1c2274b6..fb7be579 100644 --- a/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/IUpdateQuery.cs +++ b/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/IUpdateQuery.cs @@ -12,7 +12,7 @@ namespace EdgeDB.Interfaces.Queries /// /// The type which this UPDATE query is querying against. /// The type of context representing the current builder. - public interface IUpdateQuery : IGroupable, IMultiCardinalityExecutable + public interface IUpdateQuery : IMultiCardinalityExecutable where TContext : IQueryContext { /// /// Filters the current update query by the given predicate. diff --git a/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/InsertChildren/IUnlessConflictOn.cs b/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/InsertChildren/IUnlessConflictOn.cs index d972a731..321cd024 100644 --- a/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/InsertChildren/IUnlessConflictOn.cs +++ b/src/EdgeDB.Net.QueryBuilder/Interfaces/Queries/InsertChildren/IUnlessConflictOn.cs @@ -11,7 +11,7 @@ namespace EdgeDB.Interfaces /// /// The type which this UNLESS CONFLICT ON query is querying against. /// The type of context representing the current builder. - public interface IUnlessConflictOn : ISingleCardinalityExecutable + public interface IUnlessConflictOn : ISingleCardinalityExecutable where TContext : IQueryContext { /// /// Adds an ELSE (SELECT ...) statment to the current query returning the conflicting object. diff --git a/src/EdgeDB.Net.QueryBuilder/Lexical/QueryWriter.cs b/src/EdgeDB.Net.QueryBuilder/Lexical/QueryWriter.cs index 2468a731..56bf4e7f 100644 --- a/src/EdgeDB.Net.QueryBuilder/Lexical/QueryWriter.cs +++ b/src/EdgeDB.Net.QueryBuilder/Lexical/QueryWriter.cs @@ -421,6 +421,12 @@ public bool TryWrite(Value value) current = current.Next; } + foreach (var remaining in activeMarkers) + { + var content = remaining.Builder.ToString(); + spans.AddLast(new QuerySpan(remaining.Index..(remaining.Index + content.Length), content, remaining.Marker, remaining.Name)); + } + return (query.ToString(), spans); } diff --git a/src/EdgeDB.Net.QueryBuilder/QueryBuilder.cs b/src/EdgeDB.Net.QueryBuilder/QueryBuilder.cs index 5c4a6868..ef42993e 100644 --- a/src/EdgeDB.Net.QueryBuilder/QueryBuilder.cs +++ b/src/EdgeDB.Net.QueryBuilder/QueryBuilder.cs @@ -20,14 +20,13 @@ namespace EdgeDB /// Represents a query builder used to build queries against . /// /// The type that this query builder is currently building queries for. - public partial class QueryBuilder : QueryBuilder> + public partial class QueryBuilder : QueryBuilder> { - public QueryBuilder() : base() { } - - internal QueryBuilder(SchemaInfo info) : base(info) { } + public QueryBuilder() + { } - internal QueryBuilder(List nodes, List globals, Dictionary variables) - : base(nodes, globals, variables) { } + internal QueryBuilder(QueryBuilderState state) + : base(state) { } } /// @@ -36,16 +35,17 @@ internal QueryBuilder(List nodes, List globals, Dictiona /// /// The type that this query builder is currently building queries for. /// The context type used for contextual expressions. - public partial class QueryBuilder : IQueryBuilder + public partial class QueryBuilder : IQueryBuilder where TContext : IQueryContext { /// public bool RequiresIntrospection - => _nodes.Any(x => x.RequiresIntrospection); + => Nodes.Any(x => x.RequiresIntrospection); /// /// A list of query nodes that make up the current query builder. /// - private readonly List _nodes; + private List Nodes + => _state.Nodes; /// /// The current user defined query node. @@ -54,17 +54,17 @@ private QueryNode? CurrentUserNode { get { - var latestNode = _nodes.LastOrDefault(x => !x.IsAutoGenerated); + var latestNode = Nodes.LastOrDefault(x => !x.IsAutoGenerated); if (latestNode is not null) return latestNode; - if (_nodes.Count == 0) + if (Nodes.Count == 0) return null; - for (int i = _nodes.Count - 1; i >= 0; i--) + for (int i = Nodes.Count - 1; i >= 0; i--) { - var n = _nodes[i]; + var n = Nodes[i]; if (n.IsAutoGenerated) { var child = n.SubNodes.FirstOrDefault(x => !x.IsAutoGenerated); @@ -80,39 +80,35 @@ private QueryNode? CurrentUserNode /// /// A list of query globals used by this query builder. /// - private readonly List _queryGlobals; + private List QueryGlobals + => _state.Globals; /// /// The current schema introspection info if it has been fetched. /// - private SchemaInfo? _schemaInfo; + private SchemaInfo? SchemaInfo { get => _state.SchemaInfo; set => _state.SchemaInfo = value; } /// - /// A dictionary of query variables used by the . + /// A dictionary of query variables used by the . /// - private readonly Dictionary _queryVariables; + private Dictionary QueryVariables + => _state.Variables; + + private readonly QueryBuilderState _state; /// /// Constructs an empty query builder. /// - public QueryBuilder() - { - _nodes = new(); - _queryGlobals = new(); - _queryVariables = new(); - } + public QueryBuilder() : this(QueryBuilderState.Empty) + {} /// /// Constructs a query builder with the given nodes, globals, and variables. /// - /// The query nodes to initialize with. - /// The query globals to initialize with. - /// The query variables to initialize with. - internal QueryBuilder(List nodes, List globals, Dictionary variables) + /// The state information for this querybuilder. + internal QueryBuilder(QueryBuilderState state) { - _nodes = nodes; - _queryGlobals = globals; - _queryVariables = variables; + _state = state; } /// @@ -122,7 +118,7 @@ internal QueryBuilder(List nodes, List globals, Dictiona internal QueryBuilder(SchemaInfo info) : this() { - _schemaInfo = info; + SchemaInfo = info; } /// @@ -131,7 +127,7 @@ internal QueryBuilder(SchemaInfo info) /// The name of the variable. /// The value of the variable. internal void AddQueryVariable(string name, object? value) - => _queryVariables[name] = value; + => QueryVariables[name] = value; /// /// Copies this query builders nodes, globals, and variables @@ -142,7 +138,12 @@ internal void AddQueryVariable(string name, object? value) /// A new with the target type. /// private QueryBuilder EnterNewType() - => new(_nodes, _queryGlobals, _queryVariables); + { + if (this is QueryBuilder s) + return s; + + return new(_state); + } /// /// Copies this query builders nodes, globals, and variables @@ -152,8 +153,14 @@ private QueryBuilder EnterNewType() /// /// A new with the target context type. /// - private QueryBuilder EnterNewContext() - => new(_nodes, _queryGlobals, _queryVariables); + internal QueryBuilder EnterNewContext() + where TNewContext : IQueryContext + { + if (this is QueryBuilder s) + return s; + + return new(_state); + } /// /// Adds a new node to this query builder. @@ -170,7 +177,7 @@ private TNode AddNode(NodeContext context, bool autoGenerated = false, Qu where TNode : QueryNode { // create a new builder for the node. - var builder = new NodeBuilder(context, _queryGlobals, _nodes, _queryVariables) + var builder = new NodeBuilder(context, QueryGlobals, Nodes, QueryVariables) { IsAutoGenerated = autoGenerated }; @@ -182,13 +189,13 @@ private TNode AddNode(NodeContext context, bool autoGenerated = false, Qu { node.SubNodes.Add(child); child.Parent = node; - _nodes.Remove(child); + Nodes.Remove(child); } // visit the node node.Visit(); - _nodes.Add(node); + Nodes.Add(node); return node; } @@ -207,21 +214,21 @@ internal CompiledQuery CompileInternal(CompileContext? context = null) CompileInternal(writer, context); - if (!context.Debug) return new CompiledQuery(writer.Compile().ToString(), _queryVariables); + if (!context.Debug) return new CompiledQuery(writer.Compile().ToString(), QueryVariables); var compiled = writer.CompileDebug(); - return new DebugCompiledQuery(compiled.Query, _queryVariables, compiled.Markers); + return new DebugCompiledQuery(compiled.Query, QueryVariables, compiled.Markers); } internal void CompileInternal(QueryWriter writer, CompileContext? context = null) { - _schemaInfo ??= context?.SchemaInfo; + SchemaInfo ??= context?.SchemaInfo; context ??= new(); List> parameters = new(); - var nodes = _nodes; + var nodes = Nodes; if (!context.IncludeAutogeneratedNodes) nodes = nodes @@ -231,7 +238,7 @@ internal void CompileInternal(QueryWriter writer, CompileContext? context = null // reference the introspection and finalize all nodes. foreach (var node in nodes) { - node.SchemaInfo ??= _schemaInfo; + node.SchemaInfo ??= SchemaInfo; context.PreFinalizerModifier?.Invoke(node); } @@ -248,16 +255,16 @@ internal void CompileInternal(QueryWriter writer, CompileContext? context = null QueryReducer.Apply(this, writer); // create a with block if we have any globals - if (context.IncludeGlobalsInQuery && _queryGlobals.Any()) + if (context.IncludeGlobalsInQuery && QueryGlobals.Any()) { var builder = new NodeBuilder(new WithContext(typeof(TType)) { - Values = _queryGlobals, - }, _queryGlobals, nodes, _queryVariables); + Values = QueryGlobals, + }, QueryGlobals, nodes, QueryVariables); var with = new WithNode(builder) { - SchemaInfo = _schemaInfo + SchemaInfo = SchemaInfo }; // visit the with node and add it to the front of our local collection of nodes. @@ -276,7 +283,7 @@ internal void CompileInternal(QueryWriter writer, CompileContext? context = null .DistinctBy(x => x.Key); // add any variables that might have been added by other builders in a sub-query context. - variables = variables.Concat(_queryVariables.Where(x => !variables.Any(x => x.Key == x.Key))); + variables = variables.Concat(QueryVariables.Where(x => !variables.Any(x => x.Key == x.Key))); } /// @@ -298,17 +305,37 @@ public ValueTask CompileAsync(IEdgeDBQueryable edgedb, bool debug /// private async ValueTask IntrospectAndCompileAsync(IEdgeDBQueryable edgedb, bool debug, CancellationToken token) { - if (_nodes.Any(x => x.RequiresIntrospection) || _queryGlobals.Any(x => x.Value is SubQuery subQuery && subQuery.RequiresIntrospection)) - _schemaInfo ??= await SchemaIntrospector.GetOrCreateSchemaIntrospectionAsync(edgedb, token).ConfigureAwait(false); + if (Nodes.Any(x => x.RequiresIntrospection) || QueryGlobals.Any(x => x.Value is SubQuery subQuery && subQuery.RequiresIntrospection)) + SchemaInfo ??= await SchemaIntrospector.GetOrCreateSchemaIntrospectionAsync(edgedb, token).ConfigureAwait(false); var result = Compile(debug); - _nodes.Clear(); - _queryGlobals.Clear(); + Nodes.Clear(); + QueryGlobals.Clear(); return result; } #region Generic sub-query methods + private QueryBuilder By(LambdaExpression selector) + { + if(CurrentUserNode is not GroupNode groupNode) + throw new InvalidOperationException($"Cannot add a 'by' expression on a {CurrentUserNode}"); + + groupNode.By(selector); + return this; + } + + private QueryBuilder Using(LambdaExpression expression) + where TNewContext : IQueryContextUsing + { + if(CurrentUserNode is not GroupNode groupNode) + throw new InvalidOperationException($"Cannot add a 'by' expression on a {CurrentUserNode}"); + + groupNode.Using(expression); + return EnterNewContext(); + } + + /// /// Adds a 'FILTER' statement to the current node. /// @@ -517,7 +544,12 @@ private QueryBuilder Else(Func, if (CurrentUserNode is not InsertNode insertNode) throw new InvalidOperationException($"Cannot else on a {CurrentUserNode}"); - var builder = new QueryBuilder(new(), _queryGlobals, new()); + var builder = new QueryBuilder(_state with + { + Nodes = new(), + Variables = new() + }); + func(builder); insertNode.Else(builder); @@ -539,7 +571,11 @@ private QueryBuilder Else(Func, if (CurrentUserNode is not InsertNode insertNode) throw new InvalidOperationException($"Cannot else on a {CurrentUserNode}"); - var builder = new QueryBuilder(new(), _queryGlobals, new()); + var builder = new QueryBuilder(_state with + { + Nodes = new(), + Variables = new() + }); func(builder); insertNode.Else(builder); @@ -581,10 +617,10 @@ async Task IMultiCardinalityExecutable.ExecuteRequiredSingleAsync( #region IQueryBuilder - SchemaInfo? IQueryBuilder.SchemaInfo => _schemaInfo; - IReadOnlyCollection IQueryBuilder.Nodes => _nodes; - List IQueryBuilder.Globals => _queryGlobals; - Dictionary IQueryBuilder.Variables => _queryVariables; + SchemaInfo? IQueryBuilder.SchemaInfo => SchemaInfo; + IReadOnlyCollection IQueryBuilder.Nodes => Nodes; + List IQueryBuilder.Globals => QueryGlobals; + Dictionary IQueryBuilder.Variables => QueryVariables; void IQueryBuilder.CompileInternal(QueryWriter writer, CompileContext? context) => CompileInternal(writer, context); diff --git a/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Delete.cs b/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Delete.cs index d11556fa..99574bef 100644 --- a/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Delete.cs +++ b/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Delete.cs @@ -13,7 +13,7 @@ namespace EdgeDB public static partial class QueryBuilder { /// - public static IDeleteQuery> Delete() + public static IDeleteQuery> Delete() => new QueryBuilder().Delete; } diff --git a/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Group.cs b/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Group.cs index 6c1315ba..8bcbf57e 100644 --- a/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Group.cs +++ b/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Group.cs @@ -11,24 +11,77 @@ namespace EdgeDB { + public static partial class QueryBuilder + { + public static IGroupQuery> Group() + => new QueryBuilder().GroupInternal(); + + public static IGroupQuery> Group(Action> shape) + => new QueryBuilder().GroupInternal(shape: shape); + + public static IGroupQuery> Group(Expression> selector) + => new QueryBuilder().GroupInternal(selector); + + public static IGroupQuery> Group(Expression> selector) + => new QueryBuilder().GroupInternal(selector); + + public static IGroupQuery> Group(Expression> selector, Action> shape) + => new QueryBuilder().GroupInternal(selector, shape); + + public static IGroupQuery> Group(Expression> selector, Action> shape) + => new QueryBuilder().GroupInternal(selector, shape); + } + public partial class QueryBuilder { - IGroupQuery> IGroupable.GroupBy(Expression> propertySelector) + internal IGroupQuery GroupInternal(LambdaExpression? selector = null, + Action>? shape = null) { - AddNode(new GroupContext(typeof(TType)) - { - PropertyExpression = propertySelector - }); - return EnterNewType>(); - } + ShapeBuilder? shapeBuilder = shape is not null ? new() : null; + shape?.Invoke(shapeBuilder!); - IGroupQuery> IGroupable.Group(Expression>> groupBuilder) - { AddNode(new GroupContext(typeof(TType)) { - BuilderExpression = groupBuilder + Selector = selector, + Shape = shapeBuilder }); - return EnterNewType>(); + + return EnterNewType(); } + + public IGroupQuery Group() + => GroupInternal(); + + public IGroupQuery Group(Action> shape) + => GroupInternal(shape: shape); + + IGroupQuery IQueryBuilder.Group(Expression> selector) + => GroupInternal(selector); + + IGroupQuery IQueryBuilder.Group(Expression> selector) + => GroupInternal(selector); + + IGroupQuery IQueryBuilder.Group(Expression> selector, Action> shape) + => GroupInternal(selector, shape); + + IGroupQuery IQueryBuilder.Group(Expression> selector, Action> shape) + => GroupInternal(selector, shape); + + IMultiCardinalityExecutable> IGroupQuery.By(Expression> selector) + => By(selector).EnterNewType>(); + + IMultiCardinalityExecutable> IGroupQuery.By(Expression> selector) + => By(selector).EnterNewType>(); + + + IGroupUsingQuery> IGroupQuery.Using( + Expression> expression) + => Using, TUsing>(expression); + + IGroupUsingQuery> IGroupQuery.Using(Expression> expression) => throw new NotImplementedException(); + + IMultiCardinalityExecutable> IGroupUsingQuery.By( + Expression> selector) + => By(selector).EnterNewType>(); } } diff --git a/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Insert.cs b/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Insert.cs index 9d6cfe27..b15abdfb 100644 --- a/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Insert.cs +++ b/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Insert.cs @@ -14,32 +14,32 @@ public static partial class QueryBuilder { /// - public static IInsertQuery> Insert(Expression, TType>> value, bool returnInsertedValue) + public static IInsertQuery> Insert(Expression, TType>> value, bool returnInsertedValue) => new QueryBuilder().Insert(value, returnInsertedValue); /// - public static IInsertQuery> Insert(Expression, TType>> value) + public static IInsertQuery> Insert(Expression, TType>> value) => new QueryBuilder().Insert(value); /// - public static IInsertQuery> Insert(TType value, bool returnInsertedValue) + public static IInsertQuery> Insert(TType value, bool returnInsertedValue) => new QueryBuilder().Insert(value, returnInsertedValue); /// - public static IInsertQuery> Insert(TType value) + public static IInsertQuery> Insert(TType value) where TType : class => new QueryBuilder().Insert(value, false); - public static IInsertQuery> Insert(Type type, IDictionary values) + public static IInsertQuery> Insert(Type type, IDictionary values) => new QueryBuilder().Insert(type, values); - public static IInsertQuery> Insert(Type type, IDictionary values, bool returnInsertedValue) + public static IInsertQuery> Insert(Type type, IDictionary values, bool returnInsertedValue) => new QueryBuilder().Insert(type, values, returnInsertedValue); - public static IInsertQuery> Insert(Type type, Expression> shape) + public static IInsertQuery> Insert(Type type, Expression> shape) => new QueryBuilder().Insert(type, shape); - public static IInsertQuery> Insert(Type type, Expression> shape) + public static IInsertQuery> Insert(Type type, Expression> shape) => new QueryBuilder().Insert(type, shape); } diff --git a/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Select.cs b/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Select.cs index 38d520fb..becf0986 100644 --- a/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Select.cs +++ b/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Select.cs @@ -14,11 +14,11 @@ namespace EdgeDB public static partial class QueryBuilder { /// - public static ISelectQuery> Select() + public static ISelectQuery> Select() => new QueryBuilder().Select(); /// - public static ISelectQuery> Select(Action> shape) + public static ISelectQuery> Select(Action> shape) => new QueryBuilder().Select(shape); /// @@ -29,7 +29,7 @@ public static ISelectQuery> Select(Action /// A . /// - public static ISelectQuery> SelectExpression(Expression> expression) + public static ISelectQuery> SelectExpression(Expression> expression) => new QueryBuilder().SelectExpression(expression); /// @@ -40,7 +40,7 @@ public static ISelectQuery> SelectExpression(E /// /// A . /// - public static ISelectQuery> SelectExpression(Expression> expression) + public static ISelectQuery> SelectExpression(Expression> expression) => new QueryBuilder().SelectExp(expression); } @@ -115,10 +115,10 @@ public ISelectQuery SelectExpression(Expression /// A . /// - public ISelectQuery SelectExp( - Expression> expression, + public ISelectQuery SelectExp( + Expression> expression, Action>? shape = null - ) where TQuery : IQuery + ) { var shapeBuilder = shape is not null ? new ShapeBuilder() : null; @@ -149,7 +149,7 @@ internal ISelectQuery SelectExp(Expr return EnterNewType(); } - ISelectQuery IQueryBuilder.SelectExpression(Expression> expression, Action>? shape) where TQuery : default + ISelectQuery IQueryBuilder.SelectExpression(Expression> expression, Action>? shape) => SelectExp(expression, shape); ISelectQuery ISelectQuery.Filter(Expression> filter) diff --git a/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Update.cs b/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Update.cs index 0c414b9a..3bbe2b44 100644 --- a/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Update.cs +++ b/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Update.cs @@ -13,45 +13,45 @@ namespace EdgeDB public static partial class QueryBuilder { /// - public static IUpdateQuery> Update(Expression> updateFunc, bool returnUpdatedValue) + public static IUpdateQuery> Update(Expression> updateFunc, bool returnUpdatedValue) => new QueryBuilder().Update(updateFunc, returnUpdatedValue); /// - public static IUpdateQuery> Update(Expression> updateFunc) + public static IUpdateQuery> Update(Expression> updateFunc) => new QueryBuilder().Update(updateFunc, false); /// - public static IUpdateQuery> Update(Expression, TType, TType>> updateFunc, bool returnUpdatedValue) + public static IUpdateQuery> Update(Expression, TType, TType>> updateFunc, bool returnUpdatedValue) => new QueryBuilder().Update(updateFunc, returnUpdatedValue); /// - public static IUpdateQuery> Update(Expression, TType, TType>> updateFunc) + public static IUpdateQuery> Update(Expression, TType, TType>> updateFunc) => new QueryBuilder().Update(updateFunc, false); /// - public static IUpdateQuery> Update( - Expression, TType>> selector, - Expression, TType, TType>> updateFunc, + public static IUpdateQuery> Update( + Expression, TType>> selector, + Expression, TType, TType>> updateFunc, bool returnUpdatedValue) => new QueryBuilder().Update(selector, updateFunc, returnUpdatedValue); /// - public static IUpdateQuery> Update( - Expression, TType>> selector, + public static IUpdateQuery> Update( + Expression, TType>> selector, Expression> updateFunc, bool returnUpdatedValue) => new QueryBuilder().Update(selector, updateFunc, returnUpdatedValue); /// - public static IUpdateQuery> Update( - Expression, TType>> selector, + public static IUpdateQuery> Update( + Expression, TType>> selector, Expression> updateFunc) => new QueryBuilder().Update(selector, updateFunc); /// - public static IUpdateQuery> Update( - Expression, TType>> selector, - Expression, TType, TType>> updateFunc) + public static IUpdateQuery> Update( + Expression, TType>> selector, + Expression, TType, TType>> updateFunc) => new QueryBuilder().Update(selector, updateFunc); } diff --git a/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.With.cs b/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.With.cs index 1d1464e9..1b8bf15f 100644 --- a/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.With.cs +++ b/src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.With.cs @@ -11,26 +11,30 @@ namespace EdgeDB public static partial class QueryBuilder { /// - public static IQueryBuilder> With(TVariables variables) - => QueryBuilder.With(variables); + public static IQueryBuilder> With(TVariables variables) + => new QueryBuilder>() + .With(variables).EnterNewContext>(); /// - public static IQueryBuilder> With(Expression, TVariables>> variables) - => QueryBuilder.With(variables); + public static IQueryBuilder> With( + Expression> variables) + => new QueryBuilder>() + .With(variables).EnterNewContext>(); // QueryBuilder.With(variables); } public partial class QueryBuilder { - new public static IQueryBuilder> With(TVariables variables) - => new QueryBuilder().With(variables); + public new static IQueryBuilder> With(TVariables variables) + => new QueryBuilder>().With(variables); - new public static IQueryBuilder> With(Expression, TVariables>> variables) - => new QueryBuilder().With(variables); + public static IQueryBuilder> With(Expression, TVariables>> variables) + => new QueryBuilder>().WithInternal(variables); } public partial class QueryBuilder { - public QueryBuilder> With(Expression, TVariables>> variables) + internal QueryBuilder> WithInternal( + LambdaExpression variables) { if (variables is null) throw new NullReferenceException("Variables cannot be null"); @@ -45,13 +49,17 @@ public QueryBuilder> With(Exp // add each as a global foreach (var initialization in initializations) { - _queryGlobals.Add(new QueryGlobal(initialization.Key.Name, initialization.Value, variables)); + QueryGlobals.Add(new QueryGlobal(initialization.Key.Name, initialization.Value, variables)); } - return EnterNewContext>(); + return EnterNewContext>(); } - public QueryBuilder> With(TVariables variables) + public QueryBuilder> With( + Expression> variables) + => WithInternal(variables); + + public QueryBuilder> With(TVariables variables) { if (variables is null) throw new NullReferenceException("Variables cannot be null"); @@ -68,8 +76,8 @@ public QueryBuilder> With(TVa if (EdgeDBTypeUtils.TryGetScalarType(property.PropertyType, out var scalarInfo)) { var varName = QueryUtils.GenerateRandomVariableName(); - _queryVariables.Add(varName, value); - _queryGlobals.Add( + QueryVariables.Add(varName, value); + QueryGlobals.Add( new QueryGlobal( property.Name, new SubQuery(writer => writer @@ -81,7 +89,7 @@ public QueryBuilder> With(TVa else if (property.PropertyType.IsAssignableTo(typeof(IQueryBuilder))) { // add it as a sub-query - _queryGlobals.Add(new QueryGlobal(property.Name, value)); + QueryGlobals.Add(new QueryGlobal(property.Name, value)); } // TODO: revisit references //else if ( @@ -96,8 +104,8 @@ public QueryBuilder> With(TVa // serialize and add as global and variable var referenceValue = property.PropertyType.GetProperty("Value")!.GetValue(value); var jsonVarName = QueryUtils.GenerateRandomVariableName(); - _queryVariables.Add(jsonVarName, DataTypes.Json.Serialize(referenceValue)); - _queryGlobals.Add(new QueryGlobal(property.Name, new SubQuery(writer => writer + QueryVariables.Add(jsonVarName, DataTypes.Json.Serialize(referenceValue)); + QueryGlobals.Add(new QueryGlobal(property.Name, new SubQuery(writer => writer .QueryArgument("json", jsonVarName) ), value)); } @@ -105,10 +113,10 @@ public QueryBuilder> With(TVa throw new InvalidOperationException($"Cannot serialize {property.Name}: No serialization strategy found for {property.PropertyType}"); } - return EnterNewContext>(); + return EnterNewContext>(); } - IQueryBuilder> IQueryBuilder.With(TVariables variables) => With(variables); - IQueryBuilder> IQueryBuilder.With(Expression, TVariables>> variables) => With(variables); + IQueryBuilder> IQueryBuilder.With(TVariables variables) => With(variables); + IQueryBuilder> IQueryBuilder.With(Expression, TVariables>> variables) => WithInternal(variables); } } diff --git a/src/EdgeDB.Net.QueryBuilder/QueryBuilderState.cs b/src/EdgeDB.Net.QueryBuilder/QueryBuilderState.cs new file mode 100644 index 00000000..00629e2e --- /dev/null +++ b/src/EdgeDB.Net.QueryBuilder/QueryBuilderState.cs @@ -0,0 +1,15 @@ +using EdgeDB.QueryNodes; +using EdgeDB.Schema; + +namespace EdgeDB; + +internal sealed record QueryBuilderState( + List Nodes, + List Globals, + Dictionary Variables, + SchemaInfo? SchemaInfo +) +{ + public SchemaInfo? SchemaInfo { get; set; } = SchemaInfo; + public static QueryBuilderState Empty => new(new(), new(), new(), null); +} diff --git a/src/EdgeDB.Net.QueryBuilder/QueryContext.cs b/src/EdgeDB.Net.QueryBuilder/QueryContext.cs index 00a107b8..d3434f12 100644 --- a/src/EdgeDB.Net.QueryBuilder/QueryContext.cs +++ b/src/EdgeDB.Net.QueryBuilder/QueryContext.cs @@ -4,25 +4,15 @@ namespace EdgeDB { - public class QueryContext : QueryContext { } - /// - /// Represents context used within query functions. - /// - public class QueryContext : IQueryContext + public abstract class QueryContext : IQueryContext { - /// - /// Gets a mock reference to the current working type. - /// - public TSelf Self { get; } = default!; - /// /// References a defined query argument with the given name. /// /// The name of the query argument. /// The type of the query argument. /// A mock reference to a global with the given . - public TType QueryArgument(string name) - => default!; + public abstract TType QueryArgument(string name); /// /// References a defined query global given a name. @@ -32,8 +22,7 @@ public TType QueryArgument(string name) /// /// A mock reference to a global with the given . /// - public TType Global(string name) - => default!; + public abstract TType Global(string name); /// /// References a contextual local. @@ -43,8 +32,7 @@ public TType Global(string name) /// /// A mock reference to a local with the given . /// - public TType Local(string name) - => default!; + public abstract TType Local(string name); /// /// References a contextual local. @@ -53,8 +41,7 @@ public TType Local(string name) /// /// A mock reference to a local with the given . /// - public object? Local(string name) - => default!; + public abstract object? Local(string name); /// /// References a contextual local without checking the local context. @@ -64,8 +51,7 @@ public TType Local(string name) /// /// A mock reference to a local with the given . /// - public TType UnsafeLocal(string name) - => default!; + public abstract TType UnsafeLocal(string name); /// /// References a contextual local without checking the local context. @@ -74,8 +60,7 @@ public TType UnsafeLocal(string name) /// /// A mock reference to a local with the given . /// - public object? UnsafeLocal(string name) - => default!; + public abstract object? UnsafeLocal(string name); /// /// Adds raw edgeql to the current query. @@ -85,8 +70,7 @@ public TType UnsafeLocal(string name) /// /// A mock reference of the returning type of the raw edgeql. /// - public TType Raw(string query) - => default!; + public abstract TType Raw(string query); /// /// Adds a backlink to the current query. @@ -96,8 +80,7 @@ public TType Raw(string query) /// A mock array of containing just the objects id. /// To return a specific type use . /// - public EdgeDBObject[] BackLink(string property) - => default!; + public abstract EdgeDBObject[] BackLink(string property); /// /// Adds a backlink to the current query. @@ -108,9 +91,8 @@ public EdgeDBObject[] BackLink(string property) /// A mock collection of containing just the objects id. /// To return a specific type use . /// - public TCollection BackLink(string property) - where TCollection : IEnumerable - => default!; + public abstract TCollection BackLink(string property) + where TCollection : IEnumerable; /// /// Adds a backlink with the given type to the current query. @@ -120,8 +102,7 @@ public TCollection BackLink(string property) /// /// A mock array of . /// - public TType[] BackLink(Expression> propertySelector) - => default!; + public abstract TType[] BackLink(Expression> propertySelector); /// /// Adds a backlink with the given type and shape to the current query. @@ -132,8 +113,8 @@ public TType[] BackLink(Expression> propertySelector /// /// A mock array of . /// - public TType[] BackLink(Expression> propertySelector, Action> shape) - => default!; + public abstract TType[] BackLink(Expression> propertySelector, + Action> shape); /// /// Adds a backlink with the given type and shape to the current query. @@ -145,9 +126,9 @@ public TType[] BackLink(Expression> propertySelector /// /// A mock collection of . /// - public TCollection BackLink(Expression> propertySelector, Action> shape) - where TCollection : IEnumerable - => default!; + public abstract TCollection BackLink(Expression> propertySelector, + Action> shape) + where TCollection : IEnumerable; /// /// Adds a sub query to the current query. @@ -157,8 +138,7 @@ public TCollection BackLink(Expression> /// /// A single mock instance of . /// - public TType SubQuery(ISingleCardinalityQuery query) - => default!; + public abstract TType SubQuery(ISingleCardinalityQuery query); /// /// Adds a sub query to the current query. @@ -168,8 +148,9 @@ public TType SubQuery(ISingleCardinalityQuery query) /// /// A mock array of . /// - public TType[] SubQuery(IMultiCardinalityQuery query) - => default!; + public abstract TType[] SubQuery(IMultiCardinalityQuery query); + + public abstract TType SubQuerySingle(IMultiCardinalityQuery query); /// /// Adds a sub query to the current query. @@ -178,24 +159,67 @@ public TType[] SubQuery(IMultiCardinalityQuery query) /// The collection type to return. /// The multi-cardinality query to add as a sub query. /// A mock collection of . - public TCollection SubQuery(IMultiCardinalityQuery query) - where TCollection : IEnumerable - => default!; + public abstract TCollection SubQuery(IMultiCardinalityQuery query) + where TCollection : IEnumerable; + + public abstract T Ref(IEnumerable collection); } - /// - /// Represents context used within query functions containing a variable type. - /// - /// The type containing the variables defined in the query. - /// The current working type of the query. - public abstract class QueryContext : QueryContext + public abstract class QueryContextSelf : QueryContext, IQueryContextSelf { - /// - /// Gets a collection of variables defined in a with block. - /// - public TVariables Variables - => default!; + public abstract TSelf Self { get; } + } + + public abstract class QueryContextVars : QueryContext, IQueryContextVars + { + public abstract TVars Variables { get; } } - internal interface IQueryContext { } + public abstract class QueryContextUsing : QueryContext, IQueryContextUsing + { + public abstract TUsing Using { get; } + } + + public abstract class QueryContextSelfVars : QueryContext, IQueryContextSelf, IQueryContextVars + { + public abstract TSelf Self { get; } + + public abstract TVars Variables { get; } + } + + public abstract class QueryContextSelfUsing : QueryContext, IQueryContextUsing, IQueryContextSelf + { + public abstract TUsing Using { get; } + public abstract TSelf Self { get; } + } + + public abstract class QueryContextUsingVars : QueryContext, IQueryContextUsing, IQueryContextVars + { + public abstract TUsing Using { get; } + public abstract TVars Variables { get; } + } + + public abstract class QueryContextSelfUsingVars : QueryContext, IQueryContextUsing, IQueryContextVars, IQueryContextSelf + { + public abstract TSelf Self { get; } + public abstract TUsing Using { get; } + public abstract TVars Variables { get; } + } + + public interface IQueryContext { } + + public interface IQueryContextSelf : IQueryContext + { + TSelf Self { get; } + } + + public interface IQueryContextUsing : IQueryContext + { + TUsing Using { get; } + } + + public interface IQueryContextVars : IQueryContext + { + TVars Variables { get; } + } } diff --git a/src/EdgeDB.Net.QueryBuilder/QueryNodes/Contexts/GroupContext.cs b/src/EdgeDB.Net.QueryBuilder/QueryNodes/Contexts/GroupContext.cs index 49ab1b5f..45f1d490 100644 --- a/src/EdgeDB.Net.QueryBuilder/QueryNodes/Contexts/GroupContext.cs +++ b/src/EdgeDB.Net.QueryBuilder/QueryNodes/Contexts/GroupContext.cs @@ -1,4 +1,5 @@ -using System; +using EdgeDB.Builders; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -9,10 +10,13 @@ namespace EdgeDB.QueryNodes { internal class GroupContext : NodeContext { - public Expression? PropertyExpression { get; init; } - public Expression? BuilderExpression { get; init; } + public bool IncludeShape { get; set; } = true; + public LambdaExpression? Selector { get; init; } + public IShapeBuilder? Shape { get; init; } + public GroupContext(Type currentType) : base(currentType) { + } } } diff --git a/src/EdgeDB.Net.QueryBuilder/QueryNodes/ForNode.cs b/src/EdgeDB.Net.QueryBuilder/QueryNodes/ForNode.cs index 7b9fdc89..b3f7b444 100644 --- a/src/EdgeDB.Net.QueryBuilder/QueryNodes/ForNode.cs +++ b/src/EdgeDB.Net.QueryBuilder/QueryNodes/ForNode.cs @@ -48,7 +48,7 @@ private WriterProxy ParseExpression(string name, string varName, string json) { return x switch { - _ when x.Type == typeof(QueryContext) => new QueryContext(), + _ when x.Type == typeof(QueryContext) => null!, _ when ReflectionUtils.IsSubclassOfRawGeneric(typeof(JsonCollectionVariable<>), x.Type) => typeof(JsonCollectionVariable<>).MakeGenericType(Context.CurrentType) .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new Type[] { typeof(string), typeof(string), typeof(JArray)})! diff --git a/src/EdgeDB.Net.QueryBuilder/QueryNodes/GroupNode.cs b/src/EdgeDB.Net.QueryBuilder/QueryNodes/GroupNode.cs index b149b426..68e7f4c9 100644 --- a/src/EdgeDB.Net.QueryBuilder/QueryNodes/GroupNode.cs +++ b/src/EdgeDB.Net.QueryBuilder/QueryNodes/GroupNode.cs @@ -1,20 +1,53 @@ -using System; +using EdgeDB.Builders; +using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Text; using System.Threading.Tasks; namespace EdgeDB.QueryNodes { - internal class GroupNode : QueryNode + internal sealed class GroupNode(NodeBuilder builder) : QueryNode(builder) { - public GroupNode(NodeBuilder builder) : base(builder) + private WriterProxy? _by; + private WriterProxy? _using; + + public override void FinalizeQuery(QueryWriter writer) { + if (_by is null) + throw new InvalidOperationException("A 'by' expression is required for groups!"); + + writer.Append("group ", OperatingType.GetEdgeDBTypeName()); + + if (Context.IncludeShape) + { + (Context.Shape ?? BaseShapeBuilder.CreateDefault(GetOperatingType())) + .GetShape() + .Compile(writer.Append(' '), (writer, expression) => + { + using var consumer = NodeTranslationContext.CreateContextConsumer(expression.Root); + ExpressionTranslator.ContextualTranslate(expression.Expression, consumer, writer); + }); + } + + _using?.Invoke(writer); + _by?.Invoke(writer); } - public override void FinalizeQuery(QueryWriter writer) + public void By(LambdaExpression selector) + { + _by ??= writer => writer.Append(" by ", ProxyExpression(selector)); + } + + public void Using(LambdaExpression expression) { - throw new NotImplementedException(); + _using ??= writer => writer.Append(" using ", + ProxyExpression(expression, ctx => + { + ctx.WrapNewExpressionInBrackets = false; + ctx.UseInitializationOperator = false; + })); } } } diff --git a/src/EdgeDB.Net.QueryBuilder/QueryNodes/QueryNode.cs b/src/EdgeDB.Net.QueryBuilder/QueryNodes/QueryNode.cs index 2b6ff147..3bec561b 100644 --- a/src/EdgeDB.Net.QueryBuilder/QueryNodes/QueryNode.cs +++ b/src/EdgeDB.Net.QueryBuilder/QueryNodes/QueryNode.cs @@ -175,15 +175,17 @@ internal Type GetOperatingType() /// /// The expression to translate. /// The query string writer to append the translated expression to. - protected void TranslateExpression(LambdaExpression expression, QueryWriter writer) + /// Context to provide to the translation. + protected void TranslateExpression(LambdaExpression expression, QueryWriter writer, Action? context = null) { using var consumer = NodeTranslationContext.CreateContextConsumer(expression); + context?.Invoke(consumer); ExpressionTranslator.Translate(expression, consumer, writer); } - protected WriterProxy ProxyExpression(LambdaExpression expression) + protected WriterProxy ProxyExpression(LambdaExpression expression, Action? context = null) { - return writer => TranslateExpression(expression, writer); + return writer => TranslateExpression(expression, writer, context); } /// diff --git a/src/EdgeDB.Net.QueryBuilder/Translators/Expressions/ExpressionContext.cs b/src/EdgeDB.Net.QueryBuilder/Translators/Expressions/ExpressionContext.cs index 5704a747..779e4b7f 100644 --- a/src/EdgeDB.Net.QueryBuilder/Translators/Expressions/ExpressionContext.cs +++ b/src/EdgeDB.Net.QueryBuilder/Translators/Expressions/ExpressionContext.cs @@ -24,7 +24,7 @@ internal class ExpressionContext /// Gets the root lambda function that is currently being translated. /// public LambdaExpression RootExpression { get; set; } - + /// /// Gets a collection of method parameters within the . /// @@ -46,18 +46,23 @@ internal class ExpressionContext public bool IsShape { get; set; } /// - /// Gets or sets whether or not the current expression has an initialization + /// Gets or sets whether or not the current expression has an initialization /// operator, ex: ':=, +=, -='. /// - public bool HasInitializationOperator { get; set; } + public bool UseInitializationOperator { get; set; } /// /// Gets or sets whether or not to include a self reference. /// Ex: : '.name', : 'name' /// - /// + /// public bool IncludeSelfReference { get; set; } = true; + /// + /// Gets or sets whether or not to wrap new expressions in brackets. + /// + public bool WrapNewExpressionInBrackets { get; set; } = true; + /// /// Gets whether or not the current expression tree is a free object. /// @@ -88,7 +93,7 @@ public bool IsFreeObject /// The query arguments collection. /// The query global collection. /// The query node constructing this translation context. - public ExpressionContext(NodeContext context, LambdaExpression rootExpression, + public ExpressionContext(NodeContext context, LambdaExpression rootExpression, IDictionary queryArguments, List globals, QueryNode? node = null) { @@ -127,7 +132,7 @@ public void SetVariable(string name, object? value) /// The reference of the global. /// The out parameter containing the global. /// - /// if a global could be found with the reference; + /// if a global could be found with the reference; /// otherwise . /// public bool TryGetGlobal(object? reference, [MaybeNullWhen(false)]out QueryGlobal global) @@ -142,7 +147,7 @@ public bool TryGetGlobal(object? reference, [MaybeNullWhen(false)]out QueryGloba /// The name of the global. /// The out parameter containing the global. /// - /// if a global could be found with the reference; + /// if a global could be found with the reference; /// otherwise . /// public bool TryGetGlobal(string? name, [MaybeNullWhen(false)] out QueryGlobal global) @@ -161,7 +166,7 @@ public string GetOrAddGlobal(object? reference, object? value) { if (reference is not null && TryGetGlobal(reference, out var global)) return global.Name; - + var name = QueryUtils.GenerateRandomVariableName(); SetGlobal(name, value, reference); return name; diff --git a/src/EdgeDB.Net.QueryBuilder/Translators/Expressions/InitializationTranslator.cs b/src/EdgeDB.Net.QueryBuilder/Translators/Expressions/InitializationTranslator.cs index a1e47d83..9a78e567 100644 --- a/src/EdgeDB.Net.QueryBuilder/Translators/Expressions/InitializationTranslator.cs +++ b/src/EdgeDB.Net.QueryBuilder/Translators/Expressions/InitializationTranslator.cs @@ -46,12 +46,12 @@ public static Dictionary PullInitializationExpression(Ex return new SubQuery((info, writer) => { - new QueryBuilder(info) + new QueryBuilder() .For(collection, x => QueryBuilder .Insert(x) .UnlessConflict() ) - .WriteTo(writer, context); + .WriteTo(writer, context, new CompileContext() { SchemaInfo = info }); }); } @@ -84,8 +84,20 @@ public static void Translate( ExpressionContext context, QueryWriter writer) { - writer.Append("{ "); + if (context.WrapNewExpressionInBrackets) + { + writer.Wrapped(Value.Of(writer => TranslateInternal(expressions, context, writer)), "{}"); + return; + } + TranslateInternal(expressions, context, writer); + } + + private static void TranslateInternal( + List<(EdgeDBPropertyInfo, Expression)> expressions, + ExpressionContext context, + QueryWriter writer) + { for (var i = 0; i != expressions.Count; i++) { var (property, expression) = expressions[i]; @@ -224,24 +236,22 @@ public static void Translate( x.IsShape = false; }); - bool isSetter = !(context.NodeContext is not InsertContext and not UpdateContext && + var isSetter = !(context.NodeContext is not InsertContext and not UpdateContext && context.NodeContext.CurrentType.GetProperty(property.PropertyName) != null && expression is not MethodCallExpression); - writer.Append(property.EdgeDBName); - var value = ExpressionTranslator.Proxy(expression!, context); if (newContext.IsShape) { // add the start and end shape form. writer - .Append(": {", value, '}'); + .Append(property.EdgeDBName, ": {", value, '}'); } - else if ((isSetter || context.IsFreeObject) && !newContext.HasInitializationOperator) + else if ((isSetter || context.IsFreeObject) && newContext.UseInitializationOperator) { writer - .Append(" := ", value); + .Append(property.EdgeDBName, " := ", value); } else { @@ -254,8 +264,6 @@ public static void Translate( if (i + 1 != expressions.Count) writer.Append(", "); } - - writer.Append('}'); } } } diff --git a/src/EdgeDB.Net.QueryBuilder/Translators/Expressions/MemberExpressionTranslator.cs b/src/EdgeDB.Net.QueryBuilder/Translators/Expressions/MemberExpressionTranslator.cs index 5d34fee1..e950eb84 100644 --- a/src/EdgeDB.Net.QueryBuilder/Translators/Expressions/MemberExpressionTranslator.cs +++ b/src/EdgeDB.Net.QueryBuilder/Translators/Expressions/MemberExpressionTranslator.cs @@ -31,7 +31,14 @@ public override void Translate(MemberExpression expression, ExpressionContext co switch (memberExpression.Member.Name) { - case nameof(QueryContext.Variables): + case nameof(QueryContextUsing.Using): + // get the reference + if(deconstructed[^3] is not MemberExpression targetMemberExpression) + throw new NotSupportedException($"Cannot use expression type {deconstructed[^3] .NodeType} as a variable access"); + + writer.Append(targetMemberExpression.Member.GetEdgeDBPropertyName()); + break; + case nameof(QueryContextVars.Variables): // get the reference var target = deconstructed[^3]; @@ -94,7 +101,7 @@ public override void Translate(MemberExpression expression, ExpressionContext co default: throw new NotSupportedException($"Cannot use expression type {target.NodeType} as a variable access"); } - case nameof(QueryContext.Self): + case nameof(QueryContextSelf.Self): var paths = deconstructed[..^2]; writer.Append('.'); for (var i = 0; i != paths.Length; i++) diff --git a/src/EdgeDB.Net.QueryBuilder/Translators/Expressions/MethodCallExpressionTranslator.cs b/src/EdgeDB.Net.QueryBuilder/Translators/Expressions/MethodCallExpressionTranslator.cs index 592ef919..a1a4a221 100644 --- a/src/EdgeDB.Net.QueryBuilder/Translators/Expressions/MethodCallExpressionTranslator.cs +++ b/src/EdgeDB.Net.QueryBuilder/Translators/Expressions/MethodCallExpressionTranslator.cs @@ -196,6 +196,7 @@ private static void TranslateToEdgeQL(MethodCallExpression expression, Expressio } } return; + case nameof(QueryContext.SubQuerySingle): case nameof(QueryContext.SubQuery): { // pull the builder parameter and add it to a new lambda @@ -207,19 +208,16 @@ private static void TranslateToEdgeQL(MethodCallExpression expression, Expressio return; } + case nameof(QueryContext.Ref): + writer.Append('.', Proxy(expression.Arguments[0], context)); + break; default: throw new NotImplementedException( $"{expression.Method.Name} does not have an implementation. This is a bug, please file a github issue with your query to reproduce this exception."); } } - // check if our method translators can translate it - if (MethodTranslator.TryTranslateMethod(writer, expression, context)) - { - return; - } - - throw new Exception($"Couldn't find translator for {expression.Method.Name}"); + MethodTranslator.TranslateMethod(writer, expression, context); } } } diff --git a/src/EdgeDB.Net.QueryBuilder/Translators/Methods/EnumerableMethodTranslator.cs b/src/EdgeDB.Net.QueryBuilder/Translators/Methods/EnumerableMethodTranslator.cs index ce34cc02..dfdb2fc7 100644 --- a/src/EdgeDB.Net.QueryBuilder/Translators/Methods/EnumerableMethodTranslator.cs +++ b/src/EdgeDB.Net.QueryBuilder/Translators/Methods/EnumerableMethodTranslator.cs @@ -74,10 +74,12 @@ public void FirstOrDefault(QueryWriter writer, TranslatedParameter source, Trans ElementAt( writer, source, - new TranslatedParameter(typeof(long), writer => writer - .Append("0"), - Expression.Constant(0L) - )); + new TranslatedParameter( + typeof(long), + Expression.Constant(0L), + source.Context + ) + ); writer .Append(" ?? ") .Append(filterOrDefault); diff --git a/src/EdgeDB.Net.QueryBuilder/Translators/Methods/GroupMethodTranslator.cs b/src/EdgeDB.Net.QueryBuilder/Translators/Methods/GroupMethodTranslator.cs new file mode 100644 index 00000000..c75d99f7 --- /dev/null +++ b/src/EdgeDB.Net.QueryBuilder/Translators/Methods/GroupMethodTranslator.cs @@ -0,0 +1,18 @@ +namespace EdgeDB.Translators.Methods; + +internal sealed class GroupMethodTranslator : MethodTranslator +{ + [MethodName(nameof(EdgeQL.Cube))] + public void Cube(QueryWriter writer, TranslatedParameter newExp) + { + newExp.Context = newExp.Context.Enter(x => x.WrapNewExpressionInBrackets = false); + writer.Function("cube", newExp); + } + + [MethodName(nameof(EdgeQL.Rollup))] + public void Rollup(QueryWriter writer, TranslatedParameter newExp) + { + newExp.Context = newExp.Context.Enter(x => x.WrapNewExpressionInBrackets = false); + writer.Function("rollup", newExp); + } +} diff --git a/src/EdgeDB.Net.QueryBuilder/Translators/Methods/MethodTranslator.cs b/src/EdgeDB.Net.QueryBuilder/Translators/Methods/MethodTranslator.cs index b365b3c1..5708b156 100644 --- a/src/EdgeDB.Net.QueryBuilder/Translators/Methods/MethodTranslator.cs +++ b/src/EdgeDB.Net.QueryBuilder/Translators/Methods/MethodTranslator.cs @@ -134,26 +134,6 @@ internal static bool TryGetTranslator(Type type, string name, null; } - /// - /// Attempts to translate the given into a edgeql equivalent expression. - /// - /// The query string writer to write the translated method to, if successful. - /// The method call expression to translate. - /// The current context for the method call expression. - /// - /// if the was translated; otherwise . - /// - public static bool TryTranslateMethod(QueryWriter writer, MethodCallExpression methodCall, - ExpressionContext context) - { - try - { - TranslateMethod(writer, methodCall, context); - return true; - } - catch { return false; } - } - /// /// Translates the given into a edgeql equivalent expression. /// @@ -252,13 +232,8 @@ protected void Translate(QueryWriter writer, MethodCallExpression methodCall, Ex // its value to the remaining arguments to the expression and break out of the loop if (parameterInfo.GetCustomAttribute() != null) { - var remaining = methodCall.Arguments.Skip(i).ToArray(); - for (var j = 0; j != remaining.Length; j++) - { - parsedParameters[i + j] = new TranslatedParameter( - remaining[j].Type, ExpressionTranslator.Proxy(remaining[j], context), remaining[j] - ); - } + parsedParameters[i] = methodCall.Arguments.Skip(i) + .Select(x => new TranslatedParameter(x.Type, x, context)).ToArray(); break; } @@ -266,8 +241,8 @@ protected void Translate(QueryWriter writer, MethodCallExpression methodCall, Ex { parsedParameters[i] = new TranslatedParameter( methodCall.Arguments[i].Type, - ExpressionTranslator.Proxy(methodCall.Arguments[i], context), - methodCall.Arguments[i] + methodCall.Arguments[i], + context ); } else if (parameterInfo.ParameterType == typeof(ExpressionContext)) @@ -289,8 +264,8 @@ protected void Translate(QueryWriter writer, MethodCallExpression methodCall, Ex newParameters[0] = methodCall.Object is not null ? new TranslatedParameter( methodCall.Object.Type, - ExpressionTranslator.Proxy(methodCall.Object, context), - methodCall.Object) + methodCall.Object, + context) : null; parsedParameters = newParameters; diff --git a/src/EdgeDB.Net.QueryBuilder/Translators/Methods/TranslatedParameter.cs b/src/EdgeDB.Net.QueryBuilder/Translators/Methods/TranslatedParameter.cs index 5464b749..fa081e75 100644 --- a/src/EdgeDB.Net.QueryBuilder/Translators/Methods/TranslatedParameter.cs +++ b/src/EdgeDB.Net.QueryBuilder/Translators/Methods/TranslatedParameter.cs @@ -18,9 +18,9 @@ internal class TranslatedParameter public Type ParameterType { get; } /// - /// Gets the translated value of the parameter. + /// Gets the context for translating the . /// - public WriterProxy ValueProxy { get; } + public ExpressionContext Context { get; set; } /// /// Gets the raw expression of the parameter. @@ -57,19 +57,17 @@ public bool IsMultiLinkType /// The type of the parameter. /// The proxy to translate the value of the parameter. /// The raw expression of the parameter. - public TranslatedParameter(Type type, WriterProxy value, Expression raw) + public TranslatedParameter(Type type, Expression raw, ExpressionContext context) { ParameterType = type; - ValueProxy = value; + Context = context; RawValue = raw; } public void WriteTo(QueryWriter writer) - { - ValueProxy(writer); - } + => ExpressionTranslator.ContextualTranslate(RawValue, Context, writer); - public static implicit operator Value(TranslatedParameter param) => new(param.ValueProxy); - public static implicit operator Terms.FunctionArg(TranslatedParameter param) => new(param.ValueProxy); + public static implicit operator Value(TranslatedParameter param) => Value.Of(param.WriteTo); + public static implicit operator Terms.FunctionArg(TranslatedParameter param) => new(Value.Of(param.WriteTo)); } } diff --git a/src/EdgeDB.Net.QueryBuilder/Translators/Methods/TupleMethodTranslators.cs b/src/EdgeDB.Net.QueryBuilder/Translators/Methods/TupleMethodTranslators.cs new file mode 100644 index 00000000..19c19210 --- /dev/null +++ b/src/EdgeDB.Net.QueryBuilder/Translators/Methods/TupleMethodTranslators.cs @@ -0,0 +1,22 @@ +using System.Runtime.CompilerServices; + +namespace EdgeDB.Translators.Methods; + +internal sealed class TupleMethodTranslators : MethodTranslator +{ + protected override Type TranslatorTargetType => typeof(Tuple); + + [MethodName(nameof(Tuple.Create))] + public void Create(QueryWriter writer, params TranslatedParameter[] args) + { + writer.Wrapped(Value.Of(writer => + { + for (int i = 0; i != args.Length - 1; i++) + { + writer.Append(args[i], ", "); + } + + writer.Append(args[^1]); + })); + } +} diff --git a/src/EdgeDB.Net.QueryBuilder/Utils/QueryGenerationUtils.cs b/src/EdgeDB.Net.QueryBuilder/Utils/QueryGenerationUtils.cs index 56b6beae..038a1d00 100644 --- a/src/EdgeDB.Net.QueryBuilder/Utils/QueryGenerationUtils.cs +++ b/src/EdgeDB.Net.QueryBuilder/Utils/QueryGenerationUtils.cs @@ -149,7 +149,7 @@ public static async ValueTask>> GenerateUpdateFact /// A ValueTask representing the (a)sync operation of preforming the introspection query. /// The result of the task is a generated filter expression. /// - public static async ValueTask, bool>>> GenerateUpdateFilterAsync(IEdgeDBQueryable edgedb, TType value, CancellationToken token = default) + public static async ValueTask, bool>>> GenerateUpdateFilterAsync(IEdgeDBQueryable edgedb, TType value, CancellationToken token = default) { // TODO: revisit references // try and get object id @@ -159,14 +159,14 @@ public static async ValueTask, bool>> // get exclusive properties. var exclusiveProperties = await GetPropertiesAsync(edgedb, exclusive: true, token: token).ConfigureAwait(false); - var unsafeLocalMethod = typeof(QueryContext).GetMethod("UnsafeLocal", genericParameterCount: 0, new Type[] {typeof(string)})!; - return Expression.Lambda, bool>>( + var unsafeLocalMethod = typeof(QueryContextSelf).GetMethod("UnsafeLocal", genericParameterCount: 0, new Type[] {typeof(string)})!; + return Expression.Lambda, bool>>( exclusiveProperties.Select(x => { return Expression.Equal( Expression.Call( - Expression.Parameter(typeof(QueryContext), "ctx"), + Expression.Parameter(typeof(QueryContextSelf), "ctx"), unsafeLocalMethod, Expression.Constant(x.GetEdgeDBPropertyName()) ), @@ -174,7 +174,7 @@ public static async ValueTask, bool>> ); }).Aggregate((x, y) => Expression.And(x, y)), Expression.Parameter(typeof(TType), "x"), - Expression.Parameter(typeof(QueryContext), "ctx") + Expression.Parameter(typeof(QueryContextSelf), "ctx") ); } }