Skip to content

Implement async Database API (breaking change)#7

Open
Copilot wants to merge 3 commits intomainfrom
copilot/implement-async-database-api
Open

Implement async Database API (breaking change)#7
Copilot wants to merge 3 commits intomainfrom
copilot/implement-async-database-api

Conversation

Copy link
Copy Markdown

Copilot AI commented Apr 1, 2026

Converts all GraphDatabase public methods from synchronous to async-returning Promises, enabling browser WASM compatibility where synchronous file I/O is unavailable.

API Changes

Migration example

// Before (v0.x)
const db = new GraphDatabase('./graph.db');
const node = db.createNode('Job', { title: 'Engineer' });
const results = db.nodes('Job').where({ status: 'active' }).exec();

// After (v1.0)
const db = await GraphDatabase.create('./graph.db');
const node = await db.createNode('Job', { title: 'Engineer' });
const results = await db.nodes('Job').where({ status: 'active' }).exec();

src/core/Database.ts

  • Added static async create(path, options?) factory method
  • Added private _getNodeSync() helper for internal sync node lookups (avoids async chains inside CRUD methods that need to validate node existence)
  • Made async: createNode, getNode, updateNode, deleteNode, createEdge, getEdge, deleteEdge, transaction, export, import, close, mergeNode, mergeEdge, createPropertyIndex, listIndexes, dropIndex
  • traverse() and nodes() remain synchronous — they return builder objects with no I/O; traverse() still validates node existence via direct db.prepare() call
  • transaction() callback now accepts T | Promise<T>, enabling async operations inside transactions
  • Fixed pre-existing TypeScript constraint error on pattern() (Record<string, unknown>Record<string, GraphEntity>)

src/query/NodeQuery.ts

  • exec(), first(), count(), exists() → async

src/query/TraversalQuery.ts

  • toArray(), toPaths(), shortestPath(), paths(), allPaths() → async
  • Internal private helpers (getNode, getNeighbors) remain synchronous

Tests (11 files)

  • All beforeEach/afterEach and test callbacks made async
  • expect(() => ...).toThrow()await expect(...).rejects.toThrow() where needed
  • Transaction callbacks updated to async (ctx) => { ... }

Copilot AI linked an issue Apr 1, 2026 that may be closed by this pull request
9 tasks
Copilot AI and others added 2 commits April 1, 2026 18:31
- Add await to all async DB methods: createNode, getNode, updateNode,
  deleteNode, createEdge, getEdge, deleteEdge, export, import, close,
  mergeNode, mergeEdge, createPropertyIndex, listIndexes, dropIndex
- Make transaction callbacks async with await on inner calls
- Await NodeQuery terminal methods: exec(), first(), count(), exists()
- Await TraversalQuery terminal methods: toArray(), toPaths(), etc.
- Make beforeEach/afterEach/it() callbacks async where needed
- Transform expect(() => asyncMethod()).toThrow() to rejects.toThrow()
- Handle Array.from/map with async callbacks using Promise.all
- Convert forEach(async ...) to await Promise.all(arr.map(async ...))
- PatternQuery.exec/first/count remain synchronous (no await)
- traverse() sync validation unchanged

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: michaeloboyle <61171+michaeloboyle@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement async Database API for browser compatibility Implement async Database API (breaking change) Apr 1, 2026
Copilot AI requested a review from michaeloboyle April 1, 2026 18:35
@michaeloboyle michaeloboyle self-requested a review April 7, 2026 00:35
@michaeloboyle michaeloboyle marked this pull request as ready for review April 7, 2026 00:36
Copilot AI review requested due to automatic review settings April 7, 2026 00:36
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR migrates the public GraphDatabase API and query execution methods from synchronous returns to async Promises to enable browser/WASM compatibility where synchronous file I/O is unavailable.

Changes:

  • Converted core database CRUD/transaction/export/import/index APIs to async and introduced an async GraphDatabase.create(...) factory.
  • Updated query terminal methods (NodeQuery.exec/first/count/exists, TraversalQuery.toArray/toPaths/shortestPath/paths/allPaths) to be async.
  • Migrated unit/integration tests and helper scripts to the async API.

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 26 comments.

