Skip to content

Commit 9266a0a

Browse files
authored
Merge pull request #959 from objectstack-ai/copilot/fix-driverinterface-iadatadriver-split
2 parents 60fe962 + edddf61 commit 9266a0a

File tree

11 files changed

+366
-154
lines changed

11 files changed

+366
-154
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
- **Unified Data Driver Contract (`IDataDriver`)** — Resolved the split between `DriverInterface`
12+
(core, minimal ~13 methods) and `IDataDriver` (spec, comprehensive 28 methods). `IDataDriver`
13+
from `@objectstack/spec/contracts` is now the **single authoritative** contract for all database
14+
driver implementations. `DriverInterface` is retained as a deprecated type alias for backward
15+
compatibility. Both `@objectstack/driver-sql` and `@objectstack/driver-memory` now implement
16+
`IDataDriver` directly with full `DriverCapabilities` support.
17+
- **`@objectstack/driver-sql`** — Added missing `IDataDriver` methods: `findStream`, `upsert`,
18+
`bulkUpdate`, `bulkDelete`, `commit`, `rollback`, `dropTable`, `explain`. Aligned `supports`
19+
with full `DriverCapabilities` schema. `updateMany`/`deleteMany` now return `number` (count)
20+
instead of `{ modifiedCount }` / `{ deletedCount }` objects. `delete` returns `boolean`.
21+
- **`@objectstack/driver-memory`** — Aligned `supports` property with full `DriverCapabilities`
22+
schema (added `create`, `read`, `update`, `delete`, `bulkCreate`, `bulkUpdate`, `bulkDelete`,
23+
`savepoints`, `queryCTE`, `jsonQuery`, `geospatialQuery`, `streaming`, `schemaSync`, etc.).
24+
25+
### Deprecated
26+
- **`DriverInterface`** — Use `IDataDriver` from `@objectstack/spec/contracts` instead.
27+
`DriverInterface` remains as a type alias in both `@objectstack/spec/contracts` and
28+
`@objectstack/core` for backward compatibility.
29+
1030
### Added
1131
- **`@objectstack/driver-sql` plugin** — Migrated the Knex-based SQL driver from `@objectql/driver-sql`
1232
into `packages/plugins/driver-sql/`. The driver implements the standard `DriverInterface` from

packages/core/src/contracts/data-engine.ts

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ import {
77
DataEngineDeleteOptions,
88
DataEngineAggregateOptions,
99
DataEngineCountOptions,
10-
DataEngineRequest, // Added Request type for batch
11-
QueryAST,
12-
DriverOptions
10+
DataEngineRequest,
1311
} from '@objectstack/spec/data';
1412

1513
/**
@@ -47,30 +45,9 @@ export interface IDataEngine {
4745
execute?(command: any, options?: Record<string, any>): Promise<any>;
4846
}
4947

50-
export interface DriverInterface {
51-
name: string;
52-
version: string;
53-
connect(): Promise<void>;
54-
disconnect(): Promise<void>;
55-
56-
find(object: string, query: QueryAST, options?: DriverOptions): Promise<any[]>;
57-
findOne(object: string, query: QueryAST, options?: DriverOptions): Promise<any>;
58-
create(object: string, data: any, options?: DriverOptions): Promise<any>;
59-
update(object: string, id: any, data: any, options?: DriverOptions): Promise<any>;
60-
delete(object: string, id: any, options?: DriverOptions): Promise<any>;
61-
62-
/**
63-
* Bulk & Batch Operations
64-
*/
65-
bulkCreate?(object: string, data: any[], options?: DriverOptions): Promise<any>;
66-
updateMany?(object: string, query: QueryAST, data: any, options?: DriverOptions): Promise<any>;
67-
deleteMany?(object: string, query: QueryAST, options?: DriverOptions): Promise<any>;
68-
69-
count?(object: string, query: QueryAST, options?: DriverOptions): Promise<number>;
70-
71-
/**
72-
* Raw Execution
73-
*/
74-
execute?(command: any, params?: any, options?: DriverOptions): Promise<any>;
75-
}
48+
/**
49+
* @deprecated Use `IDataDriver` from `@objectstack/spec/contracts` instead.
50+
* This type is re-exported from `@objectstack/spec/contracts` for backward compatibility only.
51+
*/
52+
export type { DriverInterface } from '@objectstack/spec/contracts';
7653

packages/core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,7 @@ export type {
4444
RouteHandler,
4545
Middleware,
4646
IDataEngine,
47+
IDataDriver,
48+
/** @deprecated Use `IDataDriver` instead */
4749
DriverInterface
4850
} from '@objectstack/spec/contracts';

packages/objectql/src/engine.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
22
import { ObjectQL } from './engine';
33
import { SchemaRegistry } from './registry';
4-
import { DriverInterface } from '@objectstack/spec/data';
4+
import type { IDataDriver } from '@objectstack/spec/contracts';
55

