Skip to content
Draft
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 @@ export class GraphDatabase {
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;

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,7 +208,7 @@ export class GraphDatabase {
* 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);

Expand Down Expand Up @@ -204,7 +240,7 @@ export class GraphDatabase {
* }
* ```
*/
getNode(id: number): Node | null {
async getNode(id: number): Promise<Node | null> {
validateNodeId(id);

const stmt = this.preparedStatements.get('getNode')!;
Expand Down Expand Up @@ -239,10 +275,10 @@ export class GraphDatabase {
* });
* ```
*/
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`);
}
Expand Down Expand Up @@ -274,7 +310,7 @@ export class GraphDatabase {
* 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 @@ export class GraphDatabase {
* });
* ```
*/
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 Down Expand Up @@ -358,7 +394,7 @@ export class GraphDatabase {
* }
* ```
*/
getEdge(id: number): Edge | null {
async getEdge(id: number): Promise<Edge | null> {
validateNodeId(id);

const stmt = this.preparedStatements.get('getEdge')!;
Expand Down Expand Up @@ -387,7 +423,7 @@ export class GraphDatabase {
* 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 @@ export class GraphDatabase {
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 @@ export class GraphDatabase {
* .exec();
* ```
*/
pattern<T extends Record<string, unknown> = Record<string, unknown>>(): PatternQuery<T> {
pattern<T extends Record<string, GraphEntity> = Record<string, GraphEntity>>(): PatternQuery<T> {
return new PatternQuery<T>(this.db);
}

Expand Down Expand Up @@ -505,14 +541,14 @@ export class GraphDatabase {
* });
* ```
*/
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,7 +576,7 @@ export class GraphDatabase {
* 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');

Expand Down Expand Up @@ -585,14 +621,14 @@ export class GraphDatabase {
* 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 @@ export class GraphDatabase {
* db.close();
* ```
*/
close(): void {
async close(): Promise<void> {
this.db.close();
}

Expand Down Expand Up @@ -654,12 +690,12 @@ export class GraphDatabase {
* );
* ```
*/
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 @@ export class GraphDatabase {
}
}

return this.transaction(() => {
// Build SQL to find matching node
return await this.transaction(() => {
const whereConditions = matchKeys.map(
(key) => `json_extract(properties, '$.${key}') = ?`
);
Expand Down Expand Up @@ -781,20 +816,20 @@ export class GraphDatabase {
* );
* ```
*/
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 @@ export class GraphDatabase {
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 @@ export class GraphDatabase {
* 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 @@ export class GraphDatabase {
* });
* ```
*/
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 @@ export class GraphDatabase {
* 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();
}
}
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;
}
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