Show a summary per file
File Description
src/core/Database.ts Converts GraphDatabase public API to async; adds create(...), internal sync helper(s), and async index/export/import operations.
src/query/NodeQuery.ts Makes query terminal methods async (exec/first/count/exists).
src/query/TraversalQuery.ts Makes traversal terminal methods async (toArray/toPaths/shortestPath/paths/allPaths).
src/index.ts Updates public exports to reflect the async API surface.
tests/unit/Database.test.ts Updates core GraphDatabase tests for async CRUD/transaction/export/import behavior.
tests/unit/Database-merge.test.ts Updates merge/index-management tests to async behavior.
tests/unit/NodeQuery.test.ts Updates NodeQuery tests for async terminal methods and execution.
tests/unit/NodeQuery-both-direction.test.ts Updates bidirectional relationship query tests for async execution.
tests/unit/PatternQuery.test.ts Updates PatternQuery tests to the async database/query execution model.
tests/unit/Transaction.test.ts Updates transaction tests for async transaction callback support.
tests/unit/TraversalQuery.test.ts Updates traversal tests for async terminal traversal methods.
tests/unit/TraversalQuery-paths.test.ts Updates paths() wrapper tests for async traversal terminal methods.
tests/unit/concurrency.test.ts Updates concurrency-related tests to the async database API.
tests/integration/job-pipeline.test.ts Migrates end-to-end job pipeline flows to async database/query/traversal calls.
tests/integration/graph-operations.test.ts Migrates complex graph operation workflows to async database/query/traversal calls.
scripts/transform_tests.py Automation script used to transform tests to async patterns.
scripts/transform_tests_v2.py Updated/alternative automation script for async test transformations.
Comments suppressed due to low confidence (2)

