diff --git a/examples/EdgeDB.Examples.CSharp/Examples/QueryBuilder.cs b/examples/EdgeDB.Examples.CSharp/Examples/QueryBuilder.cs index f4361339..06955639 100644 --- a/examples/EdgeDB.Examples.CSharp/Examples/QueryBuilder.cs +++ b/examples/EdgeDB.Examples.CSharp/Examples/QueryBuilder.cs @@ -32,6 +32,35 @@ public async Task ExecuteAsync(EdgeDBClient client) { try { + var tests = await QueryBuilder + .Select(shape => + { + shape.IncludeMultiLink(x => x.Constraints); + shape.IncludeMultiLink(x => x.Properties, shape => + shape.Computeds((ctx, prop) => new + { + Cardinality = (string)ctx.UnsafeLocal("cardinality") == "One" + ? ctx.UnsafeLocal("required") + ? Cardinality.One + : Cardinality.AtMostOne + : ctx.UnsafeLocal("required") + ? Cardinality.AtLeastOne + : Cardinality.Many, + TargetId = ctx.UnsafeLocal("target.id"), + IsLink = ctx.Raw("[IS schema::Link]") != null, + IsExclusive = + ctx.Raw("exists (select .constraints filter .name = 'std::exclusive')"), + IsComputed = EdgeQL.Len(ctx.UnsafeLocal("computed_fields")) != 0, + IsReadonly = ctx.UnsafeLocal("readonly"), + HasDefault = + ctx.Raw( + "EXISTS .default or (\"std::sequence\" in .target[IS schema::ScalarType].ancestors.name)") + }) + ); + }) + .Filter((x, ctx) => !ctx.UnsafeLocal("builtin")) + .CompileAsync(client, true); + await QueryBuilderDemo(client); } catch (Exception x) @@ -46,14 +75,13 @@ private static async Task QueryBuilderDemo(EdgeDBClient client) var query = QueryBuilder.Select().Compile().Prettify(); // Adding a filter, orderby, offset, and limit - query = QueryBuilder + var queryTest = QueryBuilder .Select() .Filter(x => EdgeQL.ILike(x.Name, "e%")) .OrderByDesending(x => x.Name) .Offset(2) .Limit(10) - .Compile() - .Prettify(); + .Compile(true); // Specifying a shape query = QueryBuilder.Select(shape => @@ -117,12 +145,11 @@ private static async Task QueryBuilderDemo(EdgeDBClient client) .Prettify(); // Autogenerating unless conflict with introspection - query = (await QueryBuilder + queryTest = (await QueryBuilder .Insert(person) .UnlessConflict() .ElseReturn() - .CompileAsync(client)) - .Prettify(); + .CompileAsync(client, true)); // Bulk inserts var data = new Person[] diff --git a/src/EdgeDB.Net.QueryBuilder/Compiled/DebugCompiledQuery.cs b/src/EdgeDB.Net.QueryBuilder/Compiled/DebugCompiledQuery.cs index 730561f1..67543be6 100644 --- a/src/EdgeDB.Net.QueryBuilder/Compiled/DebugCompiledQuery.cs +++ b/src/EdgeDB.Net.QueryBuilder/Compiled/DebugCompiledQuery.cs @@ -1,12 +1,160 @@ -namespace EdgeDB.Compiled; +using System.Diagnostics; +using System.Text; +namespace EdgeDB.Compiled; + +[DebuggerDisplay("{DebugView}")] public sealed class DebugCompiledQuery : CompiledQuery { + public string DebugView { get; } + internal DebugCompiledQuery(string query, Dictionary variables, LinkedList markers) + : base(query, variables) + { + DebugView = CreateDebugText(query, variables, markers); + } - internal DebugCompiledQuery(string query, Dictionary variablesInternal, QueryWriter writer) - : base(query, variablesInternal) + private static string NumberCircle(int i) { + if (i <= 50) + { + return i == 0 ? "\u24ea" : ((char)('\u2460' + i - 1)).ToString(); + } + + return i.ToString(); + } + + private static string CreateDebugText(string query, Dictionary variables, + LinkedList markers) + { + var sb = new StringBuilder(); + + sb.AppendLine(query); + + if (markers.Count > 0) + { + var view = CreateMarkerView(markers); + var markerTexts = new Dictionary(); + var rows = new List(); + + foreach (var row in view) + { + var rowText = new StringBuilder($"{"".PadLeft(query.Length)}\n{"".PadLeft(query.Length)}"); + + + foreach (var column in row) + { + var size = column.Range.End.Value - column.Range.Start.Value; + + // bar + rowText.Remove(column.Range.Start.Value, size); + var barText = new StringBuilder($"\u2550".PadLeft(size - 3, '\u2550')); + barText.Insert(barText.Length / 2, "\u2566"); // T + barText.Insert(0, "\u255a"); // corner UR + barText.Insert(size - 1, "\u255d"); // corner UL + rowText.Insert(column.Range.Start.Value, barText); + + foreach (var prevRowText in rows) + { + prevRowText.Remove(column.Range.Start.Value, 1); + prevRowText.Insert(column.Range.Start.Value, '\u2551'); + prevRowText.Remove(query.Length + 1 + column.Range.Start.Value, 1); + prevRowText.Insert(query.Length + 1 + column.Range.Start.Value, '\u2551'); + + prevRowText.Remove(column.Range.End.Value - 1, 1); + prevRowText.Insert(column.Range.End.Value - 1, '\u2551'); + prevRowText.Remove(query.Length + column.Range.End.Value, 1); + prevRowText.Insert(query.Length + column.Range.End.Value, '\u2551'); + } + + // desc + var desc = $"{column.Marker.Type}: {column.Name}"; + string descriptionText; + + if (desc.Length > size) + { + var icon = NumberCircle(markerTexts.Count + 1); + markerTexts.Add(icon, desc); + descriptionText = icon; + } + else + { + descriptionText = desc; + } + + var position = query.Length + 1 // line 2 + + column.Range.Start.Value // start of the slice + + size / 2 // half of the slices' length : center of the slice + - (descriptionText.Length == 1 ? 1 : descriptionText.Length / 2); // half of the contents length : centers it + + rowText.Remove(position, descriptionText.Length); + rowText.Insert(position, descriptionText); + } + + rows.Add(rowText); + + } + + foreach (var row in rows) + { + sb.AppendLine(row.ToString()); + } + + if (markerTexts.Count > 0) + { + sb.AppendLine("Markers:"); + + foreach (var (name, value) in markerTexts) + { + sb.AppendLine($" - {name}: {value}"); + } + } + } + else + { + sb.AppendLine(); + } + + if (variables.Count > 0) + { + sb.AppendLine("Variables: "); + + foreach (var (name, value) in variables) + { + sb.AppendLine($" - {name} := {value}"); + } + } + + return sb.ToString(); + } + + private static List> CreateMarkerView(LinkedList spans) + { + 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(); + + while (ordered.TryDequeue(out var span)) + { + var head = row.LastOrDefault(); + if (head is null) + { + row.Add(span); + continue; + } + + if (head.Range.End.Value >= span.Range.Start.Value) + { + // overlap + result.Add(row); + row = [span]; + continue; + } + + row.Add(span); + } + result.Add(row); + return result; } } diff --git a/src/EdgeDB.Net.QueryBuilder/Interfaces/IQueryBuilder.cs b/src/EdgeDB.Net.QueryBuilder/Interfaces/IQueryBuilder.cs index 1b72b348..db18b299 100644 --- a/src/EdgeDB.Net.QueryBuilder/Interfaces/IQueryBuilder.cs +++ b/src/EdgeDB.Net.QueryBuilder/Interfaces/IQueryBuilder.cs @@ -305,18 +305,20 @@ public interface IQueryBuilder /// If the query requires introspection please use /// . /// + /// Whether or not to compile the query in a debug fashion, returning a . /// /// A . /// - CompiledQuery Compile(); + CompiledQuery Compile(bool debug = false); /// /// Compiles the current query asynchronously, allowing database introspection. /// /// The client to preform introspection with. + /// Whether or not to compile the query in a debug fashion, returning a . /// A cancellation token to cancel the asynchronous operation. /// A . - ValueTask CompileAsync(IEdgeDBQueryable edgedb, CancellationToken token = default); + ValueTask CompileAsync(IEdgeDBQueryable edgedb, bool debug = false, CancellationToken token = default); internal void CompileInternal(QueryWriter writer, CompileContext? context = null); } diff --git a/src/EdgeDB.Net.QueryBuilder/Lexical/Observers/LastNodeObserver.cs b/src/EdgeDB.Net.QueryBuilder/Lexical/Observers/LastNodeObserver.cs deleted file mode 100644 index 5dafcc0a..00000000 --- a/src/EdgeDB.Net.QueryBuilder/Lexical/Observers/LastNodeObserver.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace EdgeDB; - -internal sealed class LastNodeObserver : INodeObserver, IDisposable -{ - [MemberNotNullWhen(true, nameof(Value))] - public bool HasValue - => Value is not null; - - public LooseLinkedList.Node? Value { get; private set; } - - - private readonly QueryWriter _writer; - - public LastNodeObserver(QueryWriter writer) - { - _writer = writer; - _writer.AddObserver(this); - } - - public void OnAdd(LooseLinkedList.Node node) - { - Value = node; - } - - public void OnRemove(LooseLinkedList.Node node) - { - if (Value == node) - { - Value = null; - } - } - - public void Dispose() - { - _writer.RemoveObserver(this); - } -} diff --git a/src/EdgeDB.Net.QueryBuilder/Lexical/Observers/RangeNodeObserver.cs b/src/EdgeDB.Net.QueryBuilder/Lexical/Observers/RangeNodeObserver.cs new file mode 100644 index 00000000..9ffa2f24 --- /dev/null +++ b/src/EdgeDB.Net.QueryBuilder/Lexical/Observers/RangeNodeObserver.cs @@ -0,0 +1,52 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace EdgeDB; + +internal sealed class RangeNodeObserver : INodeObserver, IDisposable +{ + [MemberNotNullWhen(true, nameof(First))] + [MemberNotNullWhen(true, nameof(Last))] + public bool HasValue + => First is not null; + + public LooseLinkedList.Node? First { get; private set; } + + public LooseLinkedList.Node? Last { get; private set; } + + + private readonly QueryWriter _writer; + + public RangeNodeObserver(QueryWriter writer) + { + _writer = writer; + _writer.AddObserver(this); + } + + public void OnAdd(LooseLinkedList.Node node) + { + Last = node; + + if (First is not null) return; + + First = node; + } + + public void OnRemove(LooseLinkedList.Node node) + { + if (First == node) + { + First = null; + } + + if (Last == node) + { + Last = null; + } + } + + public void Dispose() + { + _writer.RemoveObserver(this); + } +} diff --git a/src/EdgeDB.Net.QueryBuilder/Lexical/QuerySpan.cs b/src/EdgeDB.Net.QueryBuilder/Lexical/QuerySpan.cs new file mode 100644 index 00000000..ac303614 --- /dev/null +++ b/src/EdgeDB.Net.QueryBuilder/Lexical/QuerySpan.cs @@ -0,0 +1,11 @@ +namespace EdgeDB; + +internal sealed class QuerySpan(Range range, string content, Marker marker, string name) +{ + public Range Range { get; } = range; + + public string Content { get; } = content; + + public Marker Marker { get; } = marker; + public string Name { get; } = name; +} diff --git a/src/EdgeDB.Net.QueryBuilder/Lexical/QueryWriter.cs b/src/EdgeDB.Net.QueryBuilder/Lexical/QueryWriter.cs index fa44c2f3..81b2a4ef 100644 --- a/src/EdgeDB.Net.QueryBuilder/Lexical/QueryWriter.cs +++ b/src/EdgeDB.Net.QueryBuilder/Lexical/QueryWriter.cs @@ -70,9 +70,10 @@ private ValueNode AddBeforeTracked(in Value value) private ValueNode AddTracked(in Value value, bool after) { - if (value.TryProxy(this, out var node)) + if (value.TryProxy(this, out var head, out _)) { - _track = node; + // track is already updated. + return head; } else if (_track is null) _track = _tokens.AddFirst(in value); //Set(ref _tokens.AddFirst(in value)); @@ -368,6 +369,60 @@ public StringBuilder Compile(StringBuilder? builder = null) return builder; } + private sealed class ActiveMarkerTrack(int index, Marker marker, string name, StringBuilder builder, int count) + { + public string Name { get; } = name; + public int Index { get; } = index; + public Marker Marker { get; } = marker; + public StringBuilder Builder { get; } = builder; + public bool TryWrite(Value value) + { + if (count == 0) + return false; + + value.WriteTo(Builder); + count--; + return true; + } + } + + public (string Query, LinkedList Markers) CompileDebug() + { + var query = new StringBuilder(); + var activeMarkers = new List(); + var spans = new LinkedList(); + var markers = new HashSet<(string, Marker)>(Markers.SelectMany(x => x.Value.Select(y => (x.Key, y)))); + + var current = _tokens.First; + + while (current is not null) + { + foreach (var activeMarker in activeMarkers.ToArray()) + { + if (activeMarker.TryWrite(current.Value)) continue; + + activeMarkers.Remove(activeMarker); + var content = activeMarker.Builder.ToString(); + spans.AddLast(new QuerySpan(activeMarker.Index..(activeMarker.Index + content.Length), content, activeMarker.Marker, activeMarker.Name)); + } + + foreach (var startingMarker in markers.Where(x => x.Item2.Start == current)) + { + markers.Remove(startingMarker); + var sb = new StringBuilder(); + + activeMarkers.Add(new (query.Length, startingMarker.Item2, startingMarker.Item1, sb, startingMarker.Item2.Size - 1)); + current.Value.WriteTo(sb); + } + + current.Value.WriteTo(query); + + current = current.Next; + } + + return (query.ToString(), spans); + } + public void Dispose() { Markers.Clear(); diff --git a/src/EdgeDB.Net.QueryBuilder/Lexical/Value.cs b/src/EdgeDB.Net.QueryBuilder/Lexical/Value.cs index dccd43f0..38302f36 100644 --- a/src/EdgeDB.Net.QueryBuilder/Lexical/Value.cs +++ b/src/EdgeDB.Net.QueryBuilder/Lexical/Value.cs @@ -5,7 +5,7 @@ namespace EdgeDB; -//[DebuggerDisplay("{DebugDisplay()}")] +[DebuggerDisplay("{DebugDisplay()}")] internal readonly struct Value { [MemberNotNullWhen(false, nameof(_callback))] @@ -41,21 +41,26 @@ public Value(object? value) public static Value Of(WriterProxy proxy) => new(proxy); - public bool TryProxy(QueryWriter writer, [MaybeNullWhen(false)] out LooseLinkedList.Node result) + public bool TryProxy( + QueryWriter writer, + [MaybeNullWhen(false)] out LooseLinkedList.Node first, + [MaybeNullWhen(false)] out LooseLinkedList.Node last) { if (IsScalar) { - result = null; + first = null; + last = null; return false; } - using var nodeObserver = new LastNodeObserver(writer); + using var nodeObserver = new RangeNodeObserver(writer); _callback(writer); if (!nodeObserver.HasValue) throw new InvalidOperationException("Provided proxy wrote no value"); - result = nodeObserver.Value; + first = nodeObserver.First; + last = nodeObserver.Last; return true; } diff --git a/src/EdgeDB.Net.QueryBuilder/QueryBuilder.cs b/src/EdgeDB.Net.QueryBuilder/QueryBuilder.cs index 4e18a50e..166eaeab 100644 --- a/src/EdgeDB.Net.QueryBuilder/QueryBuilder.cs +++ b/src/EdgeDB.Net.QueryBuilder/QueryBuilder.cs @@ -1,4 +1,5 @@ using EdgeDB.Builders; +using EdgeDB.Compiled; using EdgeDB.Interfaces; using EdgeDB.Interfaces.Queries; using EdgeDB.QueryNodes; @@ -206,7 +207,11 @@ internal CompiledQuery CompileInternal(CompileContext? context = null) CompileInternal(writer, context); - 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); + } internal void CompileInternal(QueryWriter writer, CompileContext? context = null) @@ -274,12 +279,12 @@ internal void CompileInternal(QueryWriter writer, CompileContext? context = null } /// - public CompiledQuery Compile() - => CompileInternal(); + public CompiledQuery Compile(bool debug = false) + => CompileInternal(new CompileContext {Debug = debug}); /// - public ValueTask CompileAsync(IEdgeDBQueryable edgedb, CancellationToken token = default) - => IntrospectAndCompileAsync(edgedb, token); + public ValueTask CompileAsync(IEdgeDBQueryable edgedb, bool debug = false, CancellationToken token = default) + => IntrospectAndCompileAsync(edgedb, debug, token); /// /// Preforms introspection and then compiles this query builder into a . @@ -290,12 +295,12 @@ public ValueTask CompileAsync(IEdgeDBQueryable edgedb, Cancellati /// A ValueTask representing the (a)sync introspection and compiling operation. /// The result is the compiled form of this query builder. /// - private async ValueTask IntrospectAndCompileAsync(IEdgeDBQueryable edgedb, CancellationToken token) + 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); - var result = Compile(); + var result = Compile(debug); _nodes.Clear(); _queryGlobals.Clear(); @@ -546,7 +551,7 @@ private QueryBuilder Else(Func, async Task> IMultiCardinalityExecutable.ExecuteAsync(IEdgeDBQueryable edgedb, Capabilities? capabilities, CancellationToken token) { - var result = await IntrospectAndCompileAsync(edgedb, token).ConfigureAwait(false); + var result = await IntrospectAndCompileAsync(edgedb, false, token).ConfigureAwait(false); return await edgedb.QueryAsync(result.Query, result.RawVariables, capabilities, token).ConfigureAwait(false); } @@ -554,21 +559,21 @@ private QueryBuilder Else(Func, async Task ISingleCardinalityExecutable.ExecuteAsync(IEdgeDBQueryable edgedb, Capabilities? capabilities, CancellationToken token) { - var result = await IntrospectAndCompileAsync(edgedb, token).ConfigureAwait(false); + var result = await IntrospectAndCompileAsync(edgedb, false, token).ConfigureAwait(false); return await edgedb.QuerySingleAsync(result.Query, result.RawVariables, capabilities, token).ConfigureAwait(false); } /// async Task IMultiCardinalityExecutable.ExecuteSingleAsync(IEdgeDBQueryable edgedb, Capabilities? capabilities, CancellationToken token) { - var result = await IntrospectAndCompileAsync(edgedb, token).ConfigureAwait(false); + var result = await IntrospectAndCompileAsync(edgedb, false, token).ConfigureAwait(false); return await edgedb.QuerySingleAsync(result.Query, result.RawVariables, capabilities, token).ConfigureAwait(false); } /// async Task IMultiCardinalityExecutable.ExecuteRequiredSingleAsync(IEdgeDBQueryable edgedb, Capabilities? capabilities, CancellationToken token) { - var result = await IntrospectAndCompileAsync(edgedb, token).ConfigureAwait(false); + var result = await IntrospectAndCompileAsync(edgedb, false, token).ConfigureAwait(false); return await edgedb.QueryRequiredSingleAsync(result.Query, result.RawVariables, capabilities, token).ConfigureAwait(false); } diff --git a/src/EdgeDB.Net.QueryBuilder/QueryBuilder/CompileContext.cs b/src/EdgeDB.Net.QueryBuilder/QueryBuilder/CompileContext.cs index d29b90da..cc180b6f 100644 --- a/src/EdgeDB.Net.QueryBuilder/QueryBuilder/CompileContext.cs +++ b/src/EdgeDB.Net.QueryBuilder/QueryBuilder/CompileContext.cs @@ -7,4 +7,5 @@ internal sealed record CompileContext public bool IncludeGlobalsInQuery { get; init; } = true; public bool IncludeAutogeneratedNodes { get; init; } = true; public Action? PreFinalizerModifier { get; init; } + public bool Debug { get; init; } } diff --git a/src/EdgeDB.Net.Queryable/Extensions/QueryableExtensions.cs b/src/EdgeDB.Net.Queryable/Extensions/QueryableExtensions.cs index b3773e2a..f956e060 100644 --- a/src/EdgeDB.Net.Queryable/Extensions/QueryableExtensions.cs +++ b/src/EdgeDB.Net.Queryable/Extensions/QueryableExtensions.cs @@ -19,7 +19,7 @@ public static class QueryableExtensions var transient = queryable.Provider.ToTransient(); - var builtQuery = await transient.Compile().CompileAsync(client, token); + var builtQuery = await transient.Compile().CompileAsync(client, token: token); return await client.QueryAsync(builtQuery.Query, builtQuery.RawVariables, token: token); } @@ -34,7 +34,7 @@ public static class QueryableExtensions var transient = queryable.Provider.ToTransient(); - var builtQuery = await transient.Compile().CompileAsync(client, token); + var builtQuery = await transient.Compile().CompileAsync(client, token: token); return await client.QuerySingleAsync(builtQuery.Query, builtQuery.RawVariables, token: token); } diff --git a/src/EdgeDB.Net.Queryable/GenericlessQueryBuilder.cs b/src/EdgeDB.Net.Queryable/GenericlessQueryBuilder.cs index ea723e31..aec37fa7 100644 --- a/src/EdgeDB.Net.Queryable/GenericlessQueryBuilder.cs +++ b/src/EdgeDB.Net.Queryable/GenericlessQueryBuilder.cs @@ -376,7 +376,7 @@ private async ValueTask IntrospectAndBuildAsync(IEdgeDBQueryable _schemaInfo ??= await SchemaIntrospector.GetOrCreateSchemaIntrospectionAsync(edgedb, token) .ConfigureAwait(false); - var result = Compile(); + var result = Compile(false); _nodes.Clear(); _queryGlobals.Clear(); @@ -479,11 +479,11 @@ internal void CompileInternal(QueryWriter writer, CompileContext? context = null } /// - public CompiledQuery Compile() + public CompiledQuery Compile(bool debug) => CompileInternal(); /// - public ValueTask CompileAsync(IEdgeDBQueryable edgedb, CancellationToken token = default) + public ValueTask CompileAsync(IEdgeDBQueryable edgedb, bool debug, CancellationToken token = default) => IntrospectAndBuildAsync(edgedb, token); #endregion