Skip to content

Commit cff743f

Browse files
authored
Add the ability to CALL other queries (#411)
1 parent fa47b70 commit cff743f

File tree

4 files changed

+238
-4
lines changed

4 files changed

+238
-4
lines changed

Neo4jClient.Tests/Cypher/CypherFluentQueryCallTests.cs

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using FluentAssertions;
25
using Neo4jClient.Cypher;
36
using NSubstitute;
47
using Xunit;
@@ -8,6 +11,11 @@ namespace Neo4jClient.Tests.Cypher
811

912
public class CypherFluentQueryCallTests : IClassFixture<CultureInfoSetupFixture>
1013
{
14+
private class Foo
15+
{
16+
public int Id { get; set; }
17+
}
18+
1119
private static IRawGraphClient GraphClient_30
1220
{
1321
get
@@ -44,5 +52,165 @@ public void ThrowsInvalidOperationException_WhenClientVersionIsLessThan_30()
4452

4553
Assert.Throws<InvalidOperationException>(() => new CypherFluentQuery(client).Call("apoc.sp").Query);
4654
}
55+
56+
[Fact]
57+
public void Call_SubQueriesAsLambda()
58+
{
59+
const string expected = @"CALL { MATCH (n)
60+
RETURN count(n) AS c }
61+
RETURN c";
62+
63+
var client = GraphClient_30;
64+
65+
var query = new CypherFluentQuery(client)
66+
.Match("(n)")
67+
.Return(n => new {c = n.Count()});
68+
69+
var callQuery = new CypherFluentQuery(client)
70+
.Call(() => query)
71+
.Return(c => c.As<long>());
72+
73+
callQuery.Query.QueryText.Should().Be(expected);
74+
}
75+
76+
[Fact]
77+
public void Call_SubQueriesAsLambdaWithParameters()
78+
{
79+
const string expected = @"CALL { MATCH (n)
80+
WHERE (n.Id = $p0)
81+
RETURN count(n) AS c }
82+
RETURN c";
83+
84+
var client = GraphClient_30;
85+
var query = new CypherFluentQuery(client)
86+
.Match("(n)")
87+
.Where((Foo n) => n.Id == 1)
88+
.Return(n => new {c = n.Count()});
89+
90+
var callQuery = new CypherFluentQuery(client)
91+
.Call(() => query)
92+
.Return(c => c.As<long>());
93+
94+
callQuery.Query.QueryText.Should().Be(expected);
95+
callQuery.Query.QueryParameters.Should().HaveCount(1);
96+
}
97+
98+
[Fact]
99+
public void Call_SubQueriesAsLambdaWithParametersPriorToCall()
100+
{
101+
const string expected = @"MATCH (x)
102+
WHERE (x.Id = $p0)
103+
CALL { MATCH (n)
104+
WHERE (n.Id = $p1)
105+
RETURN count(n) AS c }
106+
RETURN c";
107+
108+
var client = GraphClient_30;
109+
var query = new CypherFluentQuery(client)
110+
.Match("(n)")
111+
.Where((Foo n) => n.Id == 1)
112+
.Return(n => new {c = n.Count()});
113+
114+
var callQuery = new CypherFluentQuery(client)
115+
.Match("(x)")
116+
.Where((Foo x) => x.Id == 2)
117+
.Call(() => query)
118+
.Return(c => c.As<long>());
119+
120+
callQuery.Query.QueryText.Should().Be(expected);
121+
callQuery.Query.QueryParameters.Should().HaveCount(2);
122+
}
123+
124+
[Fact]
125+
public void Call_SubQueriesAsLambdaWithParametersAfterToCall()
126+
{
127+
const string expected = @"CALL { MATCH (n)
128+
WHERE (n.Id = $p0)
129+
RETURN count(n) AS c }
130+
MATCH (x)
131+
WHERE (x.Id = $p1)
132+
RETURN c";
133+
134+
var client = GraphClient_30;
135+
var query = new CypherFluentQuery(client)
136+
.Match("(n)")
137+
.Where((Foo n) => n.Id == 1)
138+
.Return(n => new {c = n.Count()});
139+
140+
var callQuery = new CypherFluentQuery(client)
141+
.Call(() => query)
142+
.Match($"(x)")
143+
.Where((Foo x) => x.Id == 2)
144+
.Return(c => c.As<long>());
145+
146+
callQuery.Query.QueryText.Should().Be(expected);
147+
callQuery.Query.QueryParameters.Should().HaveCount(2);
148+
}
149+
150+
[Fact]
151+
public void Call_SubQueriesAsLambdaWithParametersPriorToCall_WholeWord()
152+
{
153+
const string expected = @"MATCH (x)
154+
WHERE (x.Id = $p0)
155+
CALL { MATCH (n)
156+
WHERE (n.Id = $p1)
157+
AND (n.Something = $p2)
158+
RETURN count(n) AS c }
159+
RETURN c";
160+
161+
var client = GraphClient_30;
162+
var query = new CypherFluentQuery(client)
163+
.Match("(n)")
164+
.Where("(n.Id = $p0)")
165+
.AndWhere("(n.Something = $p1)")
166+
.WithParam("p0", 1)
167+
.WithParam("p1", 2)
168+
.Return(n => new {c = n.Count()});
169+
170+
var callQuery = new CypherFluentQuery(client)
171+
.Match("(x)")
172+
.Where("(x.Id = $p0)")
173+
.WithParam("p0", 3)
174+
.Call(() => query)
175+
.Return(c => c.As<long>());
176+
177+
callQuery.Query.QueryText.Should().Be(expected);
178+
callQuery.Query.QueryParameters.Should().HaveCount(3);
179+
callQuery.Query.QueryParameters.Should().ContainKeys("p0", "p1", "p2");
180+
}
181+
182+
[Fact]
183+
public void Call_SubQueriesAsLambdaWithParametersPriorAndAfterToCall_WholeWord()
184+
{
185+
const string expected = @"MATCH (x)
186+
WHERE (x.Id = $p0)
187+
CALL { MATCH (n)
188+
WHERE (n.Id = $p1)
189+
AND (n.Something = $p2)
190+
RETURN count(n) AS c }
191+
WHERE (y.Id = $p3)
192+
RETURN c";
193+
194+
var client = GraphClient_30;
195+
var query = new CypherFluentQuery(client)
196+
.Match("(n)")
197+
.Where("(n.Id = $p0)")
198+
.AndWhere("(n.Something = $p1)")
199+
.WithParam("p0", 1)
200+
.WithParam("p1", 2)
201+
.Return(n => new {c = n.Count()});
202+
203+
var callQuery = new CypherFluentQuery(client)
204+
.Match("(x)")
205+
.Where((IdClass x) => x.Id == 1)
206+
.Call(() => query)
207+
.Where((IdClass y) => y.Id == 1)
208+
.Return(c => c.As<long>());
209+
210+
callQuery.Query.QueryText.Should().Be(expected);
211+
callQuery.Query.QueryParameters.Should().HaveCount(4);
212+
callQuery.Query.QueryParameters.Should().ContainKeys("p0", "p1", "p2", "p3");
213+
}
214+
private class IdClass {public int Id { get;set; }}
47215
}
48216
}

