flowquery 1.0.41 → 1.0.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/flowquery.min.js +1 -1
- package/dist/graph/node_reference.d.ts.map +1 -1
- package/dist/graph/node_reference.js +7 -3
- package/dist/graph/node_reference.js.map +1 -1
- package/dist/graph/relationship_match_collector.d.ts +1 -0
- package/dist/graph/relationship_match_collector.d.ts.map +1 -1
- package/dist/graph/relationship_match_collector.js +2 -6
- package/dist/graph/relationship_match_collector.js.map +1 -1
- package/docs/flowquery.min.js +1 -1
- package/flowquery-py/pyproject.toml +1 -1
- package/flowquery-py/src/graph/node_reference.py +5 -4
- package/flowquery-py/src/graph/relationship_match_collector.py +7 -8
- package/flowquery-py/tests/compute/test_runner.py +228 -1
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/graph/node_reference.ts +5 -1
- package/src/graph/relationship_match_collector.ts +4 -1
- package/tests/compute/runner.test.ts +205 -0
package/package.json
CHANGED
|
@@ -16,7 +16,11 @@ class NodeReference extends Node {
|
|
|
16
16
|
return this._reference;
|
|
17
17
|
}
|
|
18
18
|
public async next(): Promise<void> {
|
|
19
|
-
this.
|
|
19
|
+
const referenced = this._reference?.value();
|
|
20
|
+
if (referenced == null) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
this.setValue(referenced);
|
|
20
24
|
await this._outgoing?.find(this._value!.id);
|
|
21
25
|
await this.runTodoNext();
|
|
22
26
|
}
|
|
@@ -5,6 +5,7 @@ export type RelationshipMatchRecord = {
|
|
|
5
5
|
startNode: Record<string, any>;
|
|
6
6
|
endNode: Record<string, any> | null;
|
|
7
7
|
properties: Record<string, any>;
|
|
8
|
+
[key: string]: any;
|
|
8
9
|
};
|
|
9
10
|
|
|
10
11
|
class RelationshipMatchCollector {
|
|
@@ -16,11 +17,13 @@ class RelationshipMatchCollector {
|
|
|
16
17
|
const currentRecord = data?.current();
|
|
17
18
|
const actualType =
|
|
18
19
|
currentRecord && "_type" in currentRecord ? currentRecord["_type"] : relationship.type!;
|
|
20
|
+
const relProperties = data?.properties() as Record<string, any>;
|
|
19
21
|
const match: RelationshipMatchRecord = {
|
|
22
|
+
...(relProperties || {}),
|
|
20
23
|
type: actualType,
|
|
21
24
|
startNode: relationship.source?.value() || {},
|
|
22
25
|
endNode: null,
|
|
23
|
-
properties:
|
|
26
|
+
properties: relProperties,
|
|
24
27
|
};
|
|
25
28
|
this._matches.push(match);
|
|
26
29
|
this._nodeIds.push(traversalId);
|
|
@@ -3343,6 +3343,69 @@ test("Test match with ORed relationship types returns correct type in relationsh
|
|
|
3343
3343
|
expect(results[1]).toEqual({ from: "NYC", to: "Chicago", type: "TRAIN" });
|
|
3344
3344
|
});
|
|
3345
3345
|
|
|
3346
|
+
test("Test relationship properties can be accessed directly via dot notation", async () => {
|
|
3347
|
+
await new Runner(`
|
|
3348
|
+
CREATE VIRTUAL (:City) AS {
|
|
3349
|
+
unwind [
|
|
3350
|
+
{id: 1, name: 'NYC'},
|
|
3351
|
+
{id: 2, name: 'LA'},
|
|
3352
|
+
{id: 3, name: 'Chicago'}
|
|
3353
|
+
] as record
|
|
3354
|
+
RETURN record.id as id, record.name as name
|
|
3355
|
+
}
|
|
3356
|
+
`).run();
|
|
3357
|
+
await new Runner(`
|
|
3358
|
+
CREATE VIRTUAL (:City)-[:FLIGHT]-(:City) AS {
|
|
3359
|
+
unwind [
|
|
3360
|
+
{left_id: 1, right_id: 2, airline: 'Delta', duration: 5}
|
|
3361
|
+
] as record
|
|
3362
|
+
RETURN record.left_id as left_id, record.right_id as right_id, record.airline as airline, record.duration as duration
|
|
3363
|
+
}
|
|
3364
|
+
`).run();
|
|
3365
|
+
const match = new Runner(`
|
|
3366
|
+
MATCH (a:City)-[r:FLIGHT]->(b:City)
|
|
3367
|
+
RETURN a.name AS from, b.name AS to, r.airline AS airline, r.duration AS duration
|
|
3368
|
+
`);
|
|
3369
|
+
await match.run();
|
|
3370
|
+
const results = match.results;
|
|
3371
|
+
expect(results.length).toBe(1);
|
|
3372
|
+
expect(results[0]).toEqual({ from: "NYC", to: "LA", airline: "Delta", duration: 5 });
|
|
3373
|
+
});
|
|
3374
|
+
|
|
3375
|
+
test("Test relationship properties accessible via both direct access and properties()", async () => {
|
|
3376
|
+
await new Runner(`
|
|
3377
|
+
CREATE VIRTUAL (:Person) AS {
|
|
3378
|
+
unwind [
|
|
3379
|
+
{id: 1, name: 'Alice'},
|
|
3380
|
+
{id: 2, name: 'Bob'}
|
|
3381
|
+
] as record
|
|
3382
|
+
RETURN record.id as id, record.name as name
|
|
3383
|
+
}
|
|
3384
|
+
`).run();
|
|
3385
|
+
await new Runner(`
|
|
3386
|
+
CREATE VIRTUAL (:Person)-[:KNOWS]-(:Person) AS {
|
|
3387
|
+
unwind [
|
|
3388
|
+
{left_id: 1, right_id: 2, since: 2020, strength: 'strong'}
|
|
3389
|
+
] as record
|
|
3390
|
+
RETURN record.left_id as left_id, record.right_id as right_id, record.since as since, record.strength as strength
|
|
3391
|
+
}
|
|
3392
|
+
`).run();
|
|
3393
|
+
const match = new Runner(`
|
|
3394
|
+
MATCH (a:Person)-[r:KNOWS]->(b:Person)
|
|
3395
|
+
RETURN a.name AS from, b.name AS to, r.since AS since, r.strength AS strength, properties(r).since AS propSince
|
|
3396
|
+
`);
|
|
3397
|
+
await match.run();
|
|
3398
|
+
const results = match.results;
|
|
3399
|
+
expect(results.length).toBe(1);
|
|
3400
|
+
expect(results[0]).toEqual({
|
|
3401
|
+
from: "Alice",
|
|
3402
|
+
to: "Bob",
|
|
3403
|
+
since: 2020,
|
|
3404
|
+
strength: "strong",
|
|
3405
|
+
propSince: 2020,
|
|
3406
|
+
});
|
|
3407
|
+
});
|
|
3408
|
+
|
|
3346
3409
|
test("Test coalesce returns first non-null value", async () => {
|
|
3347
3410
|
const runner = new Runner("RETURN coalesce(null, null, 'hello', 'world') as result");
|
|
3348
3411
|
await runner.run();
|
|
@@ -4097,3 +4160,145 @@ test("Test RETURN alias shadowing graph variable in same RETURN clause", async (
|
|
|
4097
4160
|
mentorDepartment: "Engineering",
|
|
4098
4161
|
});
|
|
4099
4162
|
});
|
|
4163
|
+
|
|
4164
|
+
test("Test chained optional match with null intermediate node", async () => {
|
|
4165
|
+
// Create a chain: A -> B -> C (C has no outgoing REPORTS_TO)
|
|
4166
|
+
await new Runner(`
|
|
4167
|
+
CREATE VIRTUAL (:Employee) AS {
|
|
4168
|
+
unwind [
|
|
4169
|
+
{id: 1, name: 'Alice'},
|
|
4170
|
+
{id: 2, name: 'Bob'},
|
|
4171
|
+
{id: 3, name: 'Charlie'}
|
|
4172
|
+
] as record
|
|
4173
|
+
RETURN record.id as id, record.name as name
|
|
4174
|
+
}
|
|
4175
|
+
`).run();
|
|
4176
|
+
await new Runner(`
|
|
4177
|
+
CREATE VIRTUAL (:Employee)-[:REPORTS_TO]-(:Employee) AS {
|
|
4178
|
+
unwind [
|
|
4179
|
+
{left_id: 1, right_id: 2},
|
|
4180
|
+
{left_id: 2, right_id: 3}
|
|
4181
|
+
] as record
|
|
4182
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
4183
|
+
}
|
|
4184
|
+
`).run();
|
|
4185
|
+
|
|
4186
|
+
// Alice -> Bob -> Charlie -> null -> null
|
|
4187
|
+
// m1=Bob, m2=Charlie, m3=null, m4=null (should not crash)
|
|
4188
|
+
const runner = new Runner(`
|
|
4189
|
+
MATCH (u:Employee)
|
|
4190
|
+
WHERE u.name = "Alice"
|
|
4191
|
+
OPTIONAL MATCH (u)-[:REPORTS_TO]->(m1:Employee)
|
|
4192
|
+
OPTIONAL MATCH (m1)-[:REPORTS_TO]->(m2:Employee)
|
|
4193
|
+
OPTIONAL MATCH (m2)-[:REPORTS_TO]->(m3:Employee)
|
|
4194
|
+
OPTIONAL MATCH (m3)-[:REPORTS_TO]->(m4:Employee)
|
|
4195
|
+
RETURN
|
|
4196
|
+
u.name AS user,
|
|
4197
|
+
m1.name AS manager1,
|
|
4198
|
+
m2.name AS manager2,
|
|
4199
|
+
m3.name AS manager3,
|
|
4200
|
+
m4.name AS manager4
|
|
4201
|
+
`);
|
|
4202
|
+
await runner.run();
|
|
4203
|
+
const results = runner.results;
|
|
4204
|
+
|
|
4205
|
+
expect(results.length).toBe(1);
|
|
4206
|
+
expect(results[0].user).toBe("Alice");
|
|
4207
|
+
expect(results[0].manager1).toBe("Bob");
|
|
4208
|
+
expect(results[0].manager2).toBe("Charlie");
|
|
4209
|
+
expect(results[0].manager3).toBeNull();
|
|
4210
|
+
expect(results[0].manager4).toBeNull();
|
|
4211
|
+
});
|
|
4212
|
+
|
|
4213
|
+
test("Test chained optional match all null from first optional", async () => {
|
|
4214
|
+
// Create nodes with no relationships
|
|
4215
|
+
await new Runner(`
|
|
4216
|
+
CREATE VIRTUAL (:Worker) AS {
|
|
4217
|
+
unwind [
|
|
4218
|
+
{id: 1, name: 'Solo'}
|
|
4219
|
+
] as record
|
|
4220
|
+
RETURN record.id as id, record.name as name
|
|
4221
|
+
}
|
|
4222
|
+
`).run();
|
|
4223
|
+
await new Runner(`
|
|
4224
|
+
CREATE VIRTUAL (:Worker)-[:MANAGES]-(:Worker) AS {
|
|
4225
|
+
unwind [] as record
|
|
4226
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
4227
|
+
}
|
|
4228
|
+
`).run();
|
|
4229
|
+
|
|
4230
|
+
// Solo has no MANAGES relationship at all
|
|
4231
|
+
// m1=null, m2=null, m3=null
|
|
4232
|
+
const runner = new Runner(`
|
|
4233
|
+
MATCH (u:Worker)
|
|
4234
|
+
OPTIONAL MATCH (u)-[:MANAGES]->(m1:Worker)
|
|
4235
|
+
OPTIONAL MATCH (m1)-[:MANAGES]->(m2:Worker)
|
|
4236
|
+
OPTIONAL MATCH (m2)-[:MANAGES]->(m3:Worker)
|
|
4237
|
+
RETURN
|
|
4238
|
+
u.name AS user,
|
|
4239
|
+
m1.name AS mgr1,
|
|
4240
|
+
m2.name AS mgr2,
|
|
4241
|
+
m3.name AS mgr3
|
|
4242
|
+
`);
|
|
4243
|
+
await runner.run();
|
|
4244
|
+
const results = runner.results;
|
|
4245
|
+
|
|
4246
|
+
expect(results.length).toBe(1);
|
|
4247
|
+
expect(results[0].user).toBe("Solo");
|
|
4248
|
+
expect(results[0].mgr1).toBeNull();
|
|
4249
|
+
expect(results[0].mgr2).toBeNull();
|
|
4250
|
+
expect(results[0].mgr3).toBeNull();
|
|
4251
|
+
});
|
|
4252
|
+
|
|
4253
|
+
test("Test chained optional match with mixed null and non-null paths", async () => {
|
|
4254
|
+
// Two starting nodes: one with full chain, one with partial
|
|
4255
|
+
await new Runner(`
|
|
4256
|
+
CREATE VIRTUAL (:Staff) AS {
|
|
4257
|
+
unwind [
|
|
4258
|
+
{id: 1, name: 'Dev'},
|
|
4259
|
+
{id: 2, name: 'Lead'},
|
|
4260
|
+
{id: 3, name: 'Director'},
|
|
4261
|
+
{id: 4, name: 'Intern'}
|
|
4262
|
+
] as record
|
|
4263
|
+
RETURN record.id as id, record.name as name
|
|
4264
|
+
}
|
|
4265
|
+
`).run();
|
|
4266
|
+
await new Runner(`
|
|
4267
|
+
CREATE VIRTUAL (:Staff)-[:REPORTS_TO]-(:Staff) AS {
|
|
4268
|
+
unwind [
|
|
4269
|
+
{left_id: 1, right_id: 2},
|
|
4270
|
+
{left_id: 2, right_id: 3}
|
|
4271
|
+
] as record
|
|
4272
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
4273
|
+
}
|
|
4274
|
+
`).run();
|
|
4275
|
+
|
|
4276
|
+
// Dev -> Lead -> Director -> null
|
|
4277
|
+
// Intern -> null -> null -> null
|
|
4278
|
+
const runner = new Runner(`
|
|
4279
|
+
MATCH (u:Staff)
|
|
4280
|
+
WHERE u.name = "Dev" OR u.name = "Intern"
|
|
4281
|
+
OPTIONAL MATCH (u)-[:REPORTS_TO]->(m1:Staff)
|
|
4282
|
+
OPTIONAL MATCH (m1)-[:REPORTS_TO]->(m2:Staff)
|
|
4283
|
+
OPTIONAL MATCH (m2)-[:REPORTS_TO]->(m3:Staff)
|
|
4284
|
+
RETURN
|
|
4285
|
+
u.name AS user,
|
|
4286
|
+
m1.name AS mgr1,
|
|
4287
|
+
m2.name AS mgr2,
|
|
4288
|
+
m3.name AS mgr3
|
|
4289
|
+
`);
|
|
4290
|
+
await runner.run();
|
|
4291
|
+
const results = runner.results;
|
|
4292
|
+
|
|
4293
|
+
expect(results.length).toBe(2);
|
|
4294
|
+
// Dev's chain
|
|
4295
|
+
const dev = results.find((r: any) => r.user === "Dev");
|
|
4296
|
+
expect(dev.mgr1).toBe("Lead");
|
|
4297
|
+
expect(dev.mgr2).toBe("Director");
|
|
4298
|
+
expect(dev.mgr3).toBeNull();
|
|
4299
|
+
// Intern's chain
|
|
4300
|
+
const intern = results.find((r: any) => r.user === "Intern");
|
|
4301
|
+
expect(intern.mgr1).toBeNull();
|
|
4302
|
+
expect(intern.mgr2).toBeNull();
|
|
4303
|
+
expect(intern.mgr3).toBeNull();
|
|
4304
|
+
});
|