Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
9951329
WIP: Optimize SurrealDB adapter
aguspdana Dec 25, 2025
a0f4249
WIP: Optimize SurrealDB adapter
aguspdana Dec 29, 2025
d632898
Treat array filters as OR operation
aguspdana Dec 29, 2025
cc54408
Add benchmark for SurrealDB adapter
aguspdana Dec 30, 2025
978e4da
Fix benchmark util
aguspdana Dec 30, 2025
081b5dc
Fix lint error
aguspdana Jan 5, 2026
70c984a
Refactor
aguspdana Jan 5, 2026
736a0a1
Fix bench
aguspdana Jan 6, 2026
6b7932f
Improve logical query optimizer
aguspdana Jan 6, 2026
8fb422c
Allow selecting new or legacy surrealdb adapter
aguspdana Jan 6, 2026
ea74b6e
Add tests benchmark
aguspdana Jan 6, 2026
75629e8
Refator
aguspdana Jan 6, 2026
fa98afb
Fix: optimizeProjection() was not used
aguspdana Jan 6, 2026
22bf80b
Remove console.log
aguspdana Jan 6, 2026
47808a6
Update comment in enrichRoleFields()
aguspdana Jan 6, 2026
24a2411
Remove duplicate code
aguspdana Jan 6, 2026
3159190
ensure the script exits on errors
aguspdana Jan 6, 2026
c028d6f
Remove dead code
aguspdana Jan 6, 2026
598afd7
Remove unnecessary code
aguspdana Jan 6, 2026
e753bbb
Improve error message
aguspdana Jan 6, 2026
3a7e818
Fix: potential inifite loop
aguspdana Jan 6, 2026
8dabf89
Fix: was mapped to NOT IN
aguspdana Jan 6, 2026
52b3c29
Log final error in query.v3.ts
aguspdana Jan 6, 2026
21a7f5f
Fix schema.v2.surql
aguspdana Jan 6, 2026
4ec7dca
Fix: optimizeProjectionField() passed the wrong thing for when optimi…
aguspdana Jan 6, 2026
89b3b17
Fix: NOT filter did check if condition is undefined
aguspdana Jan 6, 2026
cd7361a
Fix: ref filter was converted into surql incorrectly
aguspdana Jan 6, 2026
d9087b4
Replace custom id generator with nanoid
aguspdana Jan 7, 2026
c0560cc
Remove --allow-all from surrealdb docker container
aguspdana Jan 7, 2026
5328d6e
Allow using legacy surrealdb adapter in v2.bench.ts
aguspdana Jan 7, 2026
b650f95
Remove console.log
aguspdana Jan 7, 2026
708f018
Escape ⟩ in surql identifier
aguspdana Jan 7, 2026
5c8a745
Fix: CONTAINSALL [] was converted into FALSE
aguspdana Jan 7, 2026
280ef10
Fix tests/multidb/mocks/schema.ts
aguspdana Jan 7, 2026
3b2360c
Fix tests/multidb/mocks/schema.ts
aguspdana Jan 7, 2026
9e63ea6
Fix tests/multidb/mocks/schema.ts
aguspdana Jan 7, 2026
e75be05
Fix: typedb query machine skipped postHooks step
aguspdana Jan 7, 2026
e4f9e28
Rename tinyBench.ts to testsBench.ts
aguspdana Jan 7, 2026
dc101ae
Fix container readiness check
aguspdana Jan 7, 2026
387cd3e
Move tinybench to devDependencies
aguspdana Jan 7, 2026
c8e7225
Put dynamic path in scripts inside double quotes
aguspdana Jan 7, 2026
a43694c
Fix typo
aguspdana Jan 7, 2026
04fcb07
Fix typo
aguspdana Jan 7, 2026
3c36632
Fix: identifier was escaped incorectly
aguspdana Jan 7, 2026
7a3532c
refactor(surql2): improve code quality and type safety
lveillard Jan 7, 2026
5f6cf3c
Remove unnecessary type
aguspdana Jan 7, 2026
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
141 changes: 141 additions & 0 deletions benches/bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
const MAX_ITER = 10;
const MAX_DURATION = 60_000;

type BenchFn = (cb: () => Promise<void>) => void;

type TimeitFn = (name: string, cb: () => Promise<void>, opt?: BenchOptions) => void;

interface BenchOptions {
maxIter?: number;
maxDuration?: number;
}

