import { UserContext } from '../src/core/context'; import Database from 'better-sqlite3'; import { MutationQuery, OrderBy, SelectQuery } from '../src/core/ast'; import { ExpoSQLiteBindValue, ExpoSQLiteDatabaseLike, ExpoSQLiteExecuteResult, ExpoSQLiteStatement, ExpoSQLiteTeaQLClient, } from '../src/sql/expo-sqlite'; import { EntitySchema } from '../src/sql/core'; class TestExpoResult implements ExpoSQLiteExecuteResult { private index = 0; constructor( private readonly rows: T[], public readonly changes: number, ) {} async getAllAsync(): Promise { return [...this.rows]; } [Symbol.asyncIterator](): AsyncIterableIterator { return this; } async next(): Promise> { if (this.index >= this.rows.length) return { done: true, value: undefined }; return { done: false, value: this.rows[this.index++] }; } } class TestExpoStatement implements ExpoSQLiteStatement { constructor(private readonly statement: Database.Statement) {} async executeAsync>( values: ExpoSQLiteBindValue[], ): Promise> { if (this.statement.reader) { return new TestExpoResult(this.statement.all(...values) as T[], 0); } const result = this.statement.run(...values); return new TestExpoResult([], result.changes); } async finalizeAsync(): Promise { // better-sqlite3 finalizes statements with their owning connection. } } class TestExpoDatabase implements ExpoSQLiteDatabaseLike { private readonly database = new Database(':memory:'); public closed = false; async execAsync(sql: string): Promise { this.database.exec(sql); } async prepareAsync(sql: string): Promise { return new TestExpoStatement(this.database.prepare(sql)); } async withExclusiveTransactionAsync( work: (transaction: ExpoSQLiteDatabaseLike) => Promise, ): Promise { this.database.exec('BEGIN IMMEDIATE'); try { await work(this); this.database.exec('COMMIT'); } catch (error) { this.database.exec('ROLLBACK'); throw error; } } async closeAsync(): Promise { this.database.close(); this.closed = true; } } const schemas: Record = { Order: { table: 'orders', columns: { id: { columnName: 'id', logicalType: 'integer', decode: 'string' }, version: { columnName: 'version', logicalType: 'integer', decode: 'number' }, name: { columnName: 'name', logicalType: 'text', decode: 'native' }, active: { columnName: 'active', logicalType: 'boolean', decode: 'native' }, }, }, }; describe('Expo SQLite TeaQL driver', () => { it('ensures schema, persists governed mutations, and queries real SQLite', async () => { const database = new TestExpoDatabase(); const client = new ExpoSQLiteTeaQLClient(database, schemas); await new UserContext().insertResource('dataService', client).ensureSchema(); await expect(client.executeMutation( new MutationQuery('Order', 'Create', { name: 'unsafe' }), )).rejects.toMatchObject({ code: 'REQUEST_COMMENT_REQUIRED', field: 'comment', requestKind: 'mutation' }); const created = await client.executeMutation( new MutationQuery( 'Order', 'Create', { name: 'first', active: true }, undefined, 'create local order', ), ); expect(created).toMatchObject({ success: true, metadata: { statements: [{ operation: 'insert', affectedRows: 1 }, { operation: 'select', resultCount: 1 }] }, id: '1', version: 1, persistedRecord: { id: '1', version: 1, name: 'first', active: true }, }); const rows = await client.executeQuery( new SelectQuery('Order') .comment('render local orders') .purpose('browse order history') .order(OrderBy.asc('id')), ); expect(rows).toEqual([{ id: '1', version: 1, name: 'first', active: true }]); expect(client.auditTrace[0].reason).toBe('create local order'); await client.close(); expect(database.closed).toBe(true); }); it('uses optimistic versions, hard limits, and true async cursor streaming', async () => { const database = new TestExpoDatabase(); const client = new ExpoSQLiteTeaQLClient(database, schemas); await new UserContext().insertResource('dataService', client).ensureSchema(); for (let index = 1; index <= 5; index++) { await client.executeMutation(new MutationQuery( 'Order', 'Create', { name: `order-${index}`, active: true }, String(index), 'seed local order', )); } await expect(client.executeMutation({ entity: 'Order', action: 'Update', id: '1', version: 99, payload: { name: 'stale' }, comment: 'reject stale update', })).rejects.toThrow(/Optimistic lock/); expect(() => new SelectQuery('Order').limit(10_001).prepareForList()) .toThrow(/QUERY_HARD_LIMIT_EXCEEDED/); const chunks: any[][] = []; const query = new SelectQuery('Order') .comment('stream local orders') .purpose('render a long local list') .order(OrderBy.asc('id')); for await (const chunk of client.executeForStream(query, 2)) chunks.push(chunk); expect(chunks.map(chunk => chunk.length)).toEqual([2, 2, 1]); expect(chunks.flat().map(row => row.id)).toEqual(['1', '2', '3', '4', '5']); await client.close(); }); });