diff --git a/src/Core/Queries/GremlinQuery.ProjectBuilder.cs b/src/Core/Queries/GremlinQuery.ProjectBuilder.cs index 1ffca86637..3cb8fd42a3 100644 --- a/src/Core/Queries/GremlinQuery.ProjectBuilder.cs +++ b/src/Core/Queries/GremlinQuery.ProjectBuilder.cs @@ -131,7 +131,29 @@ IProjectMapBuilder, T1, TItem1> IProjectMapBuilder< .Push(new ProjectStep.ByTraversalStep(_outer .Continue() .With(projection) - .Build(static (_, traversal) => traversal))), + .Build(static (_, traversal) => + { + // by() takes the first result of its traversal for the one element it is given, so a map() or + // a local() that is all of that traversal changes nothing about it. A lone values('key') keeps + // what is around it: TinkerPop turns by(__.values('key')) into by('key'), which fails on more + // than one value. + while (true) + { + Traversal innerTraversal; + + if (traversal is [MapStep mapStep]) + innerTraversal = mapStep.Traversal; + else if (traversal is [LocalStep localStep]) + innerTraversal = localStep.Traversal; + else + return traversal; + + if (innerTraversal is [] or [ValuesStep { Keys.Length: 1 }]) + return traversal; + + traversal = innerTraversal; + } + }))), _emptyProjectionProtectionDecoratorSteps); private ProjectBuilder ByExpression(Expression projection, string? name = null) => projection is LambdaExpression lambdaExpression && lambdaExpression.IsIdentityExpression() diff --git a/src/Core/Serialization/Serializer.cs b/src/Core/Serialization/Serializer.cs index e11aaa76ba..88f889fbc5 100644 --- a/src/Core/Serialization/Serializer.cs +++ b/src/Core/Serialization/Serializer.cs @@ -544,15 +544,7 @@ private static ITransformer AddDefaultStepConverters(this ITransformer serialize return CreateInstruction("property", recurse, env, parameters); }) .Add((step, env, _, recurse) => CreateInstruction("project", recurse, env, step.Projections)) - .Add((step, env, _, recurse) => - { - var traversal = step.Traversal; - - if (traversal is [LocalStep localStep]) - traversal = localStep.Traversal; - - return CreateInstruction("by", recurse, env, traversal); - }) + .Add((step, env, _, recurse) => CreateInstruction("by", recurse, env, step.Traversal)) .Add((step, env, _, recurse) => CreateInstruction("by", recurse, env, step.Key)) .Add((step, env, _, recurse) => step.Scope.Equals(Scope.Local) ? CreateInstruction("range", recurse, env, step.Scope, step.Lower, step.Upper) diff --git a/test/Core.Tests/Issues/Github/Issue2464.Local_Map_Out_Count.verified.txt b/test/Core.Tests/Issues/Github/Issue2464.Local_Map_Out_Count.verified.txt new file mode 100644 index 0000000000..f1b05d76da --- /dev/null +++ b/test/Core.Tests/Issues/Github/Issue2464.Local_Map_Out_Count.verified.txt @@ -0,0 +1 @@ +g.V().hasLabel('Person').project('Item1','Item2').by(__.out('WorksFor').count()).by(__.id()) \ No newline at end of file diff --git a/test/Core.Tests/Issues/Github/Issue2464.Local_Out_Count.verified.txt b/test/Core.Tests/Issues/Github/Issue2464.Local_Out_Count.verified.txt new file mode 100644 index 0000000000..f1b05d76da --- /dev/null +++ b/test/Core.Tests/Issues/Github/Issue2464.Local_Out_Count.verified.txt @@ -0,0 +1 @@ +g.V().hasLabel('Person').project('Item1','Item2').by(__.out('WorksFor').count()).by(__.id()) \ No newline at end of file diff --git a/test/Core.Tests/Issues/Github/Issue2464.Local_Out_Count_with_empty_projection_value_protection.verified.txt b/test/Core.Tests/Issues/Github/Issue2464.Local_Out_Count_with_empty_projection_value_protection.verified.txt new file mode 100644 index 0000000000..63d5f14f97 --- /dev/null +++ b/test/Core.Tests/Issues/Github/Issue2464.Local_Out_Count_with_empty_projection_value_protection.verified.txt @@ -0,0 +1 @@ +g.V().hasLabel('Person').project('Item1','Item2').by(__.out('WorksFor').count().limit(1).fold()).by(__.id().limit(1).fold()).map(__.unfold().group().by(__.select(keys)).by(__.select(values).unfold())) \ No newline at end of file diff --git a/test/Core.Tests/Issues/Github/Issue2464.Local_Values.verified.txt b/test/Core.Tests/Issues/Github/Issue2464.Local_Values.verified.txt new file mode 100644 index 0000000000..f0a33e27f2 --- /dev/null +++ b/test/Core.Tests/Issues/Github/Issue2464.Local_Values.verified.txt @@ -0,0 +1 @@ +g.V().hasLabel('Person').project('Item1','Item2').by(__.local(__.values('Name'))).by(__.id()) \ No newline at end of file diff --git a/test/Core.Tests/Issues/Github/Issue2464.Map_Local_Values.verified.txt b/test/Core.Tests/Issues/Github/Issue2464.Map_Local_Values.verified.txt new file mode 100644 index 0000000000..f0a33e27f2 --- /dev/null +++ b/test/Core.Tests/Issues/Github/Issue2464.Map_Local_Values.verified.txt @@ -0,0 +1 @@ +g.V().hasLabel('Person').project('Item1','Item2').by(__.local(__.values('Name'))).by(__.id()) \ No newline at end of file diff --git a/test/Core.Tests/Issues/Github/Issue2464.Map_Map_Out_Count.verified.txt b/test/Core.Tests/Issues/Github/Issue2464.Map_Map_Out_Count.verified.txt new file mode 100644 index 0000000000..f1b05d76da --- /dev/null +++ b/test/Core.Tests/Issues/Github/Issue2464.Map_Map_Out_Count.verified.txt @@ -0,0 +1 @@ +g.V().hasLabel('Person').project('Item1','Item2').by(__.out('WorksFor').count()).by(__.id()) \ No newline at end of file diff --git a/test/Core.Tests/Issues/Github/Issue2464.Map_Out.verified.txt b/test/Core.Tests/Issues/Github/Issue2464.Map_Out.verified.txt new file mode 100644 index 0000000000..67bd462b10 --- /dev/null +++ b/test/Core.Tests/Issues/Github/Issue2464.Map_Out.verified.txt @@ -0,0 +1 @@ +g.V().hasLabel('Person').project('Item1','Item2').by(__.out('WorksFor')).by(__.id()).project('Item1','Item2').by(__.select('Item1').project('id','label','properties').by(id).by(label).by(__.properties().group().by(label).by(__.project('id','label','value','properties').by(id).by(label).by(value).by(__.valueMap()).fold()))).by(__.select('Item2')) \ No newline at end of file diff --git a/test/Core.Tests/Issues/Github/Issue2464.Map_Out_Count.verified.txt b/test/Core.Tests/Issues/Github/Issue2464.Map_Out_Count.verified.txt new file mode 100644 index 0000000000..f1b05d76da --- /dev/null +++ b/test/Core.Tests/Issues/Github/Issue2464.Map_Out_Count.verified.txt @@ -0,0 +1 @@ +g.V().hasLabel('Person').project('Item1','Item2').by(__.out('WorksFor').count()).by(__.id()) \ No newline at end of file diff --git a/test/Core.Tests/Issues/Github/Issue2464.Map_Out_Count_by_name.verified.txt b/test/Core.Tests/Issues/Github/Issue2464.Map_Out_Count_by_name.verified.txt new file mode 100644 index 0000000000..e2064fb858 --- /dev/null +++ b/test/Core.Tests/Issues/Github/Issue2464.Map_Out_Count_by_name.verified.txt @@ -0,0 +1 @@ +g.V().hasLabel('Person').project('count','id').by(__.out('WorksFor').count()).by(__.id()) \ No newline at end of file diff --git a/test/Core.Tests/Issues/Github/Issue2464.Map_Out_Count_followed_by_step.verified.txt b/test/Core.Tests/Issues/Github/Issue2464.Map_Out_Count_followed_by_step.verified.txt new file mode 100644 index 0000000000..6e4f022dc8 --- /dev/null +++ b/test/Core.Tests/Issues/Github/Issue2464.Map_Out_Count_followed_by_step.verified.txt @@ -0,0 +1 @@ +g.V().hasLabel('Person').project('Item1','Item2').by(__.map(__.out('WorksFor').count()).fold()).by(__.id()) \ No newline at end of file diff --git a/test/Core.Tests/Issues/Github/Issue2464.Map_Out_Count_with_empty_projection_value_protection.verified.txt b/test/Core.Tests/Issues/Github/Issue2464.Map_Out_Count_with_empty_projection_value_protection.verified.txt new file mode 100644 index 0000000000..63d5f14f97 --- /dev/null +++ b/test/Core.Tests/Issues/Github/Issue2464.Map_Out_Count_with_empty_projection_value_protection.verified.txt @@ -0,0 +1 @@ +g.V().hasLabel('Person').project('Item1','Item2').by(__.out('WorksFor').count().limit(1).fold()).by(__.id().limit(1).fold()).map(__.unfold().group().by(__.select(keys)).by(__.select(values).unfold())) \ No newline at end of file diff --git a/test/Core.Tests/Issues/Github/Issue2464.Map_Values.verified.txt b/test/Core.Tests/Issues/Github/Issue2464.Map_Values.verified.txt new file mode 100644 index 0000000000..5f54e4b5c1 --- /dev/null +++ b/test/Core.Tests/Issues/Github/Issue2464.Map_Values.verified.txt @@ -0,0 +1 @@ +g.V().hasLabel('Person').project('Item1','Item2').by(__.map(__.values('Name'))).by(__.id()) \ No newline at end of file diff --git a/test/Core.Tests/Issues/Github/Issue2464.Repro.verified.txt b/test/Core.Tests/Issues/Github/Issue2464.Repro.verified.txt new file mode 100644 index 0000000000..983f99ff0f --- /dev/null +++ b/test/Core.Tests/Issues/Github/Issue2464.Repro.verified.txt @@ -0,0 +1 @@ +g.V().hasLabel('Person').project('Item1','Item2').by(__.values('Name').limit(1)).by(__.id()) \ No newline at end of file diff --git a/test/Core.Tests/Issues/Github/Issue2464.cs b/test/Core.Tests/Issues/Github/Issue2464.cs new file mode 100644 index 0000000000..edbce3a5d0 --- /dev/null +++ b/test/Core.Tests/Issues/Github/Issue2464.cs @@ -0,0 +1,196 @@ +using ExRam.Gremlinq.Core.Models; +using ExRam.Gremlinq.Tests.Entities; +using ExRam.Gremlinq.Tests.Infrastructure; +using static ExRam.Gremlinq.Core.GremlinQuerySource; + +namespace ExRam.Gremlinq.Core.Tests +{ + public class Issue2464 : GremlinqTestBase + { + private readonly IGremlinQuerySource _g; + + public Issue2464() : base(new DebugGremlinQueryVerifier()) + { + _g = g + .ConfigureEnvironment(env => env + .UseModel(GraphModel + .FromBaseTypes())); + } + + [Fact] + public Task Repro() => _g + .V() + .Project(__ => __ + .ToTuple() + .By(__ => __ + .Map(__ => __ + .Values(x => x.Name!) + .Limit(1))) + .By(__ => __ + .Map(__ => __ + .Id()))) + .Verify(); + + [Fact] + public Task Map_Out_Count() => _g + .V() + .Project(__ => __ + .ToTuple() + .By(__ => __ + .Map(__ => __ + .Out() + .Count())) + .By(__ => __ + .Id())) + .Verify(); + + [Fact] + public Task Map_Map_Out_Count() => _g + .V() + .Project(__ => __ + .ToTuple() + .By(__ => __ + .Map(__ => __ + .Map(__ => __ + .Out() + .Count()))) + .By(__ => __ + .Id())) + .Verify(); + + [Fact] + public Task Map_Values() => _g + .V() + .Project(__ => __ + .ToTuple() + .By(__ => __ + .Map(__ => __ + .Values(x => x.Name!))) + .By(__ => __ + .Id())) + .Verify(); + + [Fact] + public Task Map_Out() => _g + .V() + .Project(__ => __ + .ToTuple() + .By(__ => __ + .Map(__ => __ + .Out())) + .By(__ => __ + .Id())) + .Verify(); + + [Fact] + public Task Local_Out_Count() => _g + .V() + .Project(__ => __ + .ToTuple() + .By(__ => __ + .Local(__ => __ + .Out() + .Count())) + .By(__ => __ + .Id())) + .Verify(); + + [Fact] + public Task Local_Out_Count_with_empty_projection_value_protection() => _g + .ConfigureEnvironment(env => env + .ConfigureOptions(options => options + .SetValue(GremlinqOption.EnableEmptyProjectionValueProtection, true))) + .V() + .Project(__ => __ + .ToTuple() + .By(__ => __ + .Local(__ => __ + .Out() + .Count())) + .By(__ => __ + .Id())) + .Verify(); + + [Fact] + public Task Local_Values() => _g + .V() + .Project(__ => __ + .ToTuple() + .By(__ => __ + .Local(__ => __ + .Values(x => x.Name!))) + .By(__ => __ + .Id())) + .Verify(); + + [Fact] + public Task Map_Local_Values() => _g + .V() + .Project(__ => __ + .ToTuple() + .By(__ => __ + .Map(__ => __ + .Local(__ => __ + .Values(x => x.Name!)))) + .By(__ => __ + .Id())) + .Verify(); + + [Fact] + public Task Local_Map_Out_Count() => _g + .V() + .Project(__ => __ + .ToTuple() + .By(__ => __ + .Local(__ => __ + .Map(__ => __ + .Out() + .Count()))) + .By(__ => __ + .Id())) + .Verify(); + + [Fact] + public Task Map_Out_Count_followed_by_step() => _g + .V() + .Project(__ => __ + .ToTuple() + .By(__ => __ + .Map(__ => __ + .Out() + .Count()) + .Fold()) + .By(__ => __ + .Id())) + .Verify(); + + [Fact] + public Task Map_Out_Count_with_empty_projection_value_protection() => _g + .ConfigureEnvironment(env => env + .ConfigureOptions(options => options + .SetValue(GremlinqOption.EnableEmptyProjectionValueProtection, true))) + .V() + .Project(__ => __ + .ToTuple() + .By(__ => __ + .Map(__ => __ + .Out() + .Count())) + .By(__ => __ + .Id())) + .Verify(); + + [Fact] + public Task Map_Out_Count_by_name() => _g + .V() + .Project(__ => __ + .ToDynamic() + .By("count", __ => __ + .Map(__ => __ + .Out() + .Count())) + .By("id", __ => __ + .Id())) + .Verify(); + } +} diff --git a/test/Core.Tests/Serialization/EmptyProjectionValueProtectionSerializationTest.Project_with_local.verified.txt b/test/Core.Tests/Serialization/EmptyProjectionValueProtectionSerializationTest.Project_with_local.verified.txt index f86ae965ca..bbd1aca164 100644 --- a/test/Core.Tests/Serialization/EmptyProjectionValueProtectionSerializationTest.Project_with_local.verified.txt +++ b/test/Core.Tests/Serialization/EmptyProjectionValueProtectionSerializationTest.Project_with_local.verified.txt @@ -40,18 +40,9 @@ { StepInstructions: [ { - OperatorName: local, + OperatorName: constant, Arguments: [ - { - StepInstructions: [ - { - OperatorName: constant, - Arguments: [ - item2 - ] - } - ] - } + item2 ] }, {