export const bench = async (
cb: (params: { beforeAll: BenchFn; afterAll: BenchFn; time: TimeitFn }) => Promise<void>,
opt?: BenchOptions,
) => {
const { maxIter = MAX_ITER, maxDuration = MAX_DURATION } = opt ?? {};
const beforePromises: (() => Promise<void>)[] = [];
const afterPromises: (() => Promise<void>)[] = [];
const beforeAll = (cb: () => Promise<void>) => {
beforePromises.push(cb);
};
const afterAll = (cb: () => Promise<void>) => {
afterPromises.push(cb);
};
const variants: {
name: string;
cb: () => Promise<void>;
durations: number[];
totalDuration: number;
maxIter: number;
maxDuration: number;
}[] = [];
const time = (name: string, cb: () => Promise<void>, opt?: BenchOptions) => {
variants.push({
name,
cb,
durations: [],
totalDuration: 0,
maxIter: opt?.maxIter ?? maxIter,
maxDuration: opt?.maxDuration ?? maxDuration,
});
};

await cb({ beforeAll, afterAll, time });

await Promise.all(beforePromises.map(async (cb) => cb()));

for (const variant of variants) {
console.log(`Running "${variant.name}"...`);
while (variant.durations.length < variant.maxIter && variant.totalDuration < variant.maxDuration) {
try {
const start = performance.now();
await variant.cb();
const duration = performance.now() - start;
variant.durations.push(duration);
variant.totalDuration += duration;
} catch (error) {
console.error(`Error running "${variant.name}":`, error);
break;
}
}
}

await Promise.all(afterPromises.map((cb) => cb()));

const summary = summarize(variants);
console.log(format(summary));
};

interface Summary {
name: string;
iter: number;
first: number;
min: number;
max: number;
mean: number;
median: number;
p90: number;
p95: number;
}

const summarize = (variants: { name: string; durations: number[] }[]): Summary[] => {
return variants.map((variant) => {
const sorted = [...variant.durations].sort((a, b) => a - b);
const total = sorted.reduce((a, b) => a + b, 0);
const count = sorted.length;

const min = sorted[0] || 0;
const max = sorted[count - 1] || 0;
const mean = count > 0 ? total / count : 0;
const median =
count === 0
? 0
: count % 2 === 0
? (sorted[count / 2 - 1] + sorted[count / 2]) / 2
: sorted[Math.floor(count / 2)];

const p90 = count === 0 ? 0 : sorted[Math.floor(count * 0.9)];
const p95 = count === 0 ? 0 : sorted[Math.floor(count * 0.95)];

return {
name: variant.name,
iter: variant.durations.length,
first: variant.durations[0] ?? 0,
min,
max,
mean,
median,
p90,
p95,
};
});
};

const format = (summary: Summary[]): string => {
const headers = ['name', 'iter', 'first', 'min', 'max', 'mean', 'median'] as const;

const rows = summary.map((s) => ({
name: s.name.length > 48 ? `${s.name.slice(0, 40)}...${s.name.slice(-5)}` : s.name,
iter: s.iter.toString(),
first: s.first.toFixed(4),
min: s.min.toFixed(4),
max: s.max.toFixed(4),
mean: s.mean.toFixed(4),
median: s.median.toFixed(4),
}));

const allRows = [
{ name: 'name', iter: 'iter', first: 'first', min: 'min', max: 'max', mean: 'mean', median: 'median' },
...rows,
];

const widths = headers.map((h) => Math.max(...allRows.map((r) => r[h].length)));

return allRows
.map((row) =>
headers.map((h, i) => (h === 'name' ? row[h].padEnd(widths[i]) : row[h].padStart(widths[i]))).join(' | '),
)
.join('\n');
};
90 changes: 90 additions & 0 deletions benches/bench.v2.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
set -euo pipefail

CONTAINER_NAME=borm_bench_v2
USER=borm_bench
PASSWORD=borm_bench
NAMESPACE=borm_bench
DATABASE=borm_bench
SCHEMA_FILE="./benches/schema.v2.surql"

# Function to clean up the container
cleanup() {
echo "Stopping and removing container..."
docker stop ${CONTAINER_NAME} >/dev/null 2>&1
docker rm ${CONTAINER_NAME} >/dev/null 2>&1
exit ${EXIT_CODE:-1} # Default to 1 if EXIT_CODE is unset (e.g. early crash)
}

# Set up trap to call cleanup function on script exit
trap cleanup EXIT INT TERM

# Function to parse command line arguments
parse_args() {
VITEST_ARGS=()
for arg in "$@"
do
case $arg in
-link=*)
# We'll ignore this parameter now
;;
*)
VITEST_ARGS+=("$arg")
;;
esac
done
}

# Parse the command line arguments
parse_args "$@"

