Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
717 changes: 717 additions & 0 deletions scripts/transform_tests.py

Large diffs are not rendered by default.

822 changes: 822 additions & 0 deletions scripts/transform_tests_v2.py

Large diffs are not rendered by default.

101 changes: 68 additions & 33 deletions src/core/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,42 @@
this.prepareStatements();
}

/**
* Async factory method for creating a GraphDatabase instance.
* Preferred over the constructor for async-first code.
*
* @param path - Path to SQLite database file. Use ':memory:' for in-memory database.
* @param options - Database configuration options
* @returns A Promise resolving to a new GraphDatabase instance
*
* @example
* ```typescript
* const db = await GraphDatabase.create('./graph.db');
* ```
*/
static async create(path: string, options?: DatabaseOptions): Promise<GraphDatabase> {
return new GraphDatabase(path, options);
}

/**
* Get a node by ID synchronously (internal helper).
* @private
*/
private _getNodeSync(id: number): Node | null {
const stmt = this.preparedStatements.get('getNode')!;
const row = stmt.get(id) as any;

Check warning on line 134 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Unexpected any. Specify a different type

Check warning on line 134 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Unexpected any. Specify a different type

if (!row) return null;

return {
id: row.id,
type: row.type,
properties: deserialize(row.properties),
createdAt: timestampToDate(row.created_at),
updatedAt: timestampToDate(row.updated_at)
};
}

