Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/cluster.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ describe('Cluster Client Tests', () => {
it('should test slowLog method', async () => {
try {
const graph = clusterClient!.selectGraph(`cluster-test-${getRandomNumber()}`);
const longQuery = 'UNWIND range (0, 200000) AS x RETURN max(x)';
const longQuery = 'UNWIND range (0, 1000000) AS x RETURN max(x)';
await graph.query(longQuery);
const result = await graph.slowLog();
expect(Array.isArray(result)).toBe(true);
Expand Down
1 change: 0 additions & 1 deletion tests/constraints.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ describe('Constraint Tests', () => {
await graphName.query(`
MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
CREATE (a)-[:KNOWS {since: 2020}]->(b)
RETURN exists((a)-[:KNOWS]->(b)) as hasRelationship
`);
await graphName.query("CREATE INDEX ON :KNOWS(since)");
await graphName.constraintCreate("MANDATORY" as ConstraintType, "RELATIONSHIP" as EntityType, "KNOWS", "since");
Expand Down
50 changes: 18 additions & 32 deletions tests/graphAndQuery.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it, beforeAll, afterAll, expect } from '@jest/globals';
import FalkorDB from "../src/falkordb";
import { ConstraintType, EntityType } from "../src/graph";
import { client } from "./dbConnection";
import { expectExecutionPlan, expectPlanShape } from "./planHelpers";
import { Temporal } from "@js-temporal/polyfill";

function getRandomNumber(): number {
Expand Down Expand Up @@ -274,7 +275,7 @@ describe("FalkorDB Execute Query", () => {

it("Verify slow query logging", async () => {
const graph = clientInstance.selectGraph(`graph_${getRandomNumber()}`);
const longQuery = "UNWIND range (0, 200000) AS x RETURN max(x)";
const longQuery = "UNWIND range (0, 1000000) AS x RETURN max(x)";
Comment thread
Naseem77 marked this conversation as resolved.

await graph.query(longQuery);
const slowLogResults = await graph.slowLog();
Expand Down Expand Up @@ -333,9 +334,11 @@ describe("FalkorDB Execute Query", () => {
const graph = clientInstance.selectGraph(`graph_${getRandomNumber()}`);
await graph.query("CREATE (:Person {name: 'Alice'})");
const executionPlan = await graph.explain("MATCH (n:Person) RETURN n");
expect(executionPlan).toContain("Results");
expect(executionPlan).toContain(" Project");
expect(executionPlan).toContain(" Node By Label Scan | (n:Person)");

expectPlanShape(executionPlan, [
"Project",
" Node By Label Scan | (n:Person)",
]);
await graph.delete();
});

Expand All @@ -355,17 +358,14 @@ describe("FalkorDB Execute Query", () => {
RETURN r.name, t.name`
);

const expectedParts = [
"Results",
" Project",
" Conditional Traverse | (t)->(r:Rider)",
" Filter",
" Node By Label Scan | (t:Team)",
];

expectedParts.forEach((expectedPart, index) => {
expect(result[index]).toEqual(expectedPart);
});
// The traverse direction is rendered differently by each engine, so pin
// its operation and nesting but not its arguments.
expectPlanShape(result, [
"Project",
" Conditional Traverse",
" Filter",
" Node By Label Scan | (t:Team)",
]);
await graph.delete();
});

Expand All @@ -388,23 +388,9 @@ describe("FalkorDB Execute Query", () => {
RETURN r.name, t.name`
);

const expectedParts = [
"Results",
" Distinct",
" Join",
" Project",
" Conditional Traverse | (t)->(r:Rider)",
" Filter",
" Node By Label Scan | (t:Team)",
" Project",
" Conditional Traverse | (t)->(r:Rider)",
" Filter",
" Node By Label Scan | (t:Team)",
];

expectedParts.forEach((expectedPart, index) => {
expect(result[index]).toEqual(expectedPart);
});
// Rust names the combining operation "Union"; C names it "Join".
// There is no single exact operation tree to assert for this query.
expectExecutionPlan(result, 7);
await graph.delete();
});

Expand Down
112 changes: 112 additions & 0 deletions tests/planHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* Assertions for the raw execution-plan arrays returned by the client.
*
* Prefer expectPlanShape wherever both engines produce the same operation
* tree. Keep expectExecutionPlan for documented engine-specific plans.
*/

import { expect } from "@jest/globals";

const INDENT = " ";
const ENGINE_ROOT_OPERATIONS = new Set(["Results", "Commit"]);
const PROFILE_STATS = /\s*\|\s*Records produced: \d+,\s*Execution time: \d+\.\d+ ms\s*$/;

export function indentOf(line: string): number {
return (line.length - line.trimStart().length) / INDENT.length;
}

export function operationName(line: string): string {
return line.split("|")[0].trim();
}

function operationLine(line: string): string {
return line.replace(PROFILE_STATS, "").trim();
}

/** Asserts the reply is a well formed execution plan. */
export function expectExecutionPlan(plan: string[], minOperations = 1): void {
expect(Array.isArray(plan)).toBe(true);
expect(plan.length).toBeGreaterThanOrEqual(minOperations);

plan.forEach((line, index) => {
expect(typeof line).toBe("string");

// every line names an operation, optionally followed by its arguments
expect(operationName(line)).not.toBe("");

// indentation is what conveys nesting, so it has to be whole levels, and
// an operation can only ever be one level deeper than the one above it
const indent = indentOf(line);
expect(Number.isInteger(indent)).toBe(true);
expect(indent).toBe(index === 0 ? 0 : Math.min(indent, indentOf(plan[index - 1]) + 1));
Comment thread
Naseem77 marked this conversation as resolved.
});
}

/**
* Asserts operation names and nesting exactly, and arguments on expected lines
* that include them after a `|`. Engine-only driver roots are ignored.
*/
export function expectPlanShape(plan: string[], expected: readonly string[]): void {
expectExecutionPlan(plan);
expect(expected.length).toBeGreaterThan(0);

const expectedRoot = operationName(expected[0]);
const skipRoot =
ENGINE_ROOT_OPERATIONS.has(operationName(plan[0])) &&
operationName(plan[0]) !== expectedRoot;
const rootOffset = skipRoot ? 1 : 0;
const actual = plan.slice(rootOffset);

expect(actual).toHaveLength(expected.length);

expected.forEach((expectedLine, index) => {
const actualLine = actual[index];
const expectedIndent = indentOf(expectedLine);
const actualIndent = indentOf(actualLine) - rootOffset;

expect(Number.isInteger(expectedIndent)).toBe(true);
expect(actualIndent).toBe(expectedIndent);

const expectedOperation = operationLine(expectedLine);
const actualOperation = expectedOperation.includes("|")
? operationLine(actualLine)
: operationName(actualLine);
expect(actualOperation).toBe(expectedOperation);
});
}

/** Asserts the reply is a well formed profile, statistics included. */
export function expectProfile(
plan: string[],
minOperations = 1,
recordsProduced?: number
): void {
expectExecutionPlan(plan, minOperations);

const counts = plan.map((line) => {
const records = line.match(/Records produced: (\d+)/);
const time = line.match(/Execution time: (\d+\.\d+) ms/);

Comment thread
Naseem77 marked this conversation as resolved.
// every operation is profiled, whichever operations they turn out to be
expect(records).not.toBeNull();
expect(time).not.toBeNull();

return parseInt(records![1], 10);
});

if (recordsProduced !== undefined) {
// how many rows the query yields is a property of the query, not of the
// engine, so the client must report it whichever engine answered
expect(Math.max(...counts)).toBe(recordsProduced);
}
}

/** Asserts an exact profile plan while retaining all profile-stat checks. */
export function expectProfileShape(
plan: string[],
expected: readonly string[],
recordsProduced?: number
): void {
expectPlanShape(plan, expected);
expectProfile(plan, expected.length, recordsProduced);
}
34 changes: 12 additions & 22 deletions tests/profile.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { describe, test, beforeAll, beforeEach, afterAll, afterEach, expect } from '@jest/globals';
import { describe, test, beforeAll, beforeEach, afterAll, afterEach } from '@jest/globals';
import { client } from './dbConnection';
import FalkorDB from '../src/falkordb';
import Graph from '../src/graph';
import { expectProfileShape } from './planHelpers';

describe('Profile Tests', () => {

Expand Down Expand Up @@ -37,31 +38,20 @@ describe('Profile Tests', () => {
test('Verifies query execution plan structure with UNWIND operation', async () => {
const plan = await graphName.profile("UNWIND range(0, 3) AS x RETURN x");

expect(plan[0]).toMatch(/Results/);
expect(plan[1]).toMatch(/Project/);
expect(plan[2]).toMatch(/Unwind/);
expect(plan[0]).toContain('Records produced: 4');
expectProfileShape(plan, [
"Project",
" Unwind",
], 4);
});

test('Verifies query execution plan structure with Cartesian operation', async () => {
const plan = await graphName.profile("MATCH (a), (b) RETURN *");
type PlanStep = string | { name: string; alias: string };

const expectedPlanSteps: PlanStep[] = [
'Results',
'Project',
'Cartesian Product',
{ name: 'All Node Scan', alias: '(a)' },
{ name: 'All Node Scan', alias: '(b)' }
];

expectedPlanSteps.forEach((step, index) => {
if (typeof step === 'string') {
expect(plan[index]).toContain(step);
} else {
expect(plan[index]).toContain(step.name);
expect(plan[index]).toContain(step.alias);
}
})
expectProfileShape(plan, [
"Project",
" Cartesian Product",
" All Node Scan | (a)",
" All Node Scan | (b)",
], 0);
});
});
2 changes: 1 addition & 1 deletion tests/single.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ describe("Single Client Tests", () => {
const graph = singleClient.selectGraph(
`test-single-slowlog-${getRandomNumber()}`
);
const longQuery = "UNWIND range (0, 200000) AS x RETURN max(x)"
const longQuery = "UNWIND range (0, 1000000) AS x RETURN max(x)"
await graph.query(longQuery);
const slowLog = await graph.slowLog();
expect(Array.isArray(slowLog)).toBe(true);
Expand Down
Loading