Neo4jClient/Cypher/CypherFluentQuery.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Collections.Specialized;
88
using System.ComponentModel.DataAnnotations;
99
using System.Reflection;
10+
using System.Text.RegularExpressions;
1011
using System.Threading.Tasks;
1112

1213
namespace Neo4jClient.Cypher
@@ -55,6 +56,14 @@ public ICypherFluentQuery WithDatabase(string databaseName)
5556
return this;
5657
}
5758

59+
public ICypherFluentQuery Call<T>(Func<ICypherFluentQuery<T>> subQuery)
60+
{
61+
var query = subQuery().Query;
62+
return Call($"{{ {query.QueryText} }}", query.QueryParameters);
63+
}
64+
65+
66+
5867
public ICypherFluentQuery Read
5968
{
6069
get
@@ -213,6 +222,54 @@ public ICypherFluentQuery Call(string storedProcedureText)
213222
w.AppendClause($"CALL {storedProcedureText}"));
214223
}
215224

225+
private ICypherFluentQuery Call(string storedProcedureText, IDictionary<string, object> parameters )
226+
{
227+
if (!Client.CypherCapabilities.SupportsStoredProcedures)
228+
throw new InvalidOperationException("CALL not supported in Neo4j versions older than 3.0");
229+
230+
if(string.IsNullOrWhiteSpace(storedProcedureText))
231+
throw new ArgumentException("The stored procedure to call can't be null or whitespace.", nameof(storedProcedureText));
232+
233+
var newParameters = RebaseParameters(storedProcedureText, parameters, Query.QueryParameters, out var newStoredProcText, Query.QueryParameters.Count);
234+
235+
return Mutate(w =>
236+
{
237+
w.AppendClause($"CALL {newStoredProcText}");
238+
w.CreateParameters(newParameters);
239+
});
240+
}
241+
242+
private static IDictionary<string, object> RebaseParameters( string storedProcedureText, IEnumerable<KeyValuePair<string, object>> parametersIn, IDictionary<string, object> queryQueryParameters, out string newStoredProcText, int rebaseFrom = 0)
243+
{
244+
const string regexFormat = @"(?<start>^|[\s\(])(?<parameter>\${0})(?<end>$|[\s\)])";
245+
var parameters = parametersIn.ToList();
246+
247+
if (!parameters.Select(x => x.Key).Any(queryQueryParameters.ContainsKey))
248+
{
249+
newStoredProcText = storedProcedureText;
250+
return parameters.ToDictionary(x => x.Key, x => x.Value);
251+
}
252+
253+
int parameterNumber = rebaseFrom;
254+
int maxParamNumber = parameters.Count;
255+
256+
var output = new Dictionary<string, object>();
257+
258+
for(int i = maxParamNumber; i >= parameterNumber && i > 0; i--)
259+
{
260+
var parameter = parameters[i-1];
261+
var newP = $"p{i}";
262+
var regex = string.Format(regexFormat, parameter.Key);
263+
output.Add(newP, parameter.Value);
264+
storedProcedureText = Regex.Replace(storedProcedureText, regex, $@"${{start}}${newP}${{end}}");
265+
}
266+
267+
newStoredProcText = storedProcedureText;
268+
return output;
269+
}
270+
271+
272+
216273
public ICypherFluentQuery Yield(string yieldText)
217274
{
218275
if (!Client.CypherCapabilities.SupportsStoredProcedures)

Neo4jClient/Cypher/CypherFluentQuery`Where.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,18 @@ internal ICypherFluentQuery Where(LambdaExpression expression)
1414
internal ICypherFluentQuery AndWhere(LambdaExpression expression)
1515
{
1616
return Mutate(w =>
17-
w.AppendClause(string.Format("AND {0}", CypherWhereExpressionBuilder.BuildText(expression, w.CreateParameter, Client.CypherCapabilities, CamelCaseProperties))));
17+
w.AppendClause($"AND {CypherWhereExpressionBuilder.BuildText(expression, w.CreateParameter, Client.CypherCapabilities, CamelCaseProperties)}"));
1818
}
1919

2020
internal ICypherFluentQuery OrWhere(LambdaExpression expression)
2121
{
2222
return Mutate(w =>
23-
w.AppendClause(string.Format("OR {0}", CypherWhereExpressionBuilder.BuildText(expression, w.CreateParameter, Client.CypherCapabilities, CamelCaseProperties))));
23+
w.AppendClause($"OR {CypherWhereExpressionBuilder.BuildText(expression, w.CreateParameter, Client.CypherCapabilities, CamelCaseProperties)}"));
2424
}
2525

2626
public ICypherFluentQuery Where(string text)
2727
{
28-
return Mutate(w =>
29-
w.AppendClause(string.Format("WHERE {0}", text)));
28+
return Mutate(w => w.AppendClause($"WHERE {text}"));
3029
}
3130

3231
public ICypherFluentQuery Where<T1>(Expression<Func<T1, bool>> expression)

Neo4jClient/Cypher/ICypherFluentQuery.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ public partial interface ICypherFluentQuery
6767
/// <exception cref="InvalidOperationException">Thrown if an attempt is made to call this against a server version prior to 3.0.</exception>
6868
ICypherFluentQuery Call(string storedProcedureText);
6969

70+
/// <summary>
71+
/// [Neo4j 4.0+] Calls a SubQuery on the Database.
72+
/// </summary>
73+
/// <remarks>This only works on Neo4j 4.0+</remarks>
74+
/// <param name="storedProcedureText">The Sub Query to execute.</param>
75+
/// <returns>An <see cref="ICypherFluentQuery"/> instance to continue the query with.</returns>
76+
/// <exception cref="ArgumentException">Thrown if the <paramref name="storedProcedureText"/> argument is empty or null.</exception>
77+
/// <exception cref="InvalidOperationException">Thrown if an attempt is made to call this against a server version prior to 3.0.</exception>
78+
ICypherFluentQuery Call<T>(Func<ICypherFluentQuery<T>> subQuery);
79+
7080
/// <summary>
7181
/// [Neo4j 3.0+] Yields the values from the response of a <see cref="Call"/> method
7282
/// </summary>

0 commit comments

Comments
 (0)