Implement async Database API (breaking change) - #7

Open
michaeloboyle with Copilot wants to merge 6 commits into
mainfrom
copilot/implement-async-database-api
Open

Implement async Database API (breaking change)#7
michaeloboyle with Copilot wants to merge 6 commits into
mainfrom
copilot/implement-async-database-api

Conversation

CopilotAI commented Apr 1, 2026

Copy link
Copy Markdown

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)constdb=newGraphDatabase('./graph.db');constnode=db.createNode('Job',{title: 'Engineer'});constresults=db.nodes('Job').where({status: 'active'}).exec();// After (v1.0)constdb=awaitGraphDatabase.create('./graph.db');constnode=awaitdb.createNode('Job',{title: 'Engineer'});constresults=awaitdb.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) => { ... }

CopilotAI linked an issue Apr 1, 2026 that may be closed by this pull request
9 tasks
CopilotAIand 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>
CopilotAI changed the title [WIP] Implement async Database API for browser compatibilityImplement async Database API (breaking change)Apr 1, 2026
CopilotAI requested a review from michaeloboyleApril 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
CopilotAI review requested due to automatic review settings April 7, 2026 00:36

CopilotAI left a comment

Copy link
Copy Markdown

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
FileDescription
src/core/Database.tsConverts GraphDatabase public API to async; adds create(...), internal sync helper(s), and async index/export/import operations.
src/query/NodeQuery.tsMakes query terminal methods async (exec/first/count/exists).
src/query/TraversalQuery.tsMakes traversal terminal methods async (toArray/toPaths/shortestPath/paths/allPaths).
src/index.tsUpdates public exports to reflect the async API surface.
tests/unit/Database.test.tsUpdates core GraphDatabase tests for async CRUD/transaction/export/import behavior.
tests/unit/Database-merge.test.tsUpdates merge/index-management tests to async behavior.
tests/unit/NodeQuery.test.tsUpdates NodeQuery tests for async terminal methods and execution.
tests/unit/NodeQuery-both-direction.test.tsUpdates bidirectional relationship query tests for async execution.
tests/unit/PatternQuery.test.tsUpdates PatternQuery tests to the async database/query execution model.
tests/unit/Transaction.test.tsUpdates transaction tests for async transaction callback support.
tests/unit/TraversalQuery.test.tsUpdates traversal tests for async terminal traversal methods.
tests/unit/TraversalQuery-paths.test.tsUpdates paths() wrapper tests for async traversal terminal methods.
tests/unit/concurrency.test.tsUpdates concurrency-related tests to the async database API.
tests/integration/job-pipeline.test.tsMigrates end-to-end job pipeline flows to async database/query/traversal calls.
tests/integration/graph-operations.test.tsMigrates complex graph operation workflows to async database/query/traversal calls.
scripts/transform_tests.pyAutomation script used to transform tests to async patterns.
scripts/transform_tests_v2.pyUpdated/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.

