-
Notifications
You must be signed in to change notification settings - Fork 9
Optimize SurrealDB adapter #108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
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 a0f4249
WIP: Optimize SurrealDB adapter
aguspdana d632898
Treat array filters as OR operation
aguspdana cc54408
Add benchmark for SurrealDB adapter
aguspdana 978e4da
Fix benchmark util
aguspdana 081b5dc
Fix lint error
aguspdana 70c984a
Refactor
aguspdana 736a0a1
Fix bench
aguspdana 6b7932f
Improve logical query optimizer
aguspdana 8fb422c
Allow selecting new or legacy surrealdb adapter
aguspdana ea74b6e
Add tests benchmark
aguspdana 75629e8
Refator
aguspdana fa98afb
Fix: optimizeProjection() was not used
aguspdana 22bf80b
Remove console.log
aguspdana 47808a6
Update comment in enrichRoleFields()
aguspdana 24a2411
Remove duplicate code
aguspdana 3159190
ensure the script exits on errors
aguspdana c028d6f
Remove dead code
aguspdana 598afd7
Remove unnecessary code
aguspdana e753bbb
Improve error message
aguspdana 3a7e818
Fix: potential inifite loop
aguspdana 8dabf89
Fix: was mapped to NOT IN
aguspdana 52b3c29
Log final error in query.v3.ts
aguspdana 21a7f5f
Fix schema.v2.surql
aguspdana 4ec7dca
Fix: optimizeProjectionField() passed the wrong thing for when optimi…
aguspdana 89b3b17
Fix: NOT filter did check if condition is undefined
aguspdana cd7361a
Fix: ref filter was converted into surql incorrectly
aguspdana d9087b4
Replace custom id generator with nanoid
aguspdana c0560cc
Remove --allow-all from surrealdb docker container
aguspdana 5328d6e
Allow using legacy surrealdb adapter in v2.bench.ts
aguspdana b650f95
Remove console.log
aguspdana 708f018
Escape ⟩ in surql identifier
aguspdana 5c8a745
Fix: CONTAINSALL [] was converted into FALSE
aguspdana 280ef10
Fix tests/multidb/mocks/schema.ts
aguspdana 3b2360c
Fix tests/multidb/mocks/schema.ts
aguspdana 9e63ea6
Fix tests/multidb/mocks/schema.ts
aguspdana e75be05
Fix: typedb query machine skipped postHooks step
aguspdana e4f9e28
Rename tinyBench.ts to testsBench.ts
aguspdana dc101ae
Fix container readiness check
aguspdana 387cd3e
Move tinybench to devDependencies
aguspdana c8e7225
Put dynamic path in scripts inside double quotes
aguspdana a43694c
Fix typo
aguspdana 04fcb07
Fix typo
aguspdana 3c36632
Fix: identifier was escaped incorectly
aguspdana 7a3532c
refactor(surql2): improve code quality and type safety
lveillard 5f6cf3c
Remove unnecessary type
aguspdana File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) => { | ||
| 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 }; | ||
| }; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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
✅ Addressed in
d9087b4