# Start the container
if ! docker run \
--rm \
--detach \
--name $CONTAINER_NAME \
--user root \
-p 8002:8002 \
--pull always \
surrealdb/surrealdb:v2.3.7 \
start \
-u $USER \
-p $PASSWORD \
--bind 0.0.0.0:8002 \
rocksdb:///data/blitz.db; then
echo "Failed to start SurrealDB container"
exit 1
fi

until [ "`docker inspect -f {{.State.Running}} $CONTAINER_NAME`" == "true" ]; do
sleep 0.1;
done;

# Wait for SurrealDB to be ready
echo "Waiting for SurrealDB to be ready..."
until docker exec $CONTAINER_NAME ./surreal is-ready --endpoint http://localhost:8002 2>/dev/null; do
sleep 0.5;
done;
echo "SurrealDB is ready!"

# Setup surrealdb database: create the namespace, database, and user dynamically
docker exec -i $CONTAINER_NAME ./surreal sql -u $USER -p $PASSWORD --endpoint http://localhost:8002 <<EOF
DEFINE NAMESPACE $NAMESPACE;
USE NS $NAMESPACE;
DEFINE DATABASE $DATABASE;
DEFINE USER $USER ON NAMESPACE PASSWORD '$PASSWORD' ROLES OWNER;
EOF

# Create the schema
docker cp $SCHEMA_FILE $CONTAINER_NAME:/tmp/schema.surql
docker exec -i $CONTAINER_NAME ./surreal import -u $USER -p $PASSWORD --namespace $NAMESPACE --database $DATABASE --endpoint http://localhost:8002 /tmp/schema.surql

# Always stop container, but exit with 1 when tests are failing
# if CONTAINER_NAME=${CONTAINER_NAME} npx vitest bench "${VITEST_ARGS[@]}"; then
if CONTAINER_NAME=${CONTAINER_NAME} tsx benches/v2.bench.ts; then
echo "Bench passed. Container ${CONTAINER_NAME} is still running."
EXIT_CODE=0
else
echo "Bench failed. Container ${CONTAINER_NAME} is still running."
EXIT_CODE=1
fi
79 changes: 79 additions & 0 deletions benches/generateData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { genAlphaId } from '../src/helpers';

export interface Base {
id: string;
string_1: string;
number_1: number;
boolean_1: boolean;
datetime_1: Date;
}

export interface A extends Base {
one: B['id'];
few: B['id'][];
many: B['id'][];
}

export type B = Base;

export const generateData = (params: {
records: number;
few: { min: number; max: number };
many: { min: number; max: number };
}): { a: A[]; b: B[] } => {
const a: A[] = [];
const b: B[] = [];

const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const randomString = (min: number, max: number) => {
Copy link

@cubic-dev-ai cubic-dev-ai bot Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Rule violated: Ensure all TypeScript code adheres to ECMAScript 2025 standards

String concatenation in loops is a less performant older pattern. Consider using Array.from() with .join() for more idiomatic modern JavaScript.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At benches/generateData.ts, line 27:

<comment>String concatenation in loops is a less performant older pattern. Consider using `Array.from()` with `.join()` for more idiomatic modern JavaScript.</comment>

<file context>
@@ -0,0 +1,87 @@
+
+  const randomInt = (min: number, max: number) =&gt; Math.floor(Math.random() * (max - min + 1)) + min;
+  const chars = &#39;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789&#39;;
+  const randomString = (min: number, max: number) =&gt; {
+      const length = randomInt(min, max);
+      let result = &#39;&#39;;
</file context>

✅ Addressed in d9087b4

const length = randomInt(min, max);
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
};
const randomBoolean = () => Math.random() < 0.5;
const randomDate = () => {
const start = new Date('2020-01-01').getTime();
const end = new Date('2026-01-01').getTime();
return new Date(start + Math.random() * (end - start));
};

const generateBase = (): Base => ({
id: genAlphaId(16),
string_1: randomString(10, 20),
number_1: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER),
boolean_1: randomBoolean(),
datetime_1: randomDate(),
});

for (let i = 0; i < params.records; i++) {
b.push(generateBase());
}

for (let i = 0; i < params.records; i++) {
const fewLength = randomInt(params.few.min, params.few.max);
const manyLength = randomInt(params.many.min, params.many.max);
const fewSet = new Set<string>();
const manySet = new Set<string>();

while (fewSet.size < fewLength && fewSet.size < b.length) {
fewSet.add(b[randomInt(0, b.length - 1)].id);
}

while (manySet.size < manyLength && manySet.size < b.length) {
manySet.add(b[randomInt(0, b.length - 1)].id);
}

a.push({
...generateBase(),
one: b[i].id,
few: Array.from(fewSet),
many: Array.from(manySet),
});
}

return { a, b };
};
Loading