Comment threadtests/unit/TraversalQuery.test.ts Outdated
* Tests all traversal methods, path finding, and cycle detection
*/
describe('TraversalQuery', () => {
describe('TraversalQuery', async () => {

CopilotAIApr 7, 2026

Copy link

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.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in bda750a. Removed async from all describe() callbacks across all 10 test files (Jest does not await Promises returned from describe blocks). While applying that fix I also addressed all the other reviewer issues in the same commit:

  • Async filter()/forEach() → replaced with Promise.all(arr.map(...)) + synchronous filter
  • Missing await on toArray()/DB calls → added throughout
  • Syntax errors (mapmap, missing ))) → fixed
  • expect(async () => ...).not.toThrow() → replaced with await expect(...).resolves.toBeDefined()
  • dropIndex() SQL injection → added [A-Za-z0-9_]+ identifier validation
  • first() not restoring limitValue on error → wrapped in try/finally
  • TraversalQuery.getNeighbors() both-direction nodeType bug → fixed SQL to put AND EXISTS inside each UNION branch
  • TraversalQuery.toArray() filter preventing neighbor exploration at depth 0 → separated filter application from neighbor push
  • Cyclic graph infinite BFS → added .maxDepth(1)/.unique() to traversal calls on cyclic test graphs
  • Nested db.transaction() inside outer transaction → replaced with savepoints (SQLite doesn't support nested BEGIN)

All 408 tests now pass across all 12 test suites.

Comment threadtests/unit/TraversalQuery-paths.test.ts Outdated
Comment threadtests/unit/Transaction.test.ts Outdated
import * as fs from 'fs';

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

CopilotAIApr 7, 2026

Copy link

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.
Comment threadtests/unit/PatternQuery.test.ts Outdated
import { PatternError } from '../../src/types/pattern';

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

CopilotAIApr 7, 2026

Copy link

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.
Comment threadtests/unit/NodeQuery.test.ts Outdated
import { Node } from '../../src/types';

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

CopilotAIApr 7, 2026

Copy link

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))

CopilotAIApr 7, 2026

Copy link

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
returnbool(re.search(r'expect\s*\(\s*$', pre)) orbool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
return(
bool(re.search(r'expect\s*\(\s*$', pre))
orbool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
)

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

CopilotAIApr 7, 2026

Copy link

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 threadsrc/query/NodeQuery.ts Outdated
Comment on lines 283 to 285
const results = await this.exec();
this.limitValue = original;
return results.length > 0 ? results[0] : null;

CopilotAIApr 7, 2026

Copy link

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
constresults=awaitthis.exec();
this.limitValue=original;
returnresults.length>0 ? results[0] : null;
try{
constresults=awaitthis.exec();
returnresults.length>0 ? results[0] : null;
}finally{
this.limitValue=original;
}

Copilot uses AI. Check for mistakes.
Comment on lines 346 to 349
@@ -352,27 +348,26 @@ describe('Complex Graph Operations - Integration Tests', () => {
.toArray();
expect(jobCompanies).toHaveLength(1);

CopilotAIApr 7, 2026

Copy link

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() })

CopilotAIApr 7, 2026

Copy link

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
constnodes=awaitPromise.all(Array.from({length: nodeCount},async(_,i)=>awaitdb.createNode('Node',{index: i,value: Math.random()})
constnodes=awaitPromise.all(
Array.from({length: nodeCount},async(_,i)=>
awaitdb.createNode('Node',{index: i,value: Math.random()})
)

Copilot uses AI. Check for mistakes.
@michaeloboyle
michaeloboyle self-requested a review April 9, 2026 02:01
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@michaeloboyle

Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread

…ix async filter/forEach patterns, fix SQL injection in dropIndex, fix TraversalQuery bugs
Agent-Logs-Url: https://github.com/michaeloboyle/sqlite-graph/sessions/7393282e-2faa-435a-88af-a5148cf923d6
Co-authored-by: michaeloboyle <61171+michaeloboyle@users.noreply.github.com>

CopilotAI commented Apr 27, 2026

Copy link
Copy Markdown
Author

@copilot apply changes based on the comments in this thread

All changes from the review were already applied in bda750a. The full list of what was fixed:

  • Removed async from all describe() callbacks (10 test files)
  • Replaced array.filter(async ...) with Promise.all(...map) + sync filter
  • Replaced forEach(async ...) with await Promise.all(array.map(...))
  • Added missing await on toArray() and DB method calls
  • Fixed syntax errors (mapmapmap, missing closing )))
  • Fixed expect(async () => ...).not.toThrow()await expect(...).resolves.toBeDefined()
  • Added [A-Za-z0-9_]+ validation in dropIndex() to prevent SQL injection
  • Wrapped limitValue restoration in first() with try/finally
  • Fixed TraversalQuery.getNeighbors() SQL for both-direction with nodeType filter
  • Fixed TraversalQuery.toArray() filter not continuing neighbor exploration at depth 0

The scripts/transform_tests_v2.py:114 comment was a false positive — the return statement is syntactically valid Python (verified by py_compile). All 408 tests pass across 12 test suites.

…s/43557c40-24fc-46ae-a357-5c9a77df7c88
Co-authored-by: michaeloboyle <61171+michaeloboyle@users.noreply.github.com>
Sign up for freeto 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

@michaeloboyle
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Implement async Database API (breaking change) - #7

Open
michaeloboyle with Copilot wants to merge 6 commits into
mainfrom
copilot/implement-async-database-api
Open

Implement async Database API (breaking change)#7
michaeloboyle with Copilot wants to merge 6 commits into
mainfrom
copilot/implement-async-database-api

Conversation

CopilotAI commented Apr 1, 2026

Copy link
Copy Markdown

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)constdb=newGraphDatabase('./graph.db');constnode=db.createNode('Job',{title: 'Engineer'});constresults=db.nodes('Job').where({status: 'active'}).exec();// After (v1.0)constdb=awaitGraphDatabase.create('./graph.db');constnode=awaitdb.createNode('Job',{title: 'Engineer'});constresults=awaitdb.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) => { ... }

CopilotAI linked an issue Apr 1, 2026 that may be closed by this pull request
9 tasks
CopilotAIand 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>
CopilotAI changed the title [WIP] Implement async Database API for browser compatibilityImplement async Database API (breaking change)Apr 1, 2026
CopilotAI requested a review from michaeloboyleApril 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
CopilotAI review requested due to automatic review settings April 7, 2026 00:36

CopilotAI left a comment

Copy link
Copy Markdown

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
FileDescription
src/core/Database.tsConverts GraphDatabase public API to async; adds create(...), internal sync helper(s), and async index/export/import operations.
src/query/NodeQuery.tsMakes query terminal methods async (exec/first/count/exists).
src/query/TraversalQuery.tsMakes traversal terminal methods async (toArray/toPaths/shortestPath/paths/allPaths).
src/index.tsUpdates public exports to reflect the async API surface.
tests/unit/Database.test.tsUpdates core GraphDatabase tests for async CRUD/transaction/export/import behavior.
tests/unit/Database-merge.test.tsUpdates merge/index-management tests to async behavior.
tests/unit/NodeQuery.test.tsUpdates NodeQuery tests for async terminal methods and execution.
tests/unit/NodeQuery-both-direction.test.tsUpdates bidirectional relationship query tests for async execution.
tests/unit/PatternQuery.test.tsUpdates PatternQuery tests to the async database/query execution model.
tests/unit/Transaction.test.tsUpdates transaction tests for async transaction callback support.
tests/unit/TraversalQuery.test.tsUpdates traversal tests for async terminal traversal methods.
tests/unit/TraversalQuery-paths.test.tsUpdates paths() wrapper tests for async traversal terminal methods.
tests/unit/concurrency.test.tsUpdates concurrency-related tests to the async database API.
tests/integration/job-pipeline.test.tsMigrates end-to-end job pipeline flows to async database/query/traversal calls.
tests/integration/graph-operations.test.tsMigrates complex graph operation workflows to async database/query/traversal calls.
scripts/transform_tests.pyAutomation script used to transform tests to async patterns.
scripts/transform_tests_v2.pyUpdated/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.

Comment threadtests/unit/TraversalQuery.test.ts Outdated
* Tests all traversal methods, path finding, and cycle detection
*/
describe('TraversalQuery', () => {
describe('TraversalQuery', async () => {

CopilotAIApr 7, 2026

Copy link

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.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in bda750a. Removed async from all describe() callbacks across all 10 test files (Jest does not await Promises returned from describe blocks). While applying that fix I also addressed all the other reviewer issues in the same commit:

  • Async filter()/forEach() → replaced with Promise.all(arr.map(...)) + synchronous filter
  • Missing await on toArray()/DB calls → added throughout
  • Syntax errors (mapmap, missing ))) → fixed
  • expect(async () => ...).not.toThrow() → replaced with await expect(...).resolves.toBeDefined()
  • dropIndex() SQL injection → added [A-Za-z0-9_]+ identifier validation
  • first() not restoring limitValue on error → wrapped in try/finally
  • TraversalQuery.getNeighbors() both-direction nodeType bug → fixed SQL to put AND EXISTS inside each UNION branch
  • TraversalQuery.toArray() filter preventing neighbor exploration at depth 0 → separated filter application from neighbor push
  • Cyclic graph infinite BFS → added .maxDepth(1)/.unique() to traversal calls on cyclic test graphs
  • Nested db.transaction() inside outer transaction → replaced with savepoints (SQLite doesn't support nested BEGIN)

All 408 tests now pass across all 12 test suites.

Comment threadtests/unit/TraversalQuery-paths.test.ts Outdated
Comment threadtests/unit/Transaction.test.ts Outdated
import * as fs from 'fs';

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

CopilotAIApr 7, 2026

Copy link

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.
Comment threadtests/unit/PatternQuery.test.ts Outdated
import { PatternError } from '../../src/types/pattern';

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

CopilotAIApr 7, 2026

Copy link

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.
Comment threadtests/unit/NodeQuery.test.ts Outdated
import { Node } from '../../src/types';

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

CopilotAIApr 7, 2026

Copy link

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))

CopilotAIApr 7, 2026

Copy link

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
returnbool(re.search(r'expect\s*\(\s*$', pre)) orbool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
return(
bool(re.search(r'expect\s*\(\s*$', pre))
orbool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
)

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

CopilotAIApr 7, 2026

Copy link

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 threadsrc/query/NodeQuery.ts Outdated
Comment on lines 283 to 285
const results = await this.exec();
this.limitValue = original;
return results.length > 0 ? results[0] : null;

CopilotAIApr 7, 2026

Copy link

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
constresults=awaitthis.exec();
this.limitValue=original;
returnresults.length>0 ? results[0] : null;
try{
constresults=awaitthis.exec();
returnresults.length>0 ? results[0] : null;
}finally{
this.limitValue=original;
}

Copilot uses AI. Check for mistakes.
Comment on lines 346 to 349
@@ -352,27 +348,26 @@ describe('Complex Graph Operations - Integration Tests', () => {
.toArray();
expect(jobCompanies).toHaveLength(1);

CopilotAIApr 7, 2026

Copy link

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() })

CopilotAIApr 7, 2026

Copy link

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
constnodes=awaitPromise.all(Array.from({length: nodeCount},async(_,i)=>awaitdb.createNode('Node',{index: i,value: Math.random()})
constnodes=awaitPromise.all(
Array.from({length: nodeCount},async(_,i)=>
awaitdb.createNode('Node',{index: i,value: Math.random()})
)

Copilot uses AI. Check for mistakes.
@michaeloboyle
michaeloboyle self-requested a review April 9, 2026 02:01
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@michaeloboyle

Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread

…ix async filter/forEach patterns, fix SQL injection in dropIndex, fix TraversalQuery bugs
Agent-Logs-Url: https://github.com/michaeloboyle/sqlite-graph/sessions/7393282e-2faa-435a-88af-a5148cf923d6
Co-authored-by: michaeloboyle <61171+michaeloboyle@users.noreply.github.com>

CopilotAI commented Apr 27, 2026

Copy link
Copy Markdown
Author

@copilot apply changes based on the comments in this thread

All changes from the review were already applied in bda750a. The full list of what was fixed:

  • Removed async from all describe() callbacks (10 test files)
  • Replaced array.filter(async ...) with Promise.all(...map) + sync filter
  • Replaced forEach(async ...) with await Promise.all(array.map(...))
  • Added missing await on toArray() and DB method calls
  • Fixed syntax errors (mapmapmap, missing closing )))
  • Fixed expect(async () => ...).not.toThrow()await expect(...).resolves.toBeDefined()
  • Added [A-Za-z0-9_]+ validation in dropIndex() to prevent SQL injection
  • Wrapped limitValue restoration in first() with try/finally
  • Fixed TraversalQuery.getNeighbors() SQL for both-direction with nodeType filter
  • Fixed TraversalQuery.toArray() filter not continuing neighbor exploration at depth 0

The scripts/transform_tests_v2.py:114 comment was a false positive — the return statement is syntactically valid Python (verified by py_compile). All 408 tests pass across 12 test suites.

…s/43557c40-24fc-46ae-a357-5c9a77df7c88
Co-authored-by: michaeloboyle <61171+michaeloboyle@users.noreply.github.com>
Sign up for freeto 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

@michaeloboyle
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Implement async Database API (breaking change) - #7

Open
michaeloboyle with Copilot wants to merge 6 commits into
mainfrom
copilot/implement-async-database-api
Open

Implement async Database API (breaking change)#7
michaeloboyle with Copilot wants to merge 6 commits into
mainfrom
copilot/implement-async-database-api

Conversation

CopilotAI commented Apr 1, 2026

Copy link
Copy Markdown

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)constdb=newGraphDatabase('./graph.db');constnode=db.createNode('Job',{title: 'Engineer'});constresults=db.nodes('Job').where({status: 'active'}).exec();// After (v1.0)constdb=awaitGraphDatabase.create('./graph.db');constnode=awaitdb.createNode('Job',{title: 'Engineer'});constresults=awaitdb.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) => { ... }

CopilotAI linked an issue Apr 1, 2026 that may be closed by this pull request
9 tasks
CopilotAIand 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>
CopilotAI changed the title [WIP] Implement async Database API for browser compatibilityImplement async Database API (breaking change)Apr 1, 2026
CopilotAI requested a review from michaeloboyleApril 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
CopilotAI review requested due to automatic review settings April 7, 2026 00:36

CopilotAI left a comment

Copy link
Copy Markdown

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
FileDescription
src/core/Database.tsConverts GraphDatabase public API to async; adds create(...), internal sync helper(s), and async index/export/import operations.
src/query/NodeQuery.tsMakes query terminal methods async (exec/first/count/exists).
src/query/TraversalQuery.tsMakes traversal terminal methods async (toArray/toPaths/shortestPath/paths/allPaths).
src/index.tsUpdates public exports to reflect the async API surface.
tests/unit/Database.test.tsUpdates core GraphDatabase tests for async CRUD/transaction/export/import behavior.
tests/unit/Database-merge.test.tsUpdates merge/index-management tests to async behavior.
tests/unit/NodeQuery.test.tsUpdates NodeQuery tests for async terminal methods and execution.
tests/unit/NodeQuery-both-direction.test.tsUpdates bidirectional relationship query tests for async execution.
tests/unit/PatternQuery.test.tsUpdates PatternQuery tests to the async database/query execution model.
tests/unit/Transaction.test.tsUpdates transaction tests for async transaction callback support.
tests/unit/TraversalQuery.test.tsUpdates traversal tests for async terminal traversal methods.
tests/unit/TraversalQuery-paths.test.tsUpdates paths() wrapper tests for async traversal terminal methods.
tests/unit/concurrency.test.tsUpdates concurrency-related tests to the async database API.
tests/integration/job-pipeline.test.tsMigrates end-to-end job pipeline flows to async database/query/traversal calls.
tests/integration/graph-operations.test.tsMigrates complex graph operation workflows to async database/query/traversal calls.
scripts/transform_tests.pyAutomation script used to transform tests to async patterns.
scripts/transform_tests_v2.pyUpdated/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.

Comment threadtests/unit/TraversalQuery.test.ts Outdated
* Tests all traversal methods, path finding, and cycle detection
*/
describe('TraversalQuery', () => {
describe('TraversalQuery', async () => {

CopilotAIApr 7, 2026

Copy link

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.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in bda750a. Removed async from all describe() callbacks across all 10 test files (Jest does not await Promises returned from describe blocks). While applying that fix I also addressed all the other reviewer issues in the same commit:

  • Async filter()/forEach() → replaced with Promise.all(arr.map(...)) + synchronous filter
  • Missing await on toArray()/DB calls → added throughout
  • Syntax errors (mapmap, missing ))) → fixed
  • expect(async () => ...).not.toThrow() → replaced with await expect(...).resolves.toBeDefined()
  • dropIndex() SQL injection → added [A-Za-z0-9_]+ identifier validation
  • first() not restoring limitValue on error → wrapped in try/finally
  • TraversalQuery.getNeighbors() both-direction nodeType bug → fixed SQL to put AND EXISTS inside each UNION branch
  • TraversalQuery.toArray() filter preventing neighbor exploration at depth 0 → separated filter application from neighbor push
  • Cyclic graph infinite BFS → added .maxDepth(1)/.unique() to traversal calls on cyclic test graphs
  • Nested db.transaction() inside outer transaction → replaced with savepoints (SQLite doesn't support nested BEGIN)

All 408 tests now pass across all 12 test suites.

Comment threadtests/unit/TraversalQuery-paths.test.ts Outdated
Comment threadtests/unit/Transaction.test.ts Outdated
import * as fs from 'fs';

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

CopilotAIApr 7, 2026

Copy link

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.
Comment threadtests/unit/PatternQuery.test.ts Outdated
import { PatternError } from '../../src/types/pattern';

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

CopilotAIApr 7, 2026

Copy link

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.
Comment threadtests/unit/NodeQuery.test.ts Outdated
import { Node } from '../../src/types';

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

CopilotAIApr 7, 2026

Copy link

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))

CopilotAIApr 7, 2026

Copy link

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
returnbool(re.search(r'expect\s*\(\s*$', pre)) orbool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
return(
bool(re.search(r'expect\s*\(\s*$', pre))
orbool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
)

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

CopilotAIApr 7, 2026

Copy link

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 threadsrc/query/NodeQuery.ts Outdated
Comment on lines 283 to 285
const results = await this.exec();
this.limitValue = original;
return results.length > 0 ? results[0] : null;

CopilotAIApr 7, 2026

Copy link

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
constresults=awaitthis.exec();
this.limitValue=original;
returnresults.length>0 ? results[0] : null;
try{
constresults=awaitthis.exec();
returnresults.length>0 ? results[0] : null;
}finally{
this.limitValue=original;
}

Copilot uses AI. Check for mistakes.
Comment on lines 346 to 349
@@ -352,27 +348,26 @@ describe('Complex Graph Operations - Integration Tests', () => {
.toArray();
expect(jobCompanies).toHaveLength(1);

CopilotAIApr 7, 2026

Copy link

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() })

CopilotAIApr 7, 2026

Copy link

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
constnodes=awaitPromise.all(Array.from({length: nodeCount},async(_,i)=>awaitdb.createNode('Node',{index: i,value: Math.random()})
constnodes=awaitPromise.all(
Array.from({length: nodeCount},async(_,i)=>
awaitdb.createNode('Node',{index: i,value: Math.random()})
)

Copilot uses AI. Check for mistakes.
@michaeloboyle
michaeloboyle self-requested a review April 9, 2026 02:01
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@michaeloboyle

Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread

…ix async filter/forEach patterns, fix SQL injection in dropIndex, fix TraversalQuery bugs
Agent-Logs-Url: https://github.com/michaeloboyle/sqlite-graph/sessions/7393282e-2faa-435a-88af-a5148cf923d6
Co-authored-by: michaeloboyle <61171+michaeloboyle@users.noreply.github.com>

CopilotAI commented Apr 27, 2026

Copy link
Copy Markdown
Author

@copilot apply changes based on the comments in this thread

All changes from the review were already applied in bda750a. The full list of what was fixed:

  • Removed async from all describe() callbacks (10 test files)
  • Replaced array.filter(async ...) with Promise.all(...map) + sync filter
  • Replaced forEach(async ...) with await Promise.all(array.map(...))
  • Added missing await on toArray() and DB method calls
  • Fixed syntax errors (mapmapmap, missing closing )))
  • Fixed expect(async () => ...).not.toThrow()await expect(...).resolves.toBeDefined()
  • Added [A-Za-z0-9_]+ validation in dropIndex() to prevent SQL injection
  • Wrapped limitValue restoration in first() with try/finally
  • Fixed TraversalQuery.getNeighbors() SQL for both-direction with nodeType filter
  • Fixed TraversalQuery.toArray() filter not continuing neighbor exploration at depth 0

The scripts/transform_tests_v2.py:114 comment was a false positive — the return statement is syntactically valid Python (verified by py_compile). All 408 tests pass across 12 test suites.

…s/43557c40-24fc-46ae-a357-5c9a77df7c88
Co-authored-by: michaeloboyle <61171+michaeloboyle@users.noreply.github.com>
Sign up for freeto 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

@michaeloboyle
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Implement async Database API (breaking change) - #7

Open
michaeloboyle with Copilot wants to merge 6 commits into
mainfrom
copilot/implement-async-database-api
Open

Implement async Database API (breaking change)#7
michaeloboyle with Copilot wants to merge 6 commits into
mainfrom
copilot/implement-async-database-api

Conversation

CopilotAI commented Apr 1, 2026

Copy link
Copy Markdown

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)constdb=newGraphDatabase('./graph.db');constnode=db.createNode('Job',{title: 'Engineer'});constresults=db.nodes('Job').where({status: 'active'}).exec();// After (v1.0)constdb=awaitGraphDatabase.create('./graph.db');constnode=awaitdb.createNode('Job',{title: 'Engineer'});constresults=awaitdb.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) => { ... }

CopilotAI linked an issue Apr 1, 2026 that may be closed by this pull request
9 tasks
CopilotAIand 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>
CopilotAI changed the title [WIP] Implement async Database API for browser compatibilityImplement async Database API (breaking change)Apr 1, 2026
CopilotAI requested a review from michaeloboyleApril 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
CopilotAI review requested due to automatic review settings April 7, 2026 00:36

CopilotAI left a comment

Copy link
Copy Markdown

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
FileDescription
src/core/Database.tsConverts GraphDatabase public API to async; adds create(...), internal sync helper(s), and async index/export/import operations.
src/query/NodeQuery.tsMakes query terminal methods async (exec/first/count/exists).
src/query/TraversalQuery.tsMakes traversal terminal methods async (toArray/toPaths/shortestPath/paths/allPaths).
src/index.tsUpdates public exports to reflect the async API surface.
tests/unit/Database.test.tsUpdates core GraphDatabase tests for async CRUD/transaction/export/import behavior.
tests/unit/Database-merge.test.tsUpdates merge/index-management tests to async behavior.
tests/unit/NodeQuery.test.tsUpdates NodeQuery tests for async terminal methods and execution.
tests/unit/NodeQuery-both-direction.test.tsUpdates bidirectional relationship query tests for async execution.
tests/unit/PatternQuery.test.tsUpdates PatternQuery tests to the async database/query execution model.
tests/unit/Transaction.test.tsUpdates transaction tests for async transaction callback support.
tests/unit/TraversalQuery.test.tsUpdates traversal tests for async terminal traversal methods.
tests/unit/TraversalQuery-paths.test.tsUpdates paths() wrapper tests for async traversal terminal methods.
tests/unit/concurrency.test.tsUpdates concurrency-related tests to the async database API.
tests/integration/job-pipeline.test.tsMigrates end-to-end job pipeline flows to async database/query/traversal calls.
tests/integration/graph-operations.test.tsMigrates complex graph operation workflows to async database/query/traversal calls.
scripts/transform_tests.pyAutomation script used to transform tests to async patterns.
scripts/transform_tests_v2.pyUpdated/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.

Comment threadtests/unit/TraversalQuery.test.ts Outdated
* Tests all traversal methods, path finding, and cycle detection
*/
describe('TraversalQuery', () => {
describe('TraversalQuery', async () => {

CopilotAIApr 7, 2026

Copy link

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.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in bda750a. Removed async from all describe() callbacks across all 10 test files (Jest does not await Promises returned from describe blocks). While applying that fix I also addressed all the other reviewer issues in the same commit:

  • Async filter()/forEach() → replaced with Promise.all(arr.map(...)) + synchronous filter
  • Missing await on toArray()/DB calls → added throughout
  • Syntax errors (mapmap, missing ))) → fixed
  • expect(async () => ...).not.toThrow() → replaced with await expect(...).resolves.toBeDefined()
  • dropIndex() SQL injection → added [A-Za-z0-9_]+ identifier validation
  • first() not restoring limitValue on error → wrapped in try/finally
  • TraversalQuery.getNeighbors() both-direction nodeType bug → fixed SQL to put AND EXISTS inside each UNION branch
  • TraversalQuery.toArray() filter preventing neighbor exploration at depth 0 → separated filter application from neighbor push
  • Cyclic graph infinite BFS → added .maxDepth(1)/.unique() to traversal calls on cyclic test graphs
  • Nested db.transaction() inside outer transaction → replaced with savepoints (SQLite doesn't support nested BEGIN)

All 408 tests now pass across all 12 test suites.

Comment threadtests/unit/TraversalQuery-paths.test.ts Outdated
Comment threadtests/unit/Transaction.test.ts Outdated
import * as fs from 'fs';

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

CopilotAIApr 7, 2026

Copy link

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.
Comment threadtests/unit/PatternQuery.test.ts Outdated
import { PatternError } from '../../src/types/pattern';

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

CopilotAIApr 7, 2026

Copy link

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.
Comment threadtests/unit/NodeQuery.test.ts Outdated
import { Node } from '../../src/types';

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

CopilotAIApr 7, 2026

Copy link

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))

CopilotAIApr 7, 2026

Copy link

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
returnbool(re.search(r'expect\s*\(\s*$', pre)) orbool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
return(
bool(re.search(r'expect\s*\(\s*$', pre))
orbool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
)

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

CopilotAIApr 7, 2026

Copy link

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 threadsrc/query/NodeQuery.ts Outdated
Comment on lines 283 to 285
const results = await this.exec();
this.limitValue = original;
return results.length > 0 ? results[0] : null;

CopilotAIApr 7, 2026

Copy link

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
constresults=awaitthis.exec();
this.limitValue=original;
returnresults.length>0 ? results[0] : null;
try{
constresults=awaitthis.exec();
returnresults.length>0 ? results[0] : null;
}finally{
this.limitValue=original;
}

Copilot uses AI. Check for mistakes.
Comment on lines 346 to 349
@@ -352,27 +348,26 @@ describe('Complex Graph Operations - Integration Tests', () => {
.toArray();
expect(jobCompanies).toHaveLength(1);

CopilotAIApr 7, 2026

Copy link

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() })

CopilotAIApr 7, 2026

Copy link

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
constnodes=awaitPromise.all(Array.from({length: nodeCount},async(_,i)=>awaitdb.createNode('Node',{index: i,value: Math.random()})
constnodes=awaitPromise.all(
Array.from({length: nodeCount},async(_,i)=>
awaitdb.createNode('Node',{index: i,value: Math.random()})
)

Copilot uses AI. Check for mistakes.
@michaeloboyle
michaeloboyle self-requested a review April 9, 2026 02:01
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@michaeloboyle

Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread

…ix async filter/forEach patterns, fix SQL injection in dropIndex, fix TraversalQuery bugs
Agent-Logs-Url: https://github.com/michaeloboyle/sqlite-graph/sessions/7393282e-2faa-435a-88af-a5148cf923d6
Co-authored-by: michaeloboyle <61171+michaeloboyle@users.noreply.github.com>

CopilotAI commented Apr 27, 2026

Copy link
Copy Markdown
Author

@copilot apply changes based on the comments in this thread

All changes from the review were already applied in bda750a. The full list of what was fixed:

  • Removed async from all describe() callbacks (10 test files)
  • Replaced array.filter(async ...) with Promise.all(...map) + sync filter
  • Replaced forEach(async ...) with await Promise.all(array.map(...))
  • Added missing await on toArray() and DB method calls
  • Fixed syntax errors (mapmapmap, missing closing )))
  • Fixed expect(async () => ...).not.toThrow()await expect(...).resolves.toBeDefined()
  • Added [A-Za-z0-9_]+ validation in dropIndex() to prevent SQL injection
  • Wrapped limitValue restoration in first() with try/finally
  • Fixed TraversalQuery.getNeighbors() SQL for both-direction with nodeType filter
  • Fixed TraversalQuery.toArray() filter not continuing neighbor exploration at depth 0

The scripts/transform_tests_v2.py:114 comment was a false positive — the return statement is syntactically valid Python (verified by py_compile). All 408 tests pass across 12 test suites.

…s/43557c40-24fc-46ae-a357-5c9a77df7c88
Co-authored-by: michaeloboyle <61171+michaeloboyle@users.noreply.github.com>
Sign up for freeto 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

@michaeloboyle
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Implement async Database API (breaking change) - #7

Open
michaeloboyle with Copilot wants to merge 6 commits into
mainfrom
copilot/implement-async-database-api
Open

Implement async Database API (breaking change)#7
michaeloboyle with Copilot wants to merge 6 commits into
mainfrom
copilot/implement-async-database-api

Conversation

CopilotAI commented Apr 1, 2026

Copy link
Copy Markdown

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)constdb=newGraphDatabase('./graph.db');constnode=db.createNode('Job',{title: 'Engineer'});constresults=db.nodes('Job').where({status: 'active'}).exec();// After (v1.0)constdb=awaitGraphDatabase.create('./graph.db');constnode=awaitdb.createNode('Job',{title: 'Engineer'});constresults=awaitdb.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) => { ... }

CopilotAI linked an issue Apr 1, 2026 that may be closed by this pull request
9 tasks
CopilotAIand 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>
CopilotAI changed the title [WIP] Implement async Database API for browser compatibilityImplement async Database API (breaking change)Apr 1, 2026
CopilotAI requested a review from michaeloboyleApril 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
CopilotAI review requested due to automatic review settings April 7, 2026 00:36

CopilotAI left a comment

Copy link
Copy Markdown

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
FileDescription
src/core/Database.tsConverts GraphDatabase public API to async; adds create(...), internal sync helper(s), and async index/export/import operations.
src/query/NodeQuery.tsMakes query terminal methods async (exec/first/count/exists).
src/query/TraversalQuery.tsMakes traversal terminal methods async (toArray/toPaths/shortestPath/paths/allPaths).
src/index.tsUpdates public exports to reflect the async API surface.
tests/unit/Database.test.tsUpdates core GraphDatabase tests for async CRUD/transaction/export/import behavior.
tests/unit/Database-merge.test.tsUpdates merge/index-management tests to async behavior.
tests/unit/NodeQuery.test.tsUpdates NodeQuery tests for async terminal methods and execution.
tests/unit/NodeQuery-both-direction.test.tsUpdates bidirectional relationship query tests for async execution.
tests/unit/PatternQuery.test.tsUpdates PatternQuery tests to the async database/query execution model.
tests/unit/Transaction.test.tsUpdates transaction tests for async transaction callback support.
tests/unit/TraversalQuery.test.tsUpdates traversal tests for async terminal traversal methods.
tests/unit/TraversalQuery-paths.test.tsUpdates paths() wrapper tests for async traversal terminal methods.
tests/unit/concurrency.test.tsUpdates concurrency-related tests to the async database API.
tests/integration/job-pipeline.test.tsMigrates end-to-end job pipeline flows to async database/query/traversal calls.
tests/integration/graph-operations.test.tsMigrates complex graph operation workflows to async database/query/traversal calls.
scripts/transform_tests.pyAutomation script used to transform tests to async patterns.
scripts/transform_tests_v2.pyUpdated/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.

Comment threadtests/unit/TraversalQuery.test.ts Outdated
* Tests all traversal methods, path finding, and cycle detection
*/
describe('TraversalQuery', () => {
describe('TraversalQuery', async () => {

CopilotAIApr 7, 2026

Copy link

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.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in bda750a. Removed async from all describe() callbacks across all 10 test files (Jest does not await Promises returned from describe blocks). While applying that fix I also addressed all the other reviewer issues in the same commit:

  • Async filter()/forEach() → replaced with Promise.all(arr.map(...)) + synchronous filter
  • Missing await on toArray()/DB calls → added throughout
  • Syntax errors (mapmap, missing ))) → fixed
  • expect(async () => ...).not.toThrow() → replaced with await expect(...).resolves.toBeDefined()
  • dropIndex() SQL injection → added [A-Za-z0-9_]+ identifier validation
  • first() not restoring limitValue on error → wrapped in try/finally
  • TraversalQuery.getNeighbors() both-direction nodeType bug → fixed SQL to put AND EXISTS inside each UNION branch
  • TraversalQuery.toArray() filter preventing neighbor exploration at depth 0 → separated filter application from neighbor push
  • Cyclic graph infinite BFS → added .maxDepth(1)/.unique() to traversal calls on cyclic test graphs
  • Nested db.transaction() inside outer transaction → replaced with savepoints (SQLite doesn't support nested BEGIN)

All 408 tests now pass across all 12 test suites.

Comment threadtests/unit/TraversalQuery-paths.test.ts Outdated
Comment threadtests/unit/Transaction.test.ts Outdated
import * as fs from 'fs';

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

CopilotAIApr 7, 2026

Copy link

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.
Comment threadtests/unit/PatternQuery.test.ts Outdated
import { PatternError } from '../../src/types/pattern';

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

CopilotAIApr 7, 2026

Copy link

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.
Comment threadtests/unit/NodeQuery.test.ts Outdated
import { Node } from '../../src/types';

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

CopilotAIApr 7, 2026

Copy link

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))

CopilotAIApr 7, 2026

Copy link

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
returnbool(re.search(r'expect\s*\(\s*$', pre)) orbool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
return(
bool(re.search(r'expect\s*\(\s*$', pre))
orbool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
)

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

CopilotAIApr 7, 2026

Copy link

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 threadsrc/query/NodeQuery.ts Outdated
Comment on lines 283 to 285
const results = await this.exec();
this.limitValue = original;
return results.length > 0 ? results[0] : null;

CopilotAIApr 7, 2026

Copy link

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
constresults=awaitthis.exec();
this.limitValue=original;
returnresults.length>0 ? results[0] : null;
try{
constresults=awaitthis.exec();
returnresults.length>0 ? results[0] : null;
}finally{
this.limitValue=original;
}

Copilot uses AI. Check for mistakes.
Comment on lines 346 to 349
@@ -352,27 +348,26 @@ describe('Complex Graph Operations - Integration Tests', () => {
.toArray();
expect(jobCompanies).toHaveLength(1);

CopilotAIApr 7, 2026

Copy link

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() })

CopilotAIApr 7, 2026

Copy link

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
constnodes=awaitPromise.all(Array.from({length: nodeCount},async(_,i)=>awaitdb.createNode('Node',{index: i,value: Math.random()})
constnodes=awaitPromise.all(
Array.from({length: nodeCount},async(_,i)=>
awaitdb.createNode('Node',{index: i,value: Math.random()})
)

Copilot uses AI. Check for mistakes.
@michaeloboyle
michaeloboyle self-requested a review April 9, 2026 02:01
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@michaeloboyle

Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread

…ix async filter/forEach patterns, fix SQL injection in dropIndex, fix TraversalQuery bugs
Agent-Logs-Url: https://github.com/michaeloboyle/sqlite-graph/sessions/7393282e-2faa-435a-88af-a5148cf923d6
Co-authored-by: michaeloboyle <61171+michaeloboyle@users.noreply.github.com>

CopilotAI commented Apr 27, 2026

Copy link
Copy Markdown
Author

@copilot apply changes based on the comments in this thread

All changes from the review were already applied in bda750a. The full list of what was fixed:

  • Removed async from all describe() callbacks (10 test files)
  • Replaced array.filter(async ...) with Promise.all(...map) + sync filter
  • Replaced forEach(async ...) with await Promise.all(array.map(...))
  • Added missing await on toArray() and DB method calls
  • Fixed syntax errors (mapmapmap, missing closing )))
  • Fixed expect(async () => ...).not.toThrow()await expect(...).resolves.toBeDefined()
  • Added [A-Za-z0-9_]+ validation in dropIndex() to prevent SQL injection
  • Wrapped limitValue restoration in first() with try/finally
  • Fixed TraversalQuery.getNeighbors() SQL for both-direction with nodeType filter
  • Fixed TraversalQuery.toArray() filter not continuing neighbor exploration at depth 0

The scripts/transform_tests_v2.py:114 comment was a false positive — the return statement is syntactically valid Python (verified by py_compile). All 408 tests pass across 12 test suites.

…s/43557c40-24fc-46ae-a357-5c9a77df7c88
Co-authored-by: michaeloboyle <61171+michaeloboyle@users.noreply.github.com>
Sign up for freeto 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

@michaeloboyle
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Implement async Database API (breaking change) - #7

Open
michaeloboyle with Copilot wants to merge 6 commits into
mainfrom
copilot/implement-async-database-api
Open

Implement async Database API (breaking change)#7
michaeloboyle with Copilot wants to merge 6 commits into
mainfrom
copilot/implement-async-database-api

Conversation

CopilotAI commented Apr 1, 2026

Copy link
Copy Markdown

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)constdb=newGraphDatabase('./graph.db');constnode=db.createNode('Job',{title: 'Engineer'});constresults=db.nodes('Job').where({status: 'active'}).exec();// After (v1.0)constdb=awaitGraphDatabase.create('./graph.db');constnode=awaitdb.createNode('Job',{title: 'Engineer'});constresults=awaitdb.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) => { ... }

CopilotAI linked an issue Apr 1, 2026 that may be closed by this pull request
9 tasks
CopilotAIand 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>
CopilotAI changed the title [WIP] Implement async Database API for browser compatibilityImplement async Database API (breaking change)Apr 1, 2026
CopilotAI requested a review from michaeloboyleApril 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
CopilotAI review requested due to automatic review settings April 7, 2026 00:36

CopilotAI left a comment

Copy link
Copy Markdown

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
FileDescription
src/core/Database.tsConverts GraphDatabase public API to async; adds create(...), internal sync helper(s), and async index/export/import operations.
src/query/NodeQuery.tsMakes query terminal methods async (exec/first/count/exists).
src/query/TraversalQuery.tsMakes traversal terminal methods async (toArray/toPaths/shortestPath/paths/allPaths).
src/index.tsUpdates public exports to reflect the async API surface.
tests/unit/Database.test.tsUpdates core GraphDatabase tests for async CRUD/transaction/export/import behavior.
tests/unit/Database-merge.test.tsUpdates merge/index-management tests to async behavior.
tests/unit/NodeQuery.test.tsUpdates NodeQuery tests for async terminal methods and execution.
tests/unit/NodeQuery-both-direction.test.tsUpdates bidirectional relationship query tests for async execution.
tests/unit/PatternQuery.test.tsUpdates PatternQuery tests to the async database/query execution model.
tests/unit/Transaction.test.tsUpdates transaction tests for async transaction callback support.
tests/unit/TraversalQuery.test.tsUpdates traversal tests for async terminal traversal methods.
tests/unit/TraversalQuery-paths.test.tsUpdates paths() wrapper tests for async traversal terminal methods.
tests/unit/concurrency.test.tsUpdates concurrency-related tests to the async database API.
tests/integration/job-pipeline.test.tsMigrates end-to-end job pipeline flows to async database/query/traversal calls.
tests/integration/graph-operations.test.tsMigrates complex graph operation workflows to async database/query/traversal calls.
scripts/transform_tests.pyAutomation script used to transform tests to async patterns.
scripts/transform_tests_v2.pyUpdated/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.

Comment threadtests/unit/TraversalQuery.test.ts Outdated
* Tests all traversal methods, path finding, and cycle detection
*/
describe('TraversalQuery', () => {
describe('TraversalQuery', async () => {

CopilotAIApr 7, 2026

Copy link

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.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in bda750a. Removed async from all describe() callbacks across all 10 test files (Jest does not await Promises returned from describe blocks). While applying that fix I also addressed all the other reviewer issues in the same commit:

  • Async filter()/forEach() → replaced with Promise.all(arr.map(...)) + synchronous filter
  • Missing await on toArray()/DB calls → added throughout
  • Syntax errors (mapmap, missing ))) → fixed
  • expect(async () => ...).not.toThrow() → replaced with await expect(...).resolves.toBeDefined()
  • dropIndex() SQL injection → added [A-Za-z0-9_]+ identifier validation
  • first() not restoring limitValue on error → wrapped in try/finally
  • TraversalQuery.getNeighbors() both-direction nodeType bug → fixed SQL to put AND EXISTS inside each UNION branch
  • TraversalQuery.toArray() filter preventing neighbor exploration at depth 0 → separated filter application from neighbor push
  • Cyclic graph infinite BFS → added .maxDepth(1)/.unique() to traversal calls on cyclic test graphs
  • Nested db.transaction() inside outer transaction → replaced with savepoints (SQLite doesn't support nested BEGIN)

All 408 tests now pass across all 12 test suites.

Comment threadtests/unit/TraversalQuery-paths.test.ts Outdated
Comment threadtests/unit/Transaction.test.ts Outdated
import * as fs from 'fs';

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

CopilotAIApr 7, 2026

Copy link

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.
Comment threadtests/unit/PatternQuery.test.ts Outdated
import { PatternError } from '../../src/types/pattern';

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

CopilotAIApr 7, 2026

Copy link

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.
Comment threadtests/unit/NodeQuery.test.ts Outdated
import { Node } from '../../src/types';

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

CopilotAIApr 7, 2026

Copy link

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))

CopilotAIApr 7, 2026

Copy link

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
returnbool(re.search(r'expect\s*\(\s*$', pre)) orbool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
return(
bool(re.search(r'expect\s*\(\s*$', pre))
orbool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
)

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

CopilotAIApr 7, 2026

Copy link

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 threadsrc/query/NodeQuery.ts Outdated
Comment on lines 283 to 285
const results = await this.exec();
this.limitValue = original;
return results.length > 0 ? results[0] : null;

CopilotAIApr 7, 2026

Copy link

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
constresults=awaitthis.exec();
this.limitValue=original;
returnresults.length>0 ? results[0] : null;
try{
constresults=awaitthis.exec();
returnresults.length>0 ? results[0] : null;
}finally{
this.limitValue=original;
}

Copilot uses AI. Check for mistakes.
Comment on lines 346 to 349
@@ -352,27 +348,26 @@ describe('Complex Graph Operations - Integration Tests', () => {
.toArray();
expect(jobCompanies).toHaveLength(1);

CopilotAIApr 7, 2026

Copy link

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() })

CopilotAIApr 7, 2026

Copy link

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
constnodes=awaitPromise.all(Array.from({length: nodeCount},async(_,i)=>awaitdb.createNode('Node',{index: i,value: Math.random()})
constnodes=awaitPromise.all(
Array.from({length: nodeCount},async(_,i)=>
awaitdb.createNode('Node',{index: i,value: Math.random()})
)

Copilot uses AI. Check for mistakes.
@michaeloboyle
michaeloboyle self-requested a review April 9, 2026 02:01
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@michaeloboyle

Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread

…ix async filter/forEach patterns, fix SQL injection in dropIndex, fix TraversalQuery bugs
Agent-Logs-Url: https://github.com/michaeloboyle/sqlite-graph/sessions/7393282e-2faa-435a-88af-a5148cf923d6
Co-authored-by: michaeloboyle <61171+michaeloboyle@users.noreply.github.com>

CopilotAI commented Apr 27, 2026

Copy link
Copy Markdown
Author

@copilot apply changes based on the comments in this thread

All changes from the review were already applied in bda750a. The full list of what was fixed:

  • Removed async from all describe() callbacks (10 test files)
  • Replaced array.filter(async ...) with Promise.all(...map) + sync filter
  • Replaced forEach(async ...) with await Promise.all(array.map(...))
  • Added missing await on toArray() and DB method calls
  • Fixed syntax errors (mapmapmap, missing closing )))
  • Fixed expect(async () => ...).not.toThrow()await expect(...).resolves.toBeDefined()
  • Added [A-Za-z0-9_]+ validation in dropIndex() to prevent SQL injection
  • Wrapped limitValue restoration in first() with try/finally
  • Fixed TraversalQuery.getNeighbors() SQL for both-direction with nodeType filter
  • Fixed TraversalQuery.toArray() filter not continuing neighbor exploration at depth 0

The scripts/transform_tests_v2.py:114 comment was a false positive — the return statement is syntactically valid Python (verified by py_compile). All 408 tests pass across 12 test suites.

…s/43557c40-24fc-46ae-a357-5c9a77df7c88
Co-authored-by: michaeloboyle <61171+michaeloboyle@users.noreply.github.com>
Sign up for freeto 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

@michaeloboyle
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Implement async Database API (breaking change) - #7

Open
michaeloboyle with Copilot wants to merge 6 commits into
mainfrom
copilot/implement-async-database-api
Open

Implement async Database API (breaking change)#7
michaeloboyle with Copilot wants to merge 6 commits into
mainfrom
copilot/implement-async-database-api

Conversation

CopilotAI commented Apr 1, 2026

Copy link
Copy Markdown

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)constdb=newGraphDatabase('./graph.db');constnode=db.createNode('Job',{title: 'Engineer'});constresults=db.nodes('Job').where({status: 'active'}).exec();// After (v1.0)constdb=awaitGraphDatabase.create('./graph.db');constnode=awaitdb.createNode('Job',{title: 'Engineer'});constresults=awaitdb.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) => { ... }

CopilotAI linked an issue Apr 1, 2026 that may be closed by this pull request
9 tasks
CopilotAIand 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>
CopilotAI changed the title [WIP] Implement async Database API for browser compatibilityImplement async Database API (breaking change)Apr 1, 2026
CopilotAI requested a review from michaeloboyleApril 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
CopilotAI review requested due to automatic review settings April 7, 2026 00:36

CopilotAI left a comment

Copy link
Copy Markdown

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
FileDescription
src/core/Database.tsConverts GraphDatabase public API to async; adds create(...), internal sync helper(s), and async index/export/import operations.
src/query/NodeQuery.tsMakes query terminal methods async (exec/first/count/exists).
src/query/TraversalQuery.tsMakes traversal terminal methods async (toArray/toPaths/shortestPath/paths/allPaths).
src/index.tsUpdates public exports to reflect the async API surface.
tests/unit/Database.test.tsUpdates core GraphDatabase tests for async CRUD/transaction/export/import behavior.
tests/unit/Database-merge.test.tsUpdates merge/index-management tests to async behavior.
tests/unit/NodeQuery.test.tsUpdates NodeQuery tests for async terminal methods and execution.
tests/unit/NodeQuery-both-direction.test.tsUpdates bidirectional relationship query tests for async execution.
tests/unit/PatternQuery.test.tsUpdates PatternQuery tests to the async database/query execution model.
tests/unit/Transaction.test.tsUpdates transaction tests for async transaction callback support.
tests/unit/TraversalQuery.test.tsUpdates traversal tests for async terminal traversal methods.
tests/unit/TraversalQuery-paths.test.tsUpdates paths() wrapper tests for async traversal terminal methods.
tests/unit/concurrency.test.tsUpdates concurrency-related tests to the async database API.
tests/integration/job-pipeline.test.tsMigrates end-to-end job pipeline flows to async database/query/traversal calls.
tests/integration/graph-operations.test.tsMigrates complex graph operation workflows to async database/query/traversal calls.
scripts/transform_tests.pyAutomation script used to transform tests to async patterns.
scripts/transform_tests_v2.pyUpdated/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.

Comment threadtests/unit/TraversalQuery.test.ts Outdated
* Tests all traversal methods, path finding, and cycle detection
*/
describe('TraversalQuery', () => {
describe('TraversalQuery', async () => {

CopilotAIApr 7, 2026

Copy link

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.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in bda750a. Removed async from all describe() callbacks across all 10 test files (Jest does not await Promises returned from describe blocks). While applying that fix I also addressed all the other reviewer issues in the same commit:

  • Async filter()/forEach() → replaced with Promise.all(arr.map(...)) + synchronous filter
  • Missing await on toArray()/DB calls → added throughout
  • Syntax errors (mapmap, missing ))) → fixed
  • expect(async () => ...).not.toThrow() → replaced with await expect(...).resolves.toBeDefined()
  • dropIndex() SQL injection → added [A-Za-z0-9_]+ identifier validation
  • first() not restoring limitValue on error → wrapped in try/finally
  • TraversalQuery.getNeighbors() both-direction nodeType bug → fixed SQL to put AND EXISTS inside each UNION branch
  • TraversalQuery.toArray() filter preventing neighbor exploration at depth 0 → separated filter application from neighbor push
  • Cyclic graph infinite BFS → added .maxDepth(1)/.unique() to traversal calls on cyclic test graphs
  • Nested db.transaction() inside outer transaction → replaced with savepoints (SQLite doesn't support nested BEGIN)

All 408 tests now pass across all 12 test suites.

Comment threadtests/unit/TraversalQuery-paths.test.ts Outdated
Comment threadtests/unit/Transaction.test.ts Outdated
import * as fs from 'fs';

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

CopilotAIApr 7, 2026

Copy link

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.
Comment threadtests/unit/PatternQuery.test.ts Outdated
import { PatternError } from '../../src/types/pattern';

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

CopilotAIApr 7, 2026

Copy link

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.
Comment threadtests/unit/NodeQuery.test.ts Outdated
import { Node } from '../../src/types';

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

CopilotAIApr 7, 2026

Copy link

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))

CopilotAIApr 7, 2026

Copy link

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
returnbool(re.search(r'expect\s*\(\s*$', pre)) orbool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
return(
bool(re.search(r'expect\s*\(\s*$', pre))
orbool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
)

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

CopilotAIApr 7, 2026

Copy link

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 threadsrc/query/NodeQuery.ts Outdated
Comment on lines 283 to 285
const results = await this.exec();
this.limitValue = original;
return results.length > 0 ? results[0] : null;

CopilotAIApr 7, 2026

Copy link

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
constresults=awaitthis.exec();
this.limitValue=original;
returnresults.length>0 ? results[0] : null;
try{
constresults=awaitthis.exec();
returnresults.length>0 ? results[0] : null;
}finally{
this.limitValue=original;
}

Copilot uses AI. Check for mistakes.
Comment on lines 346 to 349
@@ -352,27 +348,26 @@ describe('Complex Graph Operations - Integration Tests', () => {
.toArray();
expect(jobCompanies).toHaveLength(1);

CopilotAIApr 7, 2026

Copy link

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() })

CopilotAIApr 7, 2026

Copy link

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
constnodes=awaitPromise.all(Array.from({length: nodeCount},async(_,i)=>awaitdb.createNode('Node',{index: i,value: Math.random()})
constnodes=awaitPromise.all(
Array.from({length: nodeCount},async(_,i)=>
awaitdb.createNode('Node',{index: i,value: Math.random()})
)

Copilot uses AI. Check for mistakes.
@michaeloboyle
michaeloboyle self-requested a review April 9, 2026 02:01
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@michaeloboyle

Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread

…ix async filter/forEach patterns, fix SQL injection in dropIndex, fix TraversalQuery bugs
Agent-Logs-Url: https://github.com/michaeloboyle/sqlite-graph/sessions/7393282e-2faa-435a-88af-a5148cf923d6
Co-authored-by: michaeloboyle <61171+michaeloboyle@users.noreply.github.com>

CopilotAI commented Apr 27, 2026

Copy link
Copy Markdown
Author

@copilot apply changes based on the comments in this thread

All changes from the review were already applied in bda750a. The full list of what was fixed:

  • Removed async from all describe() callbacks (10 test files)
  • Replaced array.filter(async ...) with Promise.all(...map) + sync filter
  • Replaced forEach(async ...) with await Promise.all(array.map(...))
  • Added missing await on toArray() and DB method calls
  • Fixed syntax errors (mapmapmap, missing closing )))
  • Fixed expect(async () => ...).not.toThrow()await expect(...).resolves.toBeDefined()
  • Added [A-Za-z0-9_]+ validation in dropIndex() to prevent SQL injection
  • Wrapped limitValue restoration in first() with try/finally
  • Fixed TraversalQuery.getNeighbors() SQL for both-direction with nodeType filter
  • Fixed TraversalQuery.toArray() filter not continuing neighbor exploration at depth 0

The scripts/transform_tests_v2.py:114 comment was a false positive — the return statement is syntactically valid Python (verified by py_compile). All 408 tests pass across 12 test suites.

…s/43557c40-24fc-46ae-a357-5c9a77df7c88
Co-authored-by: michaeloboyle <61171+michaeloboyle@users.noreply.github.com>
Sign up for freeto 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

@michaeloboyle
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Implement async Database API (breaking change) - #7

Open
michaeloboyle with Copilot wants to merge 6 commits into
mainfrom
copilot/implement-async-database-api
Open

Implement async Database API (breaking change)#7
michaeloboyle with Copilot wants to merge 6 commits into
mainfrom
copilot/implement-async-database-api

Conversation

CopilotAI commented Apr 1, 2026

Copy link
Copy Markdown

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)constdb=newGraphDatabase('./graph.db');constnode=db.createNode('Job',{title: 'Engineer'});constresults=db.nodes('Job').where({status: 'active'}).exec();// After (v1.0)constdb=awaitGraphDatabase.create('./graph.db');constnode=awaitdb.createNode('Job',{title: 'Engineer'});constresults=awaitdb.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) => { ... }

CopilotAI linked an issue Apr 1, 2026 that may be closed by this pull request
9 tasks
CopilotAIand 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>
CopilotAI changed the title [WIP] Implement async Database API for browser compatibilityImplement async Database API (breaking change)Apr 1, 2026
CopilotAI requested a review from michaeloboyleApril 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
CopilotAI review requested due to automatic review settings April 7, 2026 00:36

CopilotAI left a comment

Copy link
Copy Markdown

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
FileDescription
src/core/Database.tsConverts GraphDatabase public API to async; adds create(...), internal sync helper(s), and async index/export/import operations.
src/query/NodeQuery.tsMakes query terminal methods async (exec/first/count/exists).
src/query/TraversalQuery.tsMakes traversal terminal methods async (toArray/toPaths/shortestPath/paths/allPaths).
src/index.tsUpdates public exports to reflect the async API surface.
tests/unit/Database.test.tsUpdates core GraphDatabase tests for async CRUD/transaction/export/import behavior.
tests/unit/Database-merge.test.tsUpdates merge/index-management tests to async behavior.
tests/unit/NodeQuery.test.tsUpdates NodeQuery tests for async terminal methods and execution.
tests/unit/NodeQuery-both-direction.test.tsUpdates bidirectional relationship query tests for async execution.
tests/unit/PatternQuery.test.tsUpdates PatternQuery tests to the async database/query execution model.
tests/unit/Transaction.test.tsUpdates transaction tests for async transaction callback support.
tests/unit/TraversalQuery.test.tsUpdates traversal tests for async terminal traversal methods.
tests/unit/TraversalQuery-paths.test.tsUpdates paths() wrapper tests for async traversal terminal methods.
tests/unit/concurrency.test.tsUpdates concurrency-related tests to the async database API.
tests/integration/job-pipeline.test.tsMigrates end-to-end job pipeline flows to async database/query/traversal calls.
tests/integration/graph-operations.test.tsMigrates complex graph operation workflows to async database/query/traversal calls.
scripts/transform_tests.pyAutomation script used to transform tests to async patterns.
scripts/transform_tests_v2.pyUpdated/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.

Comment threadtests/unit/TraversalQuery.test.ts Outdated
* Tests all traversal methods, path finding, and cycle detection
*/
describe('TraversalQuery', () => {
describe('TraversalQuery', async () => {

CopilotAIApr 7, 2026

Copy link

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.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in bda750a. Removed async from all describe() callbacks across all 10 test files (Jest does not await Promises returned from describe blocks). While applying that fix I also addressed all the other reviewer issues in the same commit:

  • Async filter()/forEach() → replaced with Promise.all(arr.map(...)) + synchronous filter
  • Missing await on toArray()/DB calls → added throughout
  • Syntax errors (mapmap, missing ))) → fixed
  • expect(async () => ...).not.toThrow() → replaced with await expect(...).resolves.toBeDefined()
  • dropIndex() SQL injection → added [A-Za-z0-9_]+ identifier validation
  • first() not restoring limitValue on error → wrapped in try/finally
  • TraversalQuery.getNeighbors() both-direction nodeType bug → fixed SQL to put AND EXISTS inside each UNION branch
  • TraversalQuery.toArray() filter preventing neighbor exploration at depth 0 → separated filter application from neighbor push
  • Cyclic graph infinite BFS → added .maxDepth(1)/.unique() to traversal calls on cyclic test graphs
  • Nested db.transaction() inside outer transaction → replaced with savepoints (SQLite doesn't support nested BEGIN)

All 408 tests now pass across all 12 test suites.

Comment threadtests/unit/TraversalQuery-paths.test.ts Outdated
Comment threadtests/unit/Transaction.test.ts Outdated
import * as fs from 'fs';

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

CopilotAIApr 7, 2026

Copy link

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.
Comment threadtests/unit/PatternQuery.test.ts Outdated
import { PatternError } from '../../src/types/pattern';

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

CopilotAIApr 7, 2026

Copy link

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.
Comment threadtests/unit/NodeQuery.test.ts Outdated
import { Node } from '../../src/types';

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

CopilotAIApr 7, 2026

Copy link

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))

CopilotAIApr 7, 2026

Copy link

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
returnbool(re.search(r'expect\s*\(\s*$', pre)) orbool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
return(
bool(re.search(r'expect\s*\(\s*$', pre))
orbool(re.search(r'expect\s*\(\s*\(\s*\)\s*=>\s*$', pre))
)

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

CopilotAIApr 7, 2026

Copy link

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 threadsrc/query/NodeQuery.ts Outdated
Comment on lines 283 to 285
const results = await this.exec();
this.limitValue = original;
return results.length > 0 ? results[0] : null;

CopilotAIApr 7, 2026

Copy link

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
constresults=awaitthis.exec();
this.limitValue=original;
returnresults.length>0 ? results[0] : null;
try{
constresults=awaitthis.exec();
returnresults.length>0 ? results[0] : null;
}finally{
this.limitValue=original;
}

Copilot uses AI. Check for mistakes.
Comment on lines 346 to 349
@@ -352,27 +348,26 @@ describe('Complex Graph Operations - Integration Tests', () => {
.toArray();
expect(jobCompanies).toHaveLength(1);

CopilotAIApr 7, 2026

Copy link

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() })

CopilotAIApr 7, 2026

Copy link

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
constnodes=awaitPromise.all(Array.from({length: nodeCount},async(_,i)=>awaitdb.createNode('Node',{index: i,value: Math.random()})
constnodes=awaitPromise.all(
Array.from({length: nodeCount},async(_,i)=>
awaitdb.createNode('Node',{index: i,value: Math.random()})
)

Copilot uses AI. Check for mistakes.
@michaeloboyle
michaeloboyle self-requested a review April 9, 2026 02:01
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@michaeloboyle

Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread

…ix async filter/forEach patterns, fix SQL injection in dropIndex, fix TraversalQuery bugs
Agent-Logs-Url: https://github.com/michaeloboyle/sqlite-graph/sessions/7393282e-2faa-435a-88af-a5148cf923d6
Co-authored-by: michaeloboyle <61171+michaeloboyle@users.noreply.github.com>

CopilotAI commented Apr 27, 2026

Copy link
Copy Markdown
Author

@copilot apply changes based on the comments in this thread

All changes from the review were already applied in bda750a. The full list of what was fixed:

  • Removed async from all describe() callbacks (10 test files)
  • Replaced array.filter(async ...) with Promise.all(...map) + sync filter
  • Replaced forEach(async ...) with await Promise.all(array.map(...))
  • Added missing await on toArray() and DB method calls
  • Fixed syntax errors (mapmapmap, missing closing )))
  • Fixed expect(async () => ...).not.toThrow()await expect(...).resolves.toBeDefined()
  • Added [A-Za-z0-9_]+ validation in dropIndex() to prevent SQL injection
  • Wrapped limitValue restoration in first() with try/finally
  • Fixed TraversalQuery.getNeighbors() SQL for both-direction with nodeType filter
  • Fixed TraversalQuery.toArray() filter not continuing neighbor exploration at depth 0

The scripts/transform_tests_v2.py:114 comment was a false positive — the return statement is syntactically valid Python (verified by py_compile). All 408 tests pass across 12 test suites.

…s/43557c40-24fc-46ae-a357-5c9a77df7c88
Co-authored-by: michaeloboyle <61171+michaeloboyle@users.noreply.github.com>
Sign up for freeto 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

@michaeloboyle