Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 0 additions & 3 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,6 @@ jobs:
echo "127.0.0.1 $i" | sudo tee -a /etc/hosts
done

- name: install docker-compose client
run: sudo apt update && sudo apt install docker-compose -y

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
Expand Down
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