Skip to content

Commit

Permalink
More questionable work on the query builder
Browse files Browse the repository at this point in the history
  • Loading branch information
quinchs committed Jan 19, 2024
1 parent 06b9167 commit 5090201
Show file tree
Hide file tree
Showing 12 changed files with 310 additions and 159 deletions.
8 changes: 4 additions & 4 deletions examples/EdgeDB.Examples.CSharp/Examples/QueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public async Task ExecuteAsync(EdgeDBClient client)

private static async Task QueryBuilderDemo(EdgeDBClient client)
{
var test = QueryBuilder.SelectExp(() => EdgeQL.Range<int>(5, 10, true, false, false));
var test = QueryBuilder.SelectExpression(() => EdgeQL.Range<int>(5, 10, true, false, false));

var result = test.Build();

Expand All @@ -66,12 +66,12 @@ private static async Task QueryBuilderDemo(EdgeDBClient client)
).Build().Prettify();

// selecting things that are not types
query = QueryBuilder.SelectExp(() =>
query = QueryBuilder.SelectExpression(() =>
EdgeQL.Count(QueryBuilder.Select<Person>())
).Build().Prettify();

// selecting 'free objects'
query = QueryBuilder.SelectExp(ctx => new
query = QueryBuilder.SelectExpression(ctx => new
{
MyString = "This is a string",
MyNumber = 42,
Expand All @@ -94,7 +94,7 @@ private static async Task QueryBuilderDemo(EdgeDBClient client)
// With object variables
query = QueryBuilder
.With(new { Args = EdgeQL.AsJson(new { Name = "Example", Email = "[email protected]" }) })
.SelectExp(ctx => new
.SelectExpression(ctx => new
{
PassedName = ctx.Variables.Args.Value.Name, PassedEmail = ctx.Variables.Args.Value.Email
}).Build().Prettify();
Expand Down
57 changes: 51 additions & 6 deletions src/EdgeDB.Net.QueryBuilder/Builders/ShapeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ public void Compile(QueryStringWriter writer, SelectShapeExpressionTranslatorCal

internal readonly struct ShapeElementExpression
{
//public readonly bool IsSelector;

public readonly LambdaExpression Root;
public readonly Expression Expression;

Expand Down Expand Up @@ -135,19 +137,28 @@ internal static Dictionary<MemberInfo, ShapeElementExpression> FlattenAnonymousE
if (expression.Body is not NewExpression init || !init.Type.IsAnonymousType() || init.Members is null)
throw new InvalidOperationException($"Expected anonymous object initializer, but got {expression.Body}");

var realProps = selectedType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
return FlattenNewExpression(selectedType, init, expression);
}

internal static Dictionary<MemberInfo, ShapeElementExpression> FlattenNewExpression(Type? selectedType, NewExpression expression, LambdaExpression root)
{
if (!expression.Type.IsAnonymousType() || expression.Members is null)
throw new InvalidOperationException($"Expected anonymous object initializer, but got {expression}");

var realProps = selectedType?.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
?? Array.Empty<PropertyInfo>();

Dictionary<MemberInfo, ShapeElementExpression> dict = new();

for (var i = 0; i != init.Arguments.Count; i++)
for (var i = 0; i != expression.Arguments.Count; i++)
{
var argExpression = init.Arguments[i];
var member = init.Members[i];
var argExpression = expression.Arguments[i];
var member = expression.Members[i];

// cross reference the 'T' type and check for any explicit name or naming convention
var realProp = realProps.FirstOrDefault(x => x.Name == member.Name);

dict.Add(realProp ?? member, new(expression, argExpression));
dict.Add(realProp ?? member, new(root, argExpression));
}

return dict;
Expand Down Expand Up @@ -234,11 +245,45 @@ public ShapeBuilder<T> Explicitly<TAnon>(Expression<Func<T, TAnon>> explicitSele
SelectedProperties.Clear();

foreach (var member in members)
SelectedProperties[member.Key.GetEdgeDBPropertyName()] = new(member.Key, member.Value);
SelectedProperties[member.Key.GetEdgeDBPropertyName()] = ParseShape(member.Key, member.Value);

return this;
}

private SelectedProperty ParseShape(MemberInfo info, ShapeElementExpression element)
{
// theres 3 types we could come across:
// 1. Property: include the specified property in the shape; ex: 'Name = x.Name'
// 2. Computed: include the compuded in the shape; ex: 'Name = {exp}'
// 3. Subshape: include the sub shape; ex: 'Friend = new { Name = x.Friend.Name }

if (element.Expression is MemberExpression)
{
var treeTail = ExpressionUtils.DisassembleExpression(element.Expression).Last();

if (treeTail is ParameterExpression param && element.Root.Parameters.Contains(param))
{
return new SelectedProperty(info);
}

// treat as a computed
return new SelectedProperty(info, element);
}

if (element.Expression is NewExpression newExpression)
{
// this is a subshape, try to get the type

var flattened = FlattenNewExpression(info.GetMemberType(), newExpression, element.Root)
.Select(x => ParseShape(x.Key, x.Value));

return new SelectedProperty(info, new SelectShape(flattened));
}

// computed
return new SelectedProperty(info, element);
}

private ShapeBuilder<T> IncludeInternal<TIncluded>(LambdaExpression selector, Action<ShapeBuilder<TIncluded>>? shape = null, bool errorOnMultiLink = false)
{
var member = GetSelectedProperty(selector);
Expand Down
49 changes: 37 additions & 12 deletions src/EdgeDB.Net.QueryBuilder/Extensions/QueryBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using EdgeDB.QueryNodes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
Expand All @@ -8,24 +9,48 @@ namespace EdgeDB
{
internal static class QueryBuilderExtensions
{
public static BuiltQuery BuildWithoutAutogeneratedNodes(this IQueryBuilder builder, NodeBuilder nodeBuilder)
public static void WriteTo(this IQueryBuilder source, QueryStringWriter writer, IQueryBuilder target, bool? includeGlobalsInQuery = true,
Action<QueryNode>? preFinalizerModifier = null, bool includeAutoGenNodes = true)
{
// remove addon & autogen nodes.
var userNodes = builder.Nodes.Where(x => !builder.Nodes.Any(y => y.SubNodes.Contains(x)) || !x.IsAutoGenerated);
if (source.Variables.Any(variable => !target.Variables.TryAdd(variable.Key, variable.Value)))
{
throw new InvalidOperationException(
"A variable with the same name already exists in the target builder");
}

target.Globals.AddRange(source.Globals);

source.InternalBuild(writer, includeGlobalsInQuery, preFinalizerModifier, includeAutoGenNodes);
}

public static void WriteTo(this IQueryBuilder source, QueryStringWriter writer, ExpressionContext context, bool? includeGlobalsInQuery = true,
Action<QueryNode>? preFinalizerModifier = null, bool includeAutoGenNodes = true)
{
foreach (var variable in source.Variables)
{
context.SetVariable(variable.Key, variable.Value);
}

// TODO: better checks for this, future should add a callback to add the
// node with its context so any parent builder can change contexts for nodes
foreach (var node in userNodes)
node.Context.SetAsGlobal = false;
foreach (var global in source.Globals)
{
context.SetGlobal(global.Name, global.Value, global.Reference);
}

foreach (var variable in builder.Variables)
source.InternalBuild(writer, includeGlobalsInQuery, preFinalizerModifier, includeAutoGenNodes);
}

public static void WriteTo(this IQueryBuilder source, QueryStringWriter writer, QueryNode node, bool? includeGlobalsInQuery = true,
Action<QueryNode>? preFinalizerModifier = null, bool includeAutoGenNodes = true)
{
if (source.Variables.Any(variable => !node.Builder.QueryVariables.TryAdd(variable.Key, variable.Value)))
{
nodeBuilder.QueryVariables[variable.Key] = variable.Value;
throw new InvalidOperationException(
"A variable with the same name already exists in the target builder");
}

var newBuilder = new QueryBuilder<object?>(userNodes.ToList(), builder.Globals.ToList(), builder.Variables.ToDictionary(x => x.Key, x => x.Value));
node.Builder.QueryGlobals.AddRange(source.Globals);

return newBuilder.BuildWithGlobals();
source.InternalBuild(writer, includeGlobalsInQuery, preFinalizerModifier, includeAutoGenNodes);
}
}
}
13 changes: 8 additions & 5 deletions src/EdgeDB.Net.QueryBuilder/Interfaces/IQueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public interface IQueryBuilder<TType, TContext> :
/// <returns>
/// A <see cref="ISelectQuery{TNewType, TContext}"/>.
/// </returns>
ISelectQuery<TNewType, TContext> SelectExp<TNewType>(Expression<Func<TNewType?>> expression);
ISelectQuery<TNewType, TContext> SelectExpression<TNewType>(Expression<Func<TNewType?>> expression);

/// <summary>
/// Adds a <c>SELECT</c> statement, selecting the result of a <paramref name="expression"/>.
Expand All @@ -91,7 +91,7 @@ public interface IQueryBuilder<TType, TContext> :
/// <returns>
/// A <see cref="ISelectQuery{TNewType, TContext}"/>.
/// </returns>
ISelectQuery<TNewType, TContext> SelectExp<TNewType>(Expression<Func<TContext, TNewType?>> expression);
ISelectQuery<TNewType, TContext> SelectExpression<TNewType>(Expression<Func<TContext, TNewType?>> expression);

/// <summary>
/// Adds a <c>SELECT</c> statement, selecting the result of a <paramref name="expression"/>.
Expand All @@ -103,7 +103,7 @@ public interface IQueryBuilder<TType, TContext> :
/// <returns>
/// A <see cref="ISelectQuery{TNewType, TContext}"/>.
/// </returns>
ISelectQuery<TNewType, TContext> SelectExp<TNewType, TQuery>(
ISelectQuery<TNewType, TContext> SelectExpression<TNewType, TQuery>(
Expression<Func<TContext, TQuery?>> expression,
Action<ShapeBuilder<TNewType>>? shape = null
) where TQuery : ISingleCardinalityExecutable<TNewType>;
Expand Down Expand Up @@ -290,12 +290,12 @@ public interface IQueryBuilder
/// <summary>
/// Gets a read-only collection of globals defined within this query builder.
/// </summary>
internal IReadOnlyCollection<QueryGlobal> Globals { get; }
internal List<QueryGlobal> Globals { get; }

/// <summary>
/// Gets a read-only dictionary of query variables defined within the query builder.
/// </summary>
internal IReadOnlyDictionary<string, object?> Variables { get; }
internal Dictionary<string, object?> Variables { get; }

/// <summary>
/// Builds the current query.
Expand Down Expand Up @@ -329,5 +329,8 @@ public interface IQueryBuilder
/// A <see cref="BuiltQuery"/> which is the current query this builder has constructed.
/// </returns>
internal BuiltQuery BuildWithGlobals(Action<QueryNode>? preFinalizerModifier = null);

internal void InternalBuild(QueryStringWriter writer, bool? includeGlobalsInQuery = true,
Action<QueryNode>? preFinalizerModifier = null, bool includeAutoGenNodes = true);
}
}
30 changes: 20 additions & 10 deletions src/EdgeDB.Net.QueryBuilder/QueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,17 @@ internal BuiltQuery InternalBuild(bool? includeGlobalsInQuery = true, Action<Que
};
}

internal void InternalBuild(QueryStringWriter writer, bool? includeGlobalsInQuery = true, Action<QueryNode>? preFinalizerModifier = null)
internal void InternalBuild(QueryStringWriter writer, bool? includeGlobalsInQuery = true, Action<QueryNode>? preFinalizerModifier = null, bool includeAutoGenNodes = true)
{
List<IDictionary<string, object?>> parameters = new();

var nodes = _nodes;

if (!includeAutoGenNodes)
nodes = nodes
.Where(x => !nodes.Any(y => y.SubNodes.Contains(x)) || !x.IsAutoGenerated)
.ToList();

// reference the introspection and finalize all nodes.
foreach (var node in nodes)
{
Expand All @@ -228,6 +233,12 @@ internal void InternalBuild(QueryStringWriter writer, bool? includeGlobalsInQuer
preFinalizerModifier(node);
}

for (var i = 0; i != nodes.Count; i++)
{
nodes[i].FinalizeQuery(writer);
parameters.Add(nodes[i].Builder.QueryVariables);
}

// create a with block if we have any globals
if ((includeGlobalsInQuery ?? false) && _queryGlobals.Any())
{
Expand All @@ -242,16 +253,10 @@ internal void InternalBuild(QueryStringWriter writer, bool? includeGlobalsInQuer
};

// visit the with node and add it to the front of our local collection of nodes.
with.Visit();
with.FinalizeQuery(writer.GetPositionalWriter(0));
nodes = nodes.Prepend(with).ToList();
}

for (var i = 0; i != nodes.Count; i++)
{
nodes[i].FinalizeQuery(writer);
parameters.Add(nodes[i].Builder.QueryVariables);
}

// // build each node starting at the last node.
// for (int i = nodes.Count - 1; i >= 0; i--)
// {
Expand Down Expand Up @@ -594,9 +599,14 @@ async Task<TType> IMultiCardinalityExecutable<TType>.ExecuteRequiredSingleAsync(

#region IQueryBuilder<TType>
IReadOnlyCollection<QueryNode> IQueryBuilder.Nodes => _nodes;
IReadOnlyCollection<QueryGlobal> IQueryBuilder.Globals => _queryGlobals;
IReadOnlyDictionary<string, object?> IQueryBuilder.Variables => _queryVariables;
List<QueryGlobal> IQueryBuilder.Globals => _queryGlobals;
Dictionary<string, object?> IQueryBuilder.Variables => _queryVariables;
BuiltQuery IQueryBuilder.BuildWithGlobals(Action<QueryNode>? preFinalizerModifier) => BuildWithGlobals(preFinalizerModifier);

void IQueryBuilder.InternalBuild(QueryStringWriter writer, bool? includeGlobalsInQuery,
Action<QueryNode>? preFinalizerModifier, bool includeAutoGenNodes) =>
InternalBuild(writer, includeGlobalsInQuery, preFinalizerModifier, includeAutoGenNodes);

#endregion
}

Expand Down
12 changes: 6 additions & 6 deletions src/EdgeDB.Net.QueryBuilder/QueryBuilder/QueryBuilder.Select.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ public static ISelectQuery<TType, QueryContext<TType>> Select<TType>(Action<Shap
/// <returns>
/// A <see cref="ISelectQuery{TNewType, TContext}"/>.
/// </returns>
public static ISelectQuery<TType, QueryContext<TType>> SelectExp<TType>(Expression<Func<TType?>> expression)
=> new QueryBuilder<TType>().SelectExp(expression);
public static ISelectQuery<TType, QueryContext<TType>> SelectExpression<TType>(Expression<Func<TType?>> expression)
=> new QueryBuilder<TType>().SelectExpression(expression);

/// <summary>
/// Adds a <c>SELECT</c> statement, selecting the result of a <paramref name="expression"/>.
Expand All @@ -40,7 +40,7 @@ public static ISelectQuery<TType, QueryContext<TType>> SelectExp<TType>(Expressi
/// <returns>
/// A <see cref="ISelectQuery{TNewType, TContext}"/>.
/// </returns>
public static ISelectQuery<TType, QueryContext<TType>> SelectExp<TType>(Expression<Func<QueryContext, TType?>> expression)
public static ISelectQuery<TType, QueryContext<TType>> SelectExpression<TType>(Expression<Func<QueryContext, TType?>> expression)
=> new QueryBuilder<TType>().SelectExp(expression);
}

Expand Down Expand Up @@ -74,7 +74,7 @@ public ISelectQuery<TResult, TContext> Select<TResult>(Action<ShapeBuilder<TResu
/// <returns>
/// A <see cref="ISelectQuery{TNewType, TContext}"/>.
/// </returns>
public ISelectQuery<TNewType, TContext> SelectExp<TNewType>(Expression<Func<TNewType?>> expression)
public ISelectQuery<TNewType, TContext> SelectExpression<TNewType>(Expression<Func<TNewType?>> expression)
{
AddNode<SelectNode>(new SelectContext(typeof(TType))
{
Expand All @@ -93,7 +93,7 @@ public ISelectQuery<TNewType, TContext> SelectExp<TNewType>(Expression<Func<TNew
/// <returns>
/// A <see cref="ISelectQuery{TNewType, TContext}"/>.
/// </returns>
public ISelectQuery<TNewType, TContext> SelectExp<TNewType>(Expression<Func<TContext, TNewType?>> expression)
public ISelectQuery<TNewType, TContext> SelectExpression<TNewType>(Expression<Func<TContext, TNewType?>> expression)
{
AddNode<SelectNode>(new SelectContext(typeof(TType))
{
Expand Down Expand Up @@ -149,7 +149,7 @@ internal ISelectQuery<TNewType, TContext> SelectExp<TNewType, TInitContext>(Expr
return EnterNewType<TNewType>();
}

ISelectQuery<TNewType, TContext> IQueryBuilder<TType, TContext>.SelectExp<TNewType, TQuery>(Expression<Func<TContext, TQuery?>> expression, Action<ShapeBuilder<TNewType>>? shape) where TQuery : default
ISelectQuery<TNewType, TContext> IQueryBuilder<TType, TContext>.SelectExpression<TNewType, TQuery>(Expression<Func<TContext, TQuery?>> expression, Action<ShapeBuilder<TNewType>>? shape) where TQuery : default
=> SelectExp(expression, shape);

ISelectQuery<TType, TContext> ISelectQuery<TType, TContext>.Filter(Expression<Func<TType, bool>> filter)
Expand Down
Loading

0 comments on commit 5090201

Please sign in to comment.