tests/unit/Database-merge.test.ts:196

  • expect(async () => { ... }).not.toThrow() does not work for async code because the function returns a Promise (rejections won’t be caught by toThrow). To assert “does not reject”, use await expect(db.mergeNode(...)).resolves... (or simply await db.mergeNode(...) and let the test fail on rejection).
      it('should not throw when only one node matches', async () => {
        await db.createNode('Company', { name: 'TechCorp', industry: 'SaaS' });

        expect(async () => {
          await db.mergeNode(
            'Company',
            { name: 'TechCorp' },
            { name: 'TechCorp', size: 'Large' }
          );
        }).not.toThrow();

src/core/Database.ts:973

  • createPropertyIndex() interpolates nodeType, property, and indexName directly into SQL (including a quoted string literal in WHERE type = '${nodeType}'). validateNodeType() only checks for non-empty strings (and schema membership is optional), so this is vulnerable to SQL injection / malformed identifiers. Consider restricting nodeType/property to a safe identifier regex, escaping quotes for the string literal, and/or using parameters where possible (e.g., WHERE type = ?).
  async createPropertyIndex(nodeType: string, property: string, unique = false): Promise<void> {
    const indexName = `idx_merge_${nodeType}_${property}`;
    const uniqueClause = unique ? 'UNIQUE' : '';

    // Note: SQLite doesn't allow parameters in partial index WHERE clauses
    // Must use string concatenation (safe here as nodeType is validated)
    const sql = `
      CREATE ${uniqueClause} INDEX IF NOT EXISTS ${indexName}
      ON nodes(type, json_extract(properties, '$.${property}'))
      WHERE type = '${nodeType}'
    `;

    this.db.prepare(sql).run();

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

* Tests all traversal methods, path finding, and cycle detection
*/
describe('TraversalQuery', () => {
describe('TraversalQuery', async () => {
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

Jest describe callbacks must be synchronous; marking this (and nested describe blocks in this file) as async returns a Promise that Jest does not await and may reject with “Returning a Promise from describe is not supported”. Remove async from describe and keep async only in beforeEach/afterEach/it callbacks.

Copilot uses AI. Check for mistakes.
import { GraphDatabase } from '../../src/core/Database';

describe('TraversalQuery - paths() wrapper', () => {
describe('TraversalQuery - paths() wrapper', async () => {
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

Jest describe callbacks must be synchronous; marking this as async returns a Promise that Jest does not await and can break test registration. Remove async from describe blocks and keep async only in hooks/tests.

Suggested change
describe('TraversalQuery - paths() wrapper', async () => {
describe('TraversalQuery - paths() wrapper', () => {

Copilot uses AI. Check for mistakes.
import * as fs from 'fs';

describe('TransactionContext', () => {
describe('TransactionContext', async () => {
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

Jest describe callbacks must be synchronous; using async here returns a Promise that Jest does not await and can break the suite. Remove async from describe blocks and keep async only in hooks/tests.

Copilot uses AI. Check for mistakes.
import { PatternError } from '../../src/types/pattern';

describe('PatternQuery', () => {
describe('PatternQuery', async () => {
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

Jest describe callbacks must be synchronous; using async here returns a Promise that Jest does not await and can break the suite. Remove async from describe blocks and keep async only in hooks/tests.

Copilot uses AI. Check for mistakes.
import { Node } from '../../src/types';

describe('NodeQuery', () => {
describe('NodeQuery', async () => {
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

Jest describe callbacks must be synchronous; using async here returns a Promise that Jest does not await and can break the suite. Remove async from describe blocks and keep async only in hooks/tests.

Suggested change
describe('NodeQuery', async () => {
describe('NodeQuery', () => {

Copilot uses AI. Check for mistakes.
"""Check if position is directly inside expect(...) as the direct argument (not inside a nested arrow fn)."""
pre = content[max(0, pos - 80):pos]
# Check if preceded by expect( (direct arg) or expect(() => (lambda wrapper)
return bool(re.search(r'expect\s*\(\s*$', pre)) or bool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

This return statement is missing a closing parenthesis, making the script invalid Python (it will raise a SyntaxError and can’t be run). Add the missing ) (and consider simplifying the boolean expression for readability).

Suggested change
return bool(re.search(r'expect\s*\(\s*$', pre)) or bool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
return (
bool(re.search(r'expect\s*\(\s*$', pre))
or bool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
)

Copilot uses AI. Check for mistakes.
Comment on lines +1040 to 1042
async dropIndex(indexName: string): Promise<void> {
this.db.prepare(`DROP INDEX IF EXISTS ${indexName}`).run();
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

dropIndex() interpolates indexName directly into the DROP INDEX statement. Since identifiers can’t be parameterized, this should validate/sanitize indexName (e.g., allow only [A-Za-z0-9_]+ and/or enforce an idx_merge_ prefix) to avoid SQL injection via this public API.

Copilot uses AI. Check for mistakes.
Comment on lines +283 to 285
const results = await this.exec();
this.limitValue = original;
return results.length > 0 ? results[0] : null;
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

If exec() throws, first() won’t restore this.limitValue, leaving the query builder mutated for subsequent calls. Wrap the await this.exec() in a try/finally so limitValue is always restored.

Suggested change
const results = await this.exec();
this.limitValue = original;
return results.length > 0 ? results[0] : null;
try {
const results = await this.exec();
return results.length > 0 ? results[0] : null;
} finally {
this.limitValue = original;
}

Copilot uses AI. Check for mistakes.
Comment on lines 346 to 349
const jobCompanies = db2.traverse(importedJobs[0].id)
.out('POSTED_BY')
.toArray();
expect(jobCompanies).toHaveLength(1);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

toArray() is now async; this call is missing await, so jobCompanies will be a Promise and the length assertion will fail. Update this to await db2.traverse(...).out(...).toArray().

Copilot uses AI. Check for mistakes.
const nodeCount = 100;
const nodes = Array.from({ length: nodeCount }, (_, i) =>
db.createNode('Node', { index: i, value: Math.random() })
const nodes = await Promise.all(Array.from({ length: nodeCount }, async (_, i) => await db.createNode('Node', { index: i, value: Math.random() })
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

This Promise.all(Array.from(...)) call is missing a closing )) for the Array.from callback / Promise.all invocation, making the test file invalid TypeScript. Close the parentheses so the array of node creation Promises is built correctly.

Suggested change
const nodes = await Promise.all(Array.from({ length: nodeCount }, async (_, i) => await db.createNode('Node', { index: i, value: Math.random() })
const nodes = await Promise.all(
Array.from({ length: nodeCount }, async (_, i) =>
await db.createNode('Node', { index: i, value: Math.random() })
)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement async Database API (breaking change)

3 participants