66
// Mock the SchemaRegistry to avoid side effects between tests
77
vi.mock('./registry', () => {
@@ -35,8 +35,8 @@ vi.mock('./registry', () => {
3535

3636
describe('ObjectQL Engine', () => {
3737
let engine: ObjectQL;
38-
let mockDriver: DriverInterface;
39-
let mockDriver2: DriverInterface;
38+
let mockDriver: IDataDriver;
39+
let mockDriver2: IDataDriver;
4040

4141
beforeEach(() => {
4242
// Clear Registry Mocks
@@ -54,7 +54,7 @@ describe('ObjectQL Engine', () => {
5454
delete: vi.fn(),
5555
count: vi.fn(),
5656
capabilities: {} as any // Simplified
57-
} as unknown as DriverInterface;
57+
} as unknown as IDataDriver;
5858

5959
mockDriver2 = {
6060
name: 'mongo',
@@ -67,7 +67,7 @@ describe('ObjectQL Engine', () => {
6767
delete: vi.fn(),
6868
count: vi.fn(),
6969
capabilities: {} as any
70-
} as unknown as DriverInterface;
70+
} as unknown as IDataDriver;
7171

7272
engine = new ObjectQL();
7373
});

packages/objectql/src/engine.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -898,7 +898,7 @@ export class ObjectQL implements IDataEngine {
898898
result = await Promise.all((hookContext.input.data as any[]).map((item: any) => driver.create(object, item, hookContext.input.options as any)));
899899
}
900900
} else {
901-
result = await driver.create(object, hookContext.input.data, hookContext.input.options as any);
901+
result = await driver.create(object, hookContext.input.data as Record<string, unknown>, hookContext.input.options as any);
902902
}
903903

904904
hookContext.event = 'afterInsert';
@@ -949,10 +949,10 @@ export class ObjectQL implements IDataEngine {
949949
try {
950950
let result;
951951
if (hookContext.input.id) {
952-
result = await driver.update(object, hookContext.input.id as string, hookContext.input.data, hookContext.input.options as any);
952+
result = await driver.update(object, hookContext.input.id as string, hookContext.input.data as Record<string, unknown>, hookContext.input.options as any);
953953
} else if (options?.multi && driver.updateMany) {
954954
const ast = this.toQueryAST(object, { filter: options.filter });
955-
result = await driver.updateMany(object, ast, hookContext.input.data, hookContext.input.options as any);
955+
result = await driver.updateMany(object, ast, hookContext.input.data as Record<string, unknown>, hookContext.input.options as any);
956956
} else {
957957
throw new Error('Update requires an ID or options.multi=true');
958958
}

packages/plugins/driver-memory/src/memory-driver.ts

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import type { QueryAST, QueryInput, DriverOptions } from '@objectstack/spec/data';
4-
import type { DriverInterface } from '@objectstack/core';
4+
import type { IDataDriver } from '@objectstack/spec/contracts';
55
import { Logger, createLogger } from '@objectstack/core';
66
import { Query, Aggregator } from 'mingo';
77
import { getValueByPath } from './memory-matcher.js';
@@ -79,10 +79,10 @@ interface MemoryTransaction {
7979
*
8080
* Reference: objectql/packages/drivers/memory
8181
*/
82-
export class InMemoryDriver implements DriverInterface {
83-
name = 'com.objectstack.driver.memory';
82+
export class InMemoryDriver implements IDataDriver {
83+
readonly name = 'com.objectstack.driver.memory';
8484
type = 'driver';
85-
version = '1.0.0';
85+
readonly version = '1.0.0';
8686
private config: InMemoryDriverConfig;
8787
private logger: Logger;
8888
private idCounters: Map<string, number> = new Map();
@@ -106,9 +106,21 @@ export class InMemoryDriver implements DriverInterface {
106106
}
107107
}
108108

109-
supports = {
109+
readonly supports = {
110+
// Basic CRUD Operations
111+
create: true,
112+
read: true,
113+
update: true,
114+
delete: true,
115+
116+
// Bulk Operations
117+
bulkCreate: true,
118+
bulkUpdate: true,
119+
bulkDelete: true,
120+
110121
// Transaction & Connection Management
111122
transactions: true, // Snapshot-based transactions
123+
savepoints: false,
112124

113125
// Query Operations
114126
queryFilters: true, // Implemented via memory-matcher
@@ -117,14 +129,27 @@ export class InMemoryDriver implements DriverInterface {
117129
queryPagination: true, // Implemented
118130
queryWindowFunctions: false, // @planned: Window functions (ROW_NUMBER, RANK, etc.)
119131
querySubqueries: false, // @planned: Subquery execution
132+
queryCTE: false,
120133
joins: false, // @planned: In-memory join operations
121134

122135
// Advanced Features
123136
fullTextSearch: false, // @planned: Text tokenization + matching
124-
vectorSearch: false, // @planned: Cosine similarity search
125-
geoSpatial: false, // @planned: Distance/within calculations
137+
jsonQuery: false,
138+
geospatialQuery: false,
139+
streaming: true, // Implemented via findStream()
126140
jsonFields: true, // Native JS object support
127141
arrayFields: true, // Native JS array support
142+
vectorSearch: false, // @planned: Cosine similarity search
143+
144+
// Schema Management
145+
schemaSync: true, // Implemented via syncSchema()
146+
migrations: false,
147+
indexes: false,
148+
149+
// Performance & Optimization
150+
connectionPooling: false,
151+
preparedStatements: false,
152+
queryCache: false,
128153
};
129154

130155
/**
@@ -230,7 +255,7 @@ export class InMemoryDriver implements DriverInterface {
230255
// CRUD
231256
// ===================================
232257

233-
async find(object: string, query: QueryInput, options?: DriverOptions) {
258+
async find(object: string, query: QueryAST, options?: DriverOptions) {
234259
this.logger.debug('Find operation', { object, query });
235260

236261
const table = this.getTable(object);
@@ -275,7 +300,7 @@ export class InMemoryDriver implements DriverInterface {
275300
return results;
276301
}
277302

278-
async *findStream(object: string, query: QueryInput, options?: DriverOptions) {
303+
async *findStream(object: string, query: QueryAST, options?: DriverOptions) {
279304
this.logger.debug('FindStream operation', { object });
280305

281306
const results = await this.find(object, query, options);
@@ -284,7 +309,7 @@ export class InMemoryDriver implements DriverInterface {
284309
}
285310
}
286311

287-
async findOne(object: string, query: QueryInput, options?: DriverOptions) {
312+
async findOne(object: string, query: QueryAST, options?: DriverOptions) {
288313
this.logger.debug('FindOne operation', { object, query });
289314

290315
const results = await this.find(object, { ...query, limit: 1 }, options);
@@ -381,7 +406,7 @@ export class InMemoryDriver implements DriverInterface {
381406
return true;
382407
}
383408

384-
async count(object: string, query?: QueryInput, options?: DriverOptions) {
409+
async count(object: string, query?: QueryAST, options?: DriverOptions) {
385410
let records = this.getTable(object);
386411
if (query?.where) {
387412
const mongoQuery = this.convertToMongoQuery(query.where);
@@ -406,7 +431,7 @@ export class InMemoryDriver implements DriverInterface {
406431
return results;
407432
}
408433

409-
async updateMany(object: string, query: QueryInput, data: Record<string, any>, options?: DriverOptions) {
434+
async updateMany(object: string, query: QueryAST, data: Record<string, any>, options?: DriverOptions): Promise<number> {
410435
this.logger.debug('UpdateMany operation', { object, query });
411436

412437
const table = this.getTable(object);
@@ -436,10 +461,10 @@ export class InMemoryDriver implements DriverInterface {
436461

437462
if (count > 0) this.markDirty();
438463
this.logger.debug('UpdateMany completed', { object, count });
439-
return { count };
464+
return count;
440465
}
441466

442-
async deleteMany(object: string, query: QueryInput, options?: DriverOptions) {
467+
async deleteMany(object: string, query: QueryAST, options?: DriverOptions): Promise<number> {
443468
this.logger.debug('DeleteMany operation', { object, query });
444469

445470
const table = this.getTable(object);
@@ -464,7 +489,7 @@ export class InMemoryDriver implements DriverInterface {
464489
const count = initialLength - this.db[object].length;
465490
if (count > 0) this.markDirty();
466491
this.logger.debug('DeleteMany completed', { object, count });
467-
return { count };
492+
return count;
468493
}
469494

470495
// Compatibility aliases

packages/plugins/driver-sql/src/sql-driver-advanced.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ describe('SqlDriver Advanced Operations (SQLite)', () => {
146146
it('should update many records', async () => {
147147
const result = await driver.updateMany('orders', { status: 'pending' } as any, { status: 'processing' });
148148

149-
expect(result.modifiedCount).toBeGreaterThan(0);
149+
expect(result).toBeGreaterThan(0);
150150

151151
const results = await driver.find('orders', { where: { status: 'processing' } });
152152
expect(results.length).toBe(1);
@@ -155,18 +155,18 @@ describe('SqlDriver Advanced Operations (SQLite)', () => {
155155
it('should delete many records', async () => {
156156
const result = await driver.deleteMany('orders', { status: 'cancelled' } as any);
157157

158-
expect(result.deletedCount).toBe(1);
158+
expect(result).toBe(1);
159159

160160
const remaining = await driver.count('orders', {});
161161
expect(remaining).toBe(4);
162162
});
163163

164164
it('should handle empty bulk update and delete', async () => {
165165
const result = await driver.updateMany('orders', { status: 'nonexistent' } as any, { status: 'updated' });
166-
expect(result.modifiedCount).toBe(0);
166+
expect(result).toBe(0);
167167

168168
const deleteResult = await driver.deleteMany('orders', { id: 'nonexistent' } as any);
169-
expect(deleteResult.deletedCount).toBe(0);
169+
expect(deleteResult).toBe(0);
170170
});
171171
});
172172

0 commit comments

Comments
 (0)