Skip to content

Commit b5b5d73

Browse files
authored
Merge branch '4.0.0' into chore/cypher-params
2 parents cdd1cce + cbf21b3 commit b5b5d73

File tree

5 files changed

+173
-82
lines changed

5 files changed

+173
-82
lines changed

packages/graphql/src/translate/create-aggregate-where-and-params.ts

Lines changed: 25 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,6 @@ export type AggregateWhereInput = {
4444
edge: WhereFilter;
4545
} & WhereFilter;
4646

47-
type AggregateWhereReturn = {
48-
returnProjections: ("*" | Cypher.ProjectionColumn)[];
49-
predicates: Cypher.Predicate[];
50-
};
51-
5247
export function aggregatePreComputedWhereFields({
5348
value,
5449
relationField,
@@ -77,20 +72,22 @@ export function aggregatePreComputedWhereFields({
7772
.to(aggregationTarget);
7873

7974
const matchQuery = new Cypher.Match(matchPattern);
80-
const { returnProjections, predicates } = aggregateWhere(
75+
const innerPredicates = aggregateWhere(
8176
value as AggregateWhereInput,
8277
refNode,
8378
relationship,
8479
aggregationTarget,
8580
cypherRelation,
8681
context
8782
);
88-
matchQuery.return(...returnProjections);
83+
84+
const predicateVariable = new Cypher.Variable();
85+
matchQuery.return([Cypher.and(...innerPredicates), predicateVariable]);
8986

9087
const subquery = new Cypher.Call(matchQuery).innerWith(matchNode);
9188

9289
return {
93-
predicate: Cypher.and(...predicates),
90+
predicate: Cypher.eq(predicateVariable, new Cypher.Literal(true)),
9491
// Cypher.concat is used because this is passed to createWherePredicate which expects a Cypher.CompositeClause
9592
preComputedSubqueries: Cypher.concat(subquery),
9693
};
@@ -103,63 +100,48 @@ function aggregateWhere(
103100
aggregationTarget: Cypher.Node,
104101
cypherRelation: Cypher.Relationship,
105102
context: Context
106-
): AggregateWhereReturn {
107-
const returnProjections: ("*" | Cypher.ProjectionColumn)[] = [];
108-
const returnPredicates: Cypher.Predicate[] = [];
103+
): Cypher.Predicate[] {
104+
const innerPredicatesRes: Cypher.Predicate[] = [];
109105
Object.entries(aggregateWhereInput).forEach(([key, value]) => {
110106
if (AGGREGATION_AGGREGATE_COUNT_OPERATORS.includes(key)) {
111-
const { returnProjection, predicate } = createCountPredicateAndProjection(aggregationTarget, key, value);
112-
returnProjections.push(returnProjection);
113-
if (predicate) returnPredicates.push(predicate);
107+
const innerPredicates = createCountPredicateAndProjection(aggregationTarget, key, value);
108+
innerPredicatesRes.push(...innerPredicates);
114109
} else if (NODE_OR_EDGE_KEYS.includes(key)) {
115110
const target = key === "edge" ? cypherRelation : aggregationTarget;
116111
const refNodeOrRelation = key === "edge" ? relationship : refNode;
117112
if (!refNodeOrRelation) throw new Error(`Edge filter ${key} on undefined relationship`);
118113

119-
const { returnProjections: innerReturnProjections, predicates } = aggregateEntityWhere(
120-
value,
121-
refNodeOrRelation,
122-
target,
123-
context
124-
);
125-
returnProjections.push(...innerReturnProjections);
126-
returnPredicates.push(...predicates);
114+
const innerPredicates = aggregateEntityWhere(value, refNodeOrRelation, target, context);
115+
116+
innerPredicatesRes.push(...innerPredicates);
127117
} else if (isLogicalOperator(key)) {
128118
const logicalPredicates: Cypher.Predicate[] = [];
129119
asArray(value).forEach((whereInput) => {
130-
const { returnProjections: innerReturnProjections, predicates } = aggregateWhere(
120+
const innerPredicates = aggregateWhere(
131121
whereInput,
132122
refNode,
133123
relationship,
134124
aggregationTarget,
135125
cypherRelation,
136126
context
137127
);
138-
returnProjections.push(...innerReturnProjections);
139-
logicalPredicates.push(...predicates);
128+
logicalPredicates.push(...innerPredicates);
140129
});
141-
142130
const logicalPredicate = getLogicalPredicate(key, logicalPredicates);
143-
144131
if (logicalPredicate) {
145-
returnPredicates.push(logicalPredicate);
132+
innerPredicatesRes.push(logicalPredicate);
146133
}
147134
}
148135
});
149-
return {
150-
returnProjections,
151-
predicates: returnPredicates,
152-
};
136+
137+
return innerPredicatesRes;
153138
}
154139

155140
function createCountPredicateAndProjection(
156141
aggregationTarget: Cypher.Node,
157142
filterKey: string,
158143
filterValue: number
159-
): {
160-
returnProjection: "*" | Cypher.ProjectionColumn;
161-
predicate: Cypher.Predicate | undefined;
162-
} {
144+
): Cypher.Predicate[] {
163145
const paramName = new Cypher.Param(filterValue);
164146
const count = Cypher.count(aggregationTarget);
165147
const operator = whereRegEx.exec(filterKey)?.groups?.operator || "EQ";
@@ -168,52 +150,34 @@ function createCountPredicateAndProjection(
168150
target: count,
169151
value: paramName,
170152
});
171-
const operationVar = new Cypher.Variable();
172153

173-
return {
174-
returnProjection: [operation, operationVar],
175-
predicate: Cypher.eq(operationVar, new Cypher.Literal(true)),
176-
};
154+
return [operation];
177155
}
178156

179157
function aggregateEntityWhere(
180158
aggregateEntityWhereInput: WhereFilter,
181159
refNodeOrRelation: Node | Relationship,
182160
target: Cypher.Node | Cypher.Relationship,
183161
context: Context
184-
): AggregateWhereReturn {
185-
const returnProjections: ("*" | Cypher.ProjectionColumn)[] = [];
186-
const predicates: Cypher.Predicate[] = [];
162+
): Cypher.Predicate[] {
163+
const innerPredicatesRes: Cypher.Predicate[] = [];
187164
Object.entries(aggregateEntityWhereInput).forEach(([key, value]) => {
188165
if (isLogicalOperator(key)) {
189166
const logicalPredicates: Cypher.Predicate[] = [];
190167
asArray(value).forEach((whereInput) => {
191-
const { returnProjections: innerReturnProjections, predicates: innerPredicates } = aggregateEntityWhere(
192-
whereInput,
193-
refNodeOrRelation,
194-
target,
195-
context
196-
);
197-
returnProjections.push(...innerReturnProjections);
168+
const innerPredicates = aggregateEntityWhere(whereInput, refNodeOrRelation, target, context);
198169
logicalPredicates.push(...innerPredicates);
199170
});
200-
201171
const logicalPredicate = getLogicalPredicate(key, logicalPredicates);
202-
203172
if (logicalPredicate) {
204-
predicates.push(logicalPredicate);
173+
innerPredicatesRes.push(logicalPredicate);
205174
}
206175
} else {
207176
const operation = createEntityOperation(refNodeOrRelation, target, key, value);
208-
const operationVar = new Cypher.Variable();
209-
returnProjections.push([operation, operationVar]);
210-
predicates.push(Cypher.eq(operationVar, new Cypher.Literal(true)));
177+
innerPredicatesRes.push(operation);
211178
}
212179
});
213-
return {
214-
returnProjections,
215-
predicates,
216-
};
180+
return innerPredicatesRes;
217181
}
218182

219183
function createEntityOperation(

packages/graphql/tests/performance/graphql/cypher-directive.graphql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
query TopLevelCypherDirective {
1+
query TopLevelCypherDirective_skip {
22
customCypher {
33
name
44
born

packages/graphql/tests/tck/aggregations/where/edge/logical.test.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,10 @@ describe("Cypher Aggregations where edge with Logical AND + OR + NOT", () => {
6565
CALL {
6666
WITH this
6767
MATCH (this)<-[this0:LIKES]-(this1:User)
68-
RETURN any(var2 IN collect(this0.someFloat) WHERE var2 = $param0) AS var3, any(var4 IN collect(this0.someFloat) WHERE var4 = $param1) AS var5
68+
RETURN (any(var2 IN collect(this0.someFloat) WHERE var2 = $param0) AND any(var3 IN collect(this0.someFloat) WHERE var3 = $param1)) AS var4
6969
}
7070
WITH *
71-
WHERE (var3 = true AND var5 = true)
71+
WHERE var4 = true
7272
RETURN this { .content } AS this"
7373
`);
7474

@@ -96,10 +96,10 @@ describe("Cypher Aggregations where edge with Logical AND + OR + NOT", () => {
9696
CALL {
9797
WITH this
9898
MATCH (this)<-[this0:LIKES]-(this1:User)
99-
RETURN any(var2 IN collect(this0.someFloat) WHERE var2 = $param0) AS var3, any(var4 IN collect(this0.someFloat) WHERE var4 = $param1) AS var5
99+
RETURN (any(var2 IN collect(this0.someFloat) WHERE var2 = $param0) OR any(var3 IN collect(this0.someFloat) WHERE var3 = $param1)) AS var4
100100
}
101101
WITH *
102-
WHERE (var3 = true OR var5 = true)
102+
WHERE var4 = true
103103
RETURN this { .content } AS this"
104104
`);
105105

@@ -127,10 +127,10 @@ describe("Cypher Aggregations where edge with Logical AND + OR + NOT", () => {
127127
CALL {
128128
WITH this
129129
MATCH (this)<-[this0:LIKES]-(this1:User)
130-
RETURN any(var2 IN collect(this0.someFloat) WHERE var2 = $param0) AS var3
130+
RETURN NOT (any(var2 IN collect(this0.someFloat) WHERE var2 = $param0)) AS var3
131131
}
132132
WITH *
133-
WHERE NOT (var3 = true)
133+
WHERE var3 = true
134134
RETURN this { .content } AS this"
135135
`);
136136

@@ -140,4 +140,39 @@ describe("Cypher Aggregations where edge with Logical AND + OR + NOT", () => {
140140
}"
141141
`);
142142
});
143+
144+
test("OR NOT", async () => {
145+
const query = gql`
146+
{
147+
posts(
148+
where: {
149+
likesAggregate: { edge: { OR: [{ NOT: { someFloat_EQUAL: 10 } }, { someFloat_EQUAL: 11 }] } }
150+
}
151+
) {
152+
content
153+
}
154+
}
155+
`;
156+
157+
const result = await translateQuery(neoSchema, query);
158+
159+
expect(formatCypher(result.cypher)).toMatchInlineSnapshot(`
160+
"MATCH (this:Post)
161+
CALL {
162+
WITH this
163+
MATCH (this)<-[this0:LIKES]-(this1:User)
164+
RETURN (NOT (any(var2 IN collect(this0.someFloat) WHERE var2 = $param0)) OR any(var3 IN collect(this0.someFloat) WHERE var3 = $param1)) AS var4
165+
}
166+
WITH *
167+
WHERE var4 = true
168+
RETURN this { .content } AS this"
169+
`);
170+
171+
expect(formatParams(result.params)).toMatchInlineSnapshot(`
172+
"{
173+
\\"param0\\": 10,
174+
\\"param1\\": 11
175+
}"
176+
`);
177+
});
143178
});

packages/graphql/tests/tck/aggregations/where/logical.test.ts

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@ describe("Cypher Aggregations where with logical AND plus OR", () => {
5959
CALL {
6060
WITH this
6161
MATCH (this)<-[this0:LIKES]-(this1:User)
62-
RETURN count(this1) > $param0 AS var2, count(this1) < $param1 AS var3
62+
RETURN (count(this1) > $param0 AND count(this1) < $param1) AS var2
6363
}
6464
WITH *
65-
WHERE (var2 = true AND var3 = true)
65+
WHERE var2 = true
6666
RETURN this { .content } AS this"
6767
`);
6868

@@ -96,10 +96,10 @@ describe("Cypher Aggregations where with logical AND plus OR", () => {
9696
CALL {
9797
WITH this
9898
MATCH (this)<-[this0:LIKES]-(this1:User)
99-
RETURN count(this1) > $param0 AS var2, count(this1) < $param1 AS var3
99+
RETURN (count(this1) > $param0 OR count(this1) < $param1) AS var2
100100
}
101101
WITH *
102-
WHERE (var2 = true OR var3 = true)
102+
WHERE var2 = true
103103
RETURN this { .content } AS this"
104104
`);
105105

@@ -133,10 +133,10 @@ describe("Cypher Aggregations where with logical AND plus OR", () => {
133133
CALL {
134134
WITH this
135135
MATCH (this)<-[this0:LIKES]-(this1:User)
136-
RETURN count(this1) > $param0 AS var2
136+
RETURN NOT (count(this1) > $param0) AS var2
137137
}
138138
WITH *
139-
WHERE NOT (var2 = true)
139+
WHERE var2 = true
140140
RETURN this { .content } AS this"
141141
`);
142142

@@ -173,10 +173,10 @@ describe("Cypher Aggregations where with logical AND plus OR", () => {
173173
CALL {
174174
WITH this
175175
MATCH (this)<-[this0:LIKES]-(this1:User)
176-
RETURN count(this1) > $param0 AS var2, count(this1) < $param1 AS var3, count(this1) > $param2 AS var4, count(this1) < $param3 AS var5
176+
RETURN ((count(this1) > $param0 AND count(this1) < $param1) AND (count(this1) > $param2 OR count(this1) < $param3)) AS var2
177177
}
178178
WITH *
179-
WHERE ((var2 = true AND var3 = true) AND (var4 = true OR var5 = true))
179+
WHERE var2 = true
180180
RETURN this { .content } AS this"
181181
`);
182182

@@ -201,4 +201,61 @@ describe("Cypher Aggregations where with logical AND plus OR", () => {
201201
}"
202202
`);
203203
});
204+
205+
test("OR with multiple count", async () => {
206+
const query = gql`
207+
{
208+
posts(
209+
where: {
210+
likesAggregate: {
211+
count_GT: 10
212+
count_LT: 20
213+
OR: [{ count_GT: 10 }, { count_LT: 20 }, { count_LT: 54 }]
214+
}
215+
}
216+
) {
217+
content
218+
}
219+
}
220+
`;
221+
222+
const result = await translateQuery(neoSchema, query);
223+
224+
expect(formatCypher(result.cypher)).toMatchInlineSnapshot(`
225+
"MATCH (this:Post)
226+
CALL {
227+
WITH this
228+
MATCH (this)<-[this0:LIKES]-(this1:User)
229+
RETURN (count(this1) < $param0 AND count(this1) > $param1 AND (count(this1) > $param2 OR count(this1) < $param3 OR count(this1) < $param4)) AS var2
230+
}
231+
WITH *
232+
WHERE var2 = true
233+
RETURN this { .content } AS this"
234+
`);
235+
236+
expect(formatParams(result.params)).toMatchInlineSnapshot(`
237+
"{
238+
\\"param0\\": {
239+
\\"low\\": 20,
240+
\\"high\\": 0
241+
},
242+
\\"param1\\": {
243+
\\"low\\": 10,
244+
\\"high\\": 0
245+
},
246+
\\"param2\\": {
247+
\\"low\\": 10,
248+
\\"high\\": 0
249+
},
250+
\\"param3\\": {
251+
\\"low\\": 20,
252+
\\"high\\": 0
253+
},
254+
\\"param4\\": {
255+
\\"low\\": 54,
256+
\\"high\\": 0
257+
}
258+
}"
259+
`);
260+
});
204261
});

0 commit comments

Comments
 (0)