/**
* Prepare frequently used SQL statements for better performance.
* @private
Expand Down Expand Up @@ -172,12 +208,12 @@
* console.log(job.createdAt); // 2025-10-27T...
* ```
*/
createNode<T extends NodeData = NodeData>(type: string, properties: T): Node<T> {
async createNode<T extends NodeData = NodeData>(type: string, properties: T): Promise<Node<T>> {
validateNodeType(type, this.schema);
validateNodeProperties(type, properties, this.schema);

const stmt = this.preparedStatements.get('insertNode')!;
const row = stmt.get(type, serialize(properties)) as any;

Check warning on line 216 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Unexpected any. Specify a different type

Check warning on line 216 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Unexpected any. Specify a different type

return {
id: row.id,
Expand All @@ -204,11 +240,11 @@
* }
* ```
*/
getNode(id: number): Node | null {
async getNode(id: number): Promise<Node | null> {
validateNodeId(id);

const stmt = this.preparedStatements.get('getNode')!;
const row = stmt.get(id) as any;

Check warning on line 247 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Unexpected any. Specify a different type

Check warning on line 247 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Unexpected any. Specify a different type

if (!row) return null;

Expand Down Expand Up @@ -239,17 +275,17 @@
* });
* ```
*/
updateNode(id: number, properties: Partial<NodeData>): Node {
async updateNode(id: number, properties: Partial<NodeData>): Promise<Node> {
validateNodeId(id);

const existing = this.getNode(id);
const existing = this._getNodeSync(id);
if (!existing) {
throw new Error(`Node with ID ${id} not found`);
}

const merged = { ...existing.properties, ...properties };
const stmt = this.preparedStatements.get('updateNode')!;
const row = stmt.get(serialize(merged), id) as any;

Check warning on line 288 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Unexpected any. Specify a different type

Check warning on line 288 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Unexpected any. Specify a different type

return {
id: row.id,
Expand All @@ -274,7 +310,7 @@
* console.log(deleted ? 'Deleted' : 'Not found');
* ```
*/
deleteNode(id: number): boolean {
async deleteNode(id: number): Promise<boolean> {
validateNodeId(id);

const stmt = this.preparedStatements.get('deleteNode')!;
Expand Down Expand Up @@ -305,19 +341,19 @@
* });
* ```
*/
createEdge<T extends NodeData = NodeData>(
async createEdge<T extends NodeData = NodeData>(
from: number,
type: string,
to: number,
properties?: T
): Edge<T> {
): Promise<Edge<T>> {
validateEdgeType(type, this.schema);
validateNodeId(from);
validateNodeId(to);

// Verify nodes exist
const fromNode = this.getNode(from);
const toNode = this.getNode(to);
const fromNode = this._getNodeSync(from);
const toNode = this._getNodeSync(to);

if (!fromNode) {
throw new Error(`Source node with ID ${from} not found`);
Expand All @@ -332,7 +368,7 @@
from,
to,
properties ? serialize(properties) : null
) as any;

Check warning on line 371 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Unexpected any. Specify a different type

Check warning on line 371 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Unexpected any. Specify a different type

return {
id: row.id,
Expand All @@ -358,11 +394,11 @@
* }
* ```
*/
getEdge(id: number): Edge | null {
async getEdge(id: number): Promise<Edge | null> {
validateNodeId(id);

const stmt = this.preparedStatements.get('getEdge')!;
const row = stmt.get(id) as any;

Check warning on line 401 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Unexpected any. Specify a different type

Check warning on line 401 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Unexpected any. Specify a different type

if (!row) return null;

Expand All @@ -387,7 +423,7 @@
* const deleted = db.deleteEdge(1);
* ```
*/
deleteEdge(id: number): boolean {
async deleteEdge(id: number): Promise<boolean> {
validateNodeId(id);

const stmt = this.preparedStatements.get('deleteEdge')!;
Expand Down Expand Up @@ -439,7 +475,7 @@
traverse(startNodeId: number): TraversalQuery {
validateNodeId(startNodeId);

const node = this.getNode(startNodeId);
const node = this.db.prepare('SELECT id FROM nodes WHERE id = ?').get(startNodeId);
if (!node) {
throw new Error(`Start node with ID ${startNodeId} not found`);
}
Expand Down Expand Up @@ -468,7 +504,7 @@
* .exec();
* ```
*/
pattern<T extends Record<string, unknown> = Record<string, unknown>>(): PatternQuery<T> {
pattern<T extends Record<string, GraphEntity> = Record<string, GraphEntity>>(): PatternQuery<T> {

Check failure on line 507 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Cannot find name 'GraphEntity'.

Check failure on line 507 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Cannot find name 'GraphEntity'.

Check failure on line 507 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Cannot find name 'GraphEntity'.

Check failure on line 507 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Cannot find name 'GraphEntity'.
return new PatternQuery<T>(this.db);
}

Expand Down Expand Up @@ -505,14 +541,14 @@
* });
* ```
*/
transaction<T>(fn: (ctx: TransactionContext) => T): T {
async transaction<T>(fn: (ctx: TransactionContext) => T | Promise<T>): Promise<T> {
// Start transaction
this.db.prepare('BEGIN').run();

const ctx = new TransactionContext(this.db);

try {
const result = fn(ctx);
const result = await fn(ctx);

// Auto-commit if not manually finalized
if (!ctx.isFinalized()) {
Expand Down Expand Up @@ -540,11 +576,11 @@
* fs.writeFileSync('graph-backup.json', JSON.stringify(data, null, 2));
* ```
*/
export(): GraphExport {
async export(): Promise<GraphExport> {
const nodesStmt = this.db.prepare('SELECT * FROM nodes ORDER BY id');
const edgesStmt = this.db.prepare('SELECT * FROM edges ORDER BY id');

const nodes = nodesStmt.all().map((row: any) => ({

Check warning on line 583 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Unexpected any. Specify a different type

Check warning on line 583 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Unexpected any. Specify a different type
id: row.id,
type: row.type,
properties: deserialize(row.properties),
Expand All @@ -552,7 +588,7 @@
updatedAt: timestampToDate(row.updated_at)
}));

const edges = edgesStmt.all().map((row: any) => ({

Check warning on line 591 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Unexpected any. Specify a different type

Check warning on line 591 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Unexpected any. Specify a different type
id: row.id,
type: row.type,
from: row.from_id,
Expand Down Expand Up @@ -585,14 +621,14 @@
* db.import(data);
* ```
*/
import(data: GraphExport): void {
this.transaction(() => {
async import(data: GraphExport): Promise<void> {
await this.transaction(async () => {
for (const node of data.nodes) {
this.createNode(node.type, node.properties);
await this.createNode(node.type, node.properties);
}

for (const edge of data.edges) {
this.createEdge(edge.from, edge.type, edge.to, edge.properties);
await this.createEdge(edge.from, edge.type, edge.to, edge.properties);
}
});
}
Expand All @@ -606,7 +642,7 @@
* db.close();
* ```
*/
close(): void {
async close(): Promise<void> {
this.db.close();
}

Expand Down Expand Up @@ -654,12 +690,12 @@
* );
* ```
*/
mergeNode<T extends NodeData = NodeData>(
async mergeNode<T extends NodeData = NodeData>(
type: string,
matchProperties: Partial<T>,
baseProperties?: T,
options?: MergeOptions<T>
): MergeResult<T> {
): Promise<MergeResult<T>> {
validateNodeType(type, this.schema);

// Build WHERE clause for all match properties
Expand All @@ -677,8 +713,7 @@
}
}

return this.transaction(() => {
// Build SQL to find matching node
return await this.transaction(() => {
const whereConditions = matchKeys.map(
(key) => `json_extract(properties, '$.${key}') = ?`
);
Expand All @@ -686,10 +721,10 @@
SELECT * FROM nodes
WHERE type = ? AND ${whereConditions.join(' AND ')}
`;
const matchValues = matchKeys.map((key) => (matchProperties as any)[key]);

Check warning on line 724 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Unexpected any. Specify a different type

Check warning on line 724 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Unexpected any. Specify a different type

const stmt = this.db.prepare(sql);
const rows = stmt.all(type, ...matchValues) as any[];

Check warning on line 727 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Unexpected any. Specify a different type

Check warning on line 727 in src/core/Database.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Unexpected any. Specify a different type

if (rows.length > 1) {
const nodes = rows.map((row) => ({
Expand Down Expand Up @@ -781,20 +816,20 @@
* );
* ```
*/
mergeEdge<T extends NodeData = NodeData>(
async mergeEdge<T extends NodeData = NodeData>(
from: number,
type: string,
to: number,
properties?: T,
options?: EdgeMergeOptions<T>
): EdgeMergeResult<T> {
): Promise<EdgeMergeResult<T>> {
validateEdgeType(type, this.schema);
validateNodeId(from);
validateNodeId(to);

// Verify nodes exist
const fromNode = this.getNode(from);
const toNode = this.getNode(to);
const fromNode = this._getNodeSync(from);
const toNode = this._getNodeSync(to);

if (!fromNode) {
throw new Error(`Source node with ID ${from} not found`);
Expand All @@ -803,7 +838,7 @@
throw new Error(`Target node with ID ${to} not found`);
}

return this.transaction(() => {
return await this.transaction(() => {
// Find existing edges
const stmt = this.db.prepare(`
SELECT * FROM edges
Expand Down Expand Up @@ -923,7 +958,7 @@
* db.mergeNode('Job', { url: 'https://...' }, ...);
* ```
*/
createPropertyIndex(nodeType: string, property: string, unique = false): void {
async createPropertyIndex(nodeType: string, property: string, unique = false): Promise<void> {
const indexName = `idx_merge_${nodeType}_${property}`;
const uniqueClause = unique ? 'UNIQUE' : '';

Expand Down Expand Up @@ -969,7 +1004,7 @@
* });
* ```
*/
listIndexes(): IndexInfo[] {
async listIndexes(): Promise<IndexInfo[]> {
const stmt = this.db.prepare(`
SELECT name, tbl_name as 'table', sql
FROM sqlite_master
Expand Down Expand Up @@ -1002,7 +1037,7 @@
* db.dropIndex('idx_merge_Job_url');
* ```
*/
dropIndex(indexName: string): void {
async dropIndex(indexName: string): Promise<void> {
this.db.prepare(`DROP INDEX IF EXISTS ${indexName}`).run();
}
Comment on lines +1040 to 1042
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

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

Copilot uses AI. Check for mistakes.
}
10 changes: 5 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
* ```typescript
* import { GraphDatabase } from 'sqlite-graph';
*
* const db = new GraphDatabase('./graph.db');
* const db = await GraphDatabase.create('./graph.db');
*
* const job = db.createNode('Job', { title: 'Engineer', status: 'active' });
* const company = db.createNode('Company', { name: 'TechCorp' });
* db.createEdge(job.id, 'POSTED_BY', company.id);
* const job = await db.createNode('Job', { title: 'Engineer', status: 'active' });
* const company = await db.createNode('Company', { name: 'TechCorp' });
* await db.createEdge(job.id, 'POSTED_BY', company.id);
*
* const activeJobs = db.nodes('Job')
* const activeJobs = await db.nodes('Job')
* .where({ status: 'active' })
* .exec();
* ```
Expand Down
12 changes: 6 additions & 6 deletions src/query/NodeQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ export class NodeQuery {
* console.log(`Found ${results.length} active jobs`);
* ```
*/
exec(): Node[] {
async exec(): Promise<Node[]> {
const sql = this.buildSQL();
const params = this.buildParams();

Expand Down Expand Up @@ -277,10 +277,10 @@ export class NodeQuery {
* }
* ```
*/
first(): Node | null {
async first(): Promise<Node | null> {
const original = this.limitValue;
this.limitValue = 1;
const results = this.exec();
const results = await this.exec();
this.limitValue = original;
return results.length > 0 ? results[0] : null;
Comment on lines +283 to 285
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

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

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

Copilot uses AI. Check for mistakes.
}
Expand All @@ -299,7 +299,7 @@ export class NodeQuery {
* console.log(`${count} active jobs`);
* ```
*/
count(): number {
async count(): Promise<number> {
const sql = this.buildSQL(true);
const params = this.buildParams();

Expand All @@ -324,8 +324,8 @@ export class NodeQuery {
* }
* ```
*/
exists(): boolean {
return this.count() > 0;
async exists(): Promise<boolean> {
return (await this.count()) > 0;
}

/**
Expand Down
Loading
Loading