forked from akai/readest
feat(database): support turso fts in tauri apps (#3484)
This commit is contained in:
+3
-3
@@ -10,6 +10,6 @@
|
||||
[submodule "packages/simplecc-wasm"]
|
||||
path = packages/simplecc-wasm
|
||||
url = https://github.com/readest/simplecc-wasm.git
|
||||
[submodule "apps/readest-app/src-tauri/plugins/tauri-plugin-libsql"]
|
||||
path = apps/readest-app/src-tauri/plugins/tauri-plugin-libsql
|
||||
url = https://github.com/readest/tauri-plugin-libsql.git
|
||||
[submodule "apps/readest-app/src-tauri/plugins/tauri-plugin-turso"]
|
||||
path = apps/readest-app/src-tauri/plugins/tauri-plugin-turso
|
||||
url = https://github.com/readest/tauri-plugin-turso.git
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ packages
|
||||
# Vendored Assets (Generated/External Code)
|
||||
apps/readest-app/public/*.js
|
||||
apps/readest-app/public/vendor
|
||||
apps/readest-app/src-tauri/plugins/tauri-plugin-libsql
|
||||
apps/readest-app/src-tauri/plugins/tauri-plugin-turso
|
||||
|
||||
# Environment & Editor
|
||||
.env
|
||||
|
||||
Generated
+1171
-727
File diff suppressed because it is too large
Load Diff
@@ -63,7 +63,7 @@ Vitest connects directly to the embedded WebDriver server (port 4445) in the run
|
||||
- **Config:** `vitest.tauri.config.mts`
|
||||
- **Pattern:** `src/**/*.tauri.test.ts`
|
||||
- **Browser provider:** `@vitest/browser-webdriverio` (connects to port 4445)
|
||||
- **Use for:** Tauri plugin commands (libsql, native-tts, etc.), native filesystem, Tauri IPC.
|
||||
- **Use for:** Tauri plugin commands (turso, native-tts, etc.), native filesystem, Tauri IPC.
|
||||
|
||||
### Writing Tauri Tests
|
||||
|
||||
@@ -73,7 +73,7 @@ Tests access Tauri IPC via a shared helper:
|
||||
import { invoke } from '../tauri/tauri-invoke';
|
||||
|
||||
it('calls a plugin command', async () => {
|
||||
const result = await invoke('plugin:libsql|load', { options: { path: 'sqlite::memory:' } });
|
||||
const result = await invoke('plugin:turso|load', { options: { path: 'sqlite::memory:' } });
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
```
|
||||
|
||||
@@ -58,7 +58,7 @@ tauri-plugin-native-tts = { path = "./plugins/tauri-plugin-native-tts" }
|
||||
tauri-plugin-websocket = "2"
|
||||
tauri-plugin-sharekit = "0.3"
|
||||
tauri-plugin-device-info = "1.0.1"
|
||||
tauri-plugin-libsql = { path = "./plugins/tauri-plugin-libsql" }
|
||||
tauri-plugin-turso = { path = "./plugins/tauri-plugin-turso" }
|
||||
tauri-plugin-webdriver = { version = "0.2", optional = true }
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"log:default",
|
||||
"shell:default",
|
||||
"process:default",
|
||||
"libsql:default",
|
||||
"turso:default",
|
||||
"native-tts:default",
|
||||
"native-bridge:default"
|
||||
]
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
"deep-link:default",
|
||||
"sharekit:default",
|
||||
"device-info:default",
|
||||
"libsql:default",
|
||||
"turso:default",
|
||||
"native-tts:default",
|
||||
"native-bridge:default"
|
||||
]
|
||||
|
||||
Submodule apps/readest-app/src-tauri/plugins/tauri-plugin-libsql deleted from 4fc646e66b
Submodule apps/readest-app/src-tauri/plugins/tauri-plugin-turso added at bb12a796eb
@@ -185,7 +185,7 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_sharekit::init())
|
||||
.plugin(tauri_plugin_device_info::init())
|
||||
.plugin(tauri_plugin_libsql::init())
|
||||
.plugin(tauri_plugin_turso::init())
|
||||
.plugin(tauri_plugin_native_bridge::init())
|
||||
.plugin(tauri_plugin_native_tts::init());
|
||||
|
||||
|
||||
@@ -1,366 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { invoke } from '../tauri/tauri-invoke';
|
||||
|
||||
/**
|
||||
* Integration tests for tauri-plugin-libsql running inside the Tauri WebView.
|
||||
* These call plugin IPC commands directly via __TAURI_INTERNALS__.invoke().
|
||||
*/
|
||||
describe('libsql plugin (native Tauri)', () => {
|
||||
const DB_PATH = 'sqlite::memory:';
|
||||
let dbPath: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
dbPath = (await invoke('plugin:libsql|load', {
|
||||
options: { path: DB_PATH },
|
||||
})) as string;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await invoke('plugin:libsql|close', { db: dbPath });
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Connection lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('loads an in-memory database and returns a path', () => {
|
||||
expect(typeof dbPath).toBe('string');
|
||||
expect(dbPath.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schema & basic operations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('creates a table and inserts a row', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)',
|
||||
values: [],
|
||||
});
|
||||
|
||||
const result = (await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['apple'],
|
||||
})) as { rowsAffected: number; lastInsertId: number };
|
||||
|
||||
expect(result.rowsAffected).toBe(1);
|
||||
expect(result.lastInsertId).toBe(1);
|
||||
});
|
||||
|
||||
it('inserts multiple rows with auto-incrementing ids', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)',
|
||||
values: [],
|
||||
});
|
||||
|
||||
const r1 = (await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['a'],
|
||||
})) as { lastInsertId: number };
|
||||
|
||||
const r2 = (await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['b'],
|
||||
})) as { lastInsertId: number };
|
||||
|
||||
const r3 = (await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['c'],
|
||||
})) as { lastInsertId: number };
|
||||
|
||||
expect(r1.lastInsertId).toBe(1);
|
||||
expect(r2.lastInsertId).toBe(2);
|
||||
expect(r3.lastInsertId).toBe(3);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SELECT queries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('select returns typed rows', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT, qty INTEGER)',
|
||||
values: [],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name, qty) VALUES (?, ?)',
|
||||
values: ['apple', 10],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name, qty) VALUES (?, ?)',
|
||||
values: ['banana', 20],
|
||||
});
|
||||
|
||||
const rows = (await invoke('plugin:libsql|select', {
|
||||
db: dbPath,
|
||||
query: 'SELECT * FROM items ORDER BY id',
|
||||
values: [],
|
||||
})) as Array<{ id: number; name: string; qty: number }>;
|
||||
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows[0]!.name).toBe('apple');
|
||||
expect(rows[0]!.qty).toBe(10);
|
||||
expect(rows[1]!.name).toBe('banana');
|
||||
expect(rows[1]!.qty).toBe(20);
|
||||
});
|
||||
|
||||
it('select with WHERE and params', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)',
|
||||
values: [],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['apple'],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['banana'],
|
||||
});
|
||||
|
||||
const rows = (await invoke('plugin:libsql|select', {
|
||||
db: dbPath,
|
||||
query: 'SELECT * FROM items WHERE name = ?',
|
||||
values: ['banana'],
|
||||
})) as Array<{ id: number; name: string }>;
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]!.name).toBe('banana');
|
||||
});
|
||||
|
||||
it('select returns empty array for no matching rows', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)',
|
||||
values: [],
|
||||
});
|
||||
|
||||
const rows = (await invoke('plugin:libsql|select', {
|
||||
db: dbPath,
|
||||
query: 'SELECT * FROM items',
|
||||
values: [],
|
||||
})) as unknown[];
|
||||
|
||||
expect(rows).toEqual([]);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UPDATE & DELETE
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('UPDATE returns rowsAffected', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)',
|
||||
values: [],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['old'],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['old'],
|
||||
});
|
||||
|
||||
const result = (await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'UPDATE items SET name = ? WHERE name = ?',
|
||||
values: ['new', 'old'],
|
||||
})) as { rowsAffected: number };
|
||||
|
||||
expect(result.rowsAffected).toBe(2);
|
||||
});
|
||||
|
||||
it('DELETE returns rowsAffected', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)',
|
||||
values: [],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['a'],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['b'],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['c'],
|
||||
});
|
||||
|
||||
const result = (await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'DELETE FROM items WHERE name IN (?, ?)',
|
||||
values: ['a', 'c'],
|
||||
})) as { rowsAffected: number };
|
||||
|
||||
expect(result.rowsAffected).toBe(2);
|
||||
|
||||
const remaining = (await invoke('plugin:libsql|select', {
|
||||
db: dbPath,
|
||||
query: 'SELECT name FROM items',
|
||||
values: [],
|
||||
})) as Array<{ name: string }>;
|
||||
|
||||
expect(remaining).toHaveLength(1);
|
||||
expect(remaining[0]!.name).toBe('b');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Batch execution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('batch executes multiple statements atomically', async () => {
|
||||
await invoke('plugin:libsql|batch', {
|
||||
db: dbPath,
|
||||
queries: [
|
||||
'CREATE TABLE t1 (id INTEGER PRIMARY KEY, val TEXT)',
|
||||
"INSERT INTO t1 (val) VALUES ('one')",
|
||||
"INSERT INTO t1 (val) VALUES ('two')",
|
||||
"INSERT INTO t1 (val) VALUES ('three')",
|
||||
],
|
||||
});
|
||||
|
||||
const rows = (await invoke('plugin:libsql|select', {
|
||||
db: dbPath,
|
||||
query: 'SELECT val FROM t1 ORDER BY id',
|
||||
values: [],
|
||||
})) as Array<{ val: string }>;
|
||||
|
||||
expect(rows).toHaveLength(3);
|
||||
expect(rows.map((r) => r.val)).toEqual(['one', 'two', 'three']);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('handles NULL values correctly', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)',
|
||||
values: [],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO t (val) VALUES (?)',
|
||||
values: [null],
|
||||
});
|
||||
|
||||
const rows = (await invoke('plugin:libsql|select', {
|
||||
db: dbPath,
|
||||
query: 'SELECT * FROM t',
|
||||
values: [],
|
||||
})) as Array<{ id: number; val: string | null }>;
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]!.val).toBeNull();
|
||||
});
|
||||
|
||||
it('handles integer and real types', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE nums (i INTEGER, r REAL)',
|
||||
values: [],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO nums (i, r) VALUES (?, ?)',
|
||||
values: [42, 3.14],
|
||||
});
|
||||
|
||||
const rows = (await invoke('plugin:libsql|select', {
|
||||
db: dbPath,
|
||||
query: 'SELECT * FROM nums',
|
||||
values: [],
|
||||
})) as Array<{ i: number; r: number }>;
|
||||
|
||||
expect(rows[0]!.i).toBe(42);
|
||||
expect(rows[0]!.r).toBeCloseTo(3.14);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Error handling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('throws on invalid SQL', async () => {
|
||||
await expect(
|
||||
invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INVALID SQL STATEMENT',
|
||||
values: [],
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('throws on constraint violation', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT UNIQUE)',
|
||||
values: [],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO t (val) VALUES (?)',
|
||||
values: ['unique_val'],
|
||||
});
|
||||
|
||||
await expect(
|
||||
invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO t (val) VALUES (?)',
|
||||
values: ['unique_val'],
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Result contract
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('execute result always has rowsAffected and lastInsertId as numbers', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE t (id INTEGER PRIMARY KEY)',
|
||||
values: [],
|
||||
});
|
||||
|
||||
const result = (await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO t DEFAULT VALUES',
|
||||
values: [],
|
||||
})) as { rowsAffected: number; lastInsertId: number };
|
||||
|
||||
expect(typeof result.rowsAffected).toBe('number');
|
||||
expect(typeof result.lastInsertId).toBe('number');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('get_config returns encryption status', async () => {
|
||||
const config = (await invoke('plugin:libsql|get_config')) as { encrypted: boolean };
|
||||
expect(typeof config.encrypted).toBe('boolean');
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import { DatabaseService, DatabaseExecResult, DatabaseRow } from '@/types/databa
|
||||
// Mock: NativeDatabaseService
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
vi.mock('tauri-plugin-libsql', () => {
|
||||
vi.mock('tauri-plugin-turso', () => {
|
||||
const rows = new Map<string, DatabaseRow[]>();
|
||||
|
||||
const mockDb = {
|
||||
@@ -99,7 +99,7 @@ describe('NativeDatabaseService', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
const mod = await import('tauri-plugin-libsql');
|
||||
const mod = await import('tauri-plugin-turso');
|
||||
(mod as unknown as { __rows: Map<string, DatabaseRow[]> }).__rows.clear();
|
||||
|
||||
const { NativeDatabaseService } = await import('@/services/database/nativeDatabaseService');
|
||||
@@ -139,7 +139,7 @@ describe('NativeDatabaseService', () => {
|
||||
|
||||
it('batch() delegates to underlying db.batch()', async () => {
|
||||
await db.batch(['CREATE TABLE t (id INTEGER)', 'INSERT INTO t VALUES (1)']);
|
||||
const mod = await import('tauri-plugin-libsql');
|
||||
const mod = await import('tauri-plugin-turso');
|
||||
const mockDb = (mod as unknown as { __mockDb: { batch: ReturnType<typeof vi.fn> } }).__mockDb;
|
||||
expect(mockDb.batch).toHaveBeenCalledWith([
|
||||
'CREATE TABLE t (id INTEGER)',
|
||||
@@ -149,7 +149,7 @@ describe('NativeDatabaseService', () => {
|
||||
|
||||
it('close() delegates to underlying db.close()', async () => {
|
||||
await db.close();
|
||||
const mod = await import('tauri-plugin-libsql');
|
||||
const mod = await import('tauri-plugin-turso');
|
||||
const mockDb = (mod as unknown as { __mockDb: { close: ReturnType<typeof vi.fn> } }).__mockDb;
|
||||
expect(mockDb.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { it, expect } from 'vitest';
|
||||
import { DatabaseService } from '@/types/database';
|
||||
|
||||
/**
|
||||
* Shared base database operation tests exercised against any DatabaseService.
|
||||
* Covers CRUD, batch, data types, error handling, and result contract.
|
||||
*
|
||||
* Call inside a describe() block:
|
||||
* describe('base ops', () => { baseTests(() => db); });
|
||||
*/
|
||||
export function baseTests(getDb: () => DatabaseService) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schema & basic operations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('creates a table and inserts a row', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
const result = await db.execute('INSERT INTO items (name) VALUES (?)', ['apple']);
|
||||
expect(result.rowsAffected).toBe(1);
|
||||
expect(result.lastInsertId).toBe(1);
|
||||
});
|
||||
|
||||
it('inserts multiple rows with auto-incrementing ids', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
const r1 = await db.execute('INSERT INTO items (name) VALUES (?)', ['a']);
|
||||
const r2 = await db.execute('INSERT INTO items (name) VALUES (?)', ['b']);
|
||||
const r3 = await db.execute('INSERT INTO items (name) VALUES (?)', ['c']);
|
||||
expect(r1.lastInsertId).toBe(1);
|
||||
expect(r2.lastInsertId).toBe(2);
|
||||
expect(r3.lastInsertId).toBe(3);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SELECT queries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('select returns typed rows', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT, qty INTEGER)');
|
||||
await db.execute('INSERT INTO items (name, qty) VALUES (?, ?)', ['apple', 10]);
|
||||
await db.execute('INSERT INTO items (name, qty) VALUES (?, ?)', ['banana', 20]);
|
||||
|
||||
const rows = await db.select<{ id: number; name: string; qty: number }>(
|
||||
'SELECT * FROM items ORDER BY id',
|
||||
);
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows[0]!.name).toBe('apple');
|
||||
expect(rows[0]!.qty).toBe(10);
|
||||
expect(rows[1]!.name).toBe('banana');
|
||||
expect(rows[1]!.qty).toBe(20);
|
||||
});
|
||||
|
||||
it('select with WHERE and params', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['apple']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['banana']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['cherry']);
|
||||
|
||||
const rows = await db.select<{ id: number; name: string }>(
|
||||
'SELECT * FROM items WHERE name = ?',
|
||||
['banana'],
|
||||
);
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]!.name).toBe('banana');
|
||||
});
|
||||
|
||||
it('select returns empty array for no matching rows', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
const rows = await db.select('SELECT * FROM items');
|
||||
expect(rows).toEqual([]);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UPDATE & DELETE
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('UPDATE returns rowsAffected', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['old']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['old']);
|
||||
|
||||
const result = await db.execute('UPDATE items SET name = ? WHERE name = ?', ['new', 'old']);
|
||||
expect(result.rowsAffected).toBe(2);
|
||||
});
|
||||
|
||||
it('DELETE returns rowsAffected', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['a']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['b']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['c']);
|
||||
|
||||
const result = await db.execute('DELETE FROM items WHERE name IN (?, ?)', ['a', 'c']);
|
||||
expect(result.rowsAffected).toBe(2);
|
||||
|
||||
const remaining = await db.select<{ name: string }>('SELECT name FROM items');
|
||||
expect(remaining).toHaveLength(1);
|
||||
expect(remaining[0]!.name).toBe('b');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Batch execution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('batch executes multiple statements atomically', async () => {
|
||||
const db = getDb();
|
||||
await db.batch([
|
||||
'CREATE TABLE t1 (id INTEGER PRIMARY KEY, val TEXT)',
|
||||
"INSERT INTO t1 (val) VALUES ('one')",
|
||||
"INSERT INTO t1 (val) VALUES ('two')",
|
||||
"INSERT INTO t1 (val) VALUES ('three')",
|
||||
]);
|
||||
|
||||
const rows = await db.select<{ val: string }>('SELECT val FROM t1 ORDER BY id');
|
||||
expect(rows).toHaveLength(3);
|
||||
expect(rows.map((r) => r.val)).toEqual(['one', 'two', 'three']);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('handles NULL values correctly', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)');
|
||||
await db.execute('INSERT INTO t (val) VALUES (?)', [null]);
|
||||
|
||||
const rows = await db.select<{ id: number; val: string | null }>('SELECT * FROM t');
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]!.val).toBeNull();
|
||||
});
|
||||
|
||||
it('handles integer and real types', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE nums (i INTEGER, r REAL)');
|
||||
await db.execute('INSERT INTO nums (i, r) VALUES (?, ?)', [42, 3.14]);
|
||||
|
||||
const rows = await db.select<{ i: number; r: number }>('SELECT * FROM nums');
|
||||
expect(rows[0]!.i).toBe(42);
|
||||
expect(rows[0]!.r).toBeCloseTo(3.14);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Error handling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('throws on invalid SQL', async () => {
|
||||
const db = getDb();
|
||||
await expect(db.execute('INVALID SQL STATEMENT')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('throws on constraint violation', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT UNIQUE)');
|
||||
await db.execute('INSERT INTO t (val) VALUES (?)', ['unique_val']);
|
||||
await expect(db.execute('INSERT INTO t (val) VALUES (?)', ['unique_val'])).rejects.toThrow();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Result contract
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('execute result always has rowsAffected and lastInsertId as numbers', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE t (id INTEGER PRIMARY KEY)');
|
||||
const result = await db.execute('INSERT INTO t DEFAULT VALUES');
|
||||
expect(typeof result.rowsAffected).toBe('number');
|
||||
expect(typeof result.lastInsertId).toBe('number');
|
||||
});
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, beforeEach, afterEach } from 'vitest';
|
||||
import { NodeDatabaseService } from '@/services/database/nodeDatabaseService';
|
||||
import { DatabaseService, DatabaseExecResult } from '@/types/database';
|
||||
import { DatabaseService } from '@/types/database';
|
||||
import { baseTests } from './suites/base-tests';
|
||||
import { ftsTests } from './suites/fts-tests';
|
||||
import { vectorTests } from './suites/vector-tests';
|
||||
|
||||
/**
|
||||
* Integration tests using a real in-memory SQLite database via @tursodatabase/database.
|
||||
* These complement the mock-based tests in databaseService.test.ts by exercising
|
||||
* These complement the mock-based tests in mock.test.ts by exercising
|
||||
* actual SQL execution through the DatabaseService interface using the same
|
||||
* libsql engine that powers the browser-based @tursodatabase/database-wasm.
|
||||
* turso engine that powers the browser-based @tursodatabase/database-wasm.
|
||||
*/
|
||||
describe('NodeDatabaseService (real in-memory SQLite)', () => {
|
||||
let db: DatabaseService;
|
||||
@@ -21,171 +22,14 @@ describe('NodeDatabaseService (real in-memory SQLite)', () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Schema & basic operations
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('creates a table and inserts a row', async () => {
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
const result: DatabaseExecResult = await db.execute('INSERT INTO items (name) VALUES (?)', [
|
||||
'apple',
|
||||
]);
|
||||
expect(result.rowsAffected).toBe(1);
|
||||
expect(result.lastInsertId).toBe(1);
|
||||
describe('Base Operations', () => {
|
||||
baseTests(() => db);
|
||||
});
|
||||
|
||||
it('inserts multiple rows with auto-incrementing ids', async () => {
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
const r1 = await db.execute('INSERT INTO items (name) VALUES (?)', ['a']);
|
||||
const r2 = await db.execute('INSERT INTO items (name) VALUES (?)', ['b']);
|
||||
const r3 = await db.execute('INSERT INTO items (name) VALUES (?)', ['c']);
|
||||
expect(r1.lastInsertId).toBe(1);
|
||||
expect(r2.lastInsertId).toBe(2);
|
||||
expect(r3.lastInsertId).toBe(3);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SELECT queries
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('select() returns typed rows', async () => {
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT, qty INTEGER)');
|
||||
await db.execute('INSERT INTO items (name, qty) VALUES (?, ?)', ['apple', 10]);
|
||||
await db.execute('INSERT INTO items (name, qty) VALUES (?, ?)', ['banana', 20]);
|
||||
|
||||
const rows = await db.select<{ id: number; name: string; qty: number }>(
|
||||
'SELECT * FROM items ORDER BY id',
|
||||
);
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows[0]!.name).toBe('apple');
|
||||
expect(rows[0]!.qty).toBe(10);
|
||||
expect(rows[1]!.name).toBe('banana');
|
||||
expect(rows[1]!.qty).toBe(20);
|
||||
});
|
||||
|
||||
it('select() with WHERE and params', async () => {
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['apple']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['banana']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['cherry']);
|
||||
|
||||
const rows = await db.select<{ id: number; name: string }>(
|
||||
'SELECT * FROM items WHERE name = ?',
|
||||
['banana'],
|
||||
);
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]!.name).toBe('banana');
|
||||
});
|
||||
|
||||
it('select() returns empty array for no matching rows', async () => {
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
const rows = await db.select('SELECT * FROM items');
|
||||
expect(rows).toEqual([]);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// UPDATE & DELETE
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('execute() UPDATE returns rowsAffected', async () => {
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['old']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['old']);
|
||||
|
||||
const result = await db.execute('UPDATE items SET name = ? WHERE name = ?', ['new', 'old']);
|
||||
expect(result.rowsAffected).toBe(2);
|
||||
});
|
||||
|
||||
it('execute() DELETE returns rowsAffected', async () => {
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['a']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['b']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['c']);
|
||||
|
||||
const result = await db.execute('DELETE FROM items WHERE name IN (?, ?)', ['a', 'c']);
|
||||
expect(result.rowsAffected).toBe(2);
|
||||
|
||||
const remaining = await db.select<{ name: string }>('SELECT name FROM items');
|
||||
expect(remaining).toHaveLength(1);
|
||||
expect(remaining[0]!.name).toBe('b');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Batch execution
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('batch() executes multiple statements atomically', async () => {
|
||||
await db.batch([
|
||||
'CREATE TABLE t1 (id INTEGER PRIMARY KEY, val TEXT)',
|
||||
"INSERT INTO t1 (val) VALUES ('one')",
|
||||
"INSERT INTO t1 (val) VALUES ('two')",
|
||||
"INSERT INTO t1 (val) VALUES ('three')",
|
||||
]);
|
||||
|
||||
const rows = await db.select<{ val: string }>('SELECT val FROM t1 ORDER BY id');
|
||||
expect(rows).toHaveLength(3);
|
||||
expect(rows.map((r) => r.val)).toEqual(['one', 'two', 'three']);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Data types
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('handles NULL values correctly', async () => {
|
||||
await db.execute('CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)');
|
||||
await db.execute('INSERT INTO t (val) VALUES (?)', [null]);
|
||||
|
||||
const rows = await db.select<{ id: number; val: string | null }>('SELECT * FROM t');
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]!.val).toBeNull();
|
||||
});
|
||||
|
||||
it('handles integer and real types', async () => {
|
||||
await db.execute('CREATE TABLE nums (i INTEGER, r REAL)');
|
||||
await db.execute('INSERT INTO nums (i, r) VALUES (?, ?)', [42, 3.14]);
|
||||
|
||||
const rows = await db.select<{ i: number; r: number }>('SELECT * FROM nums');
|
||||
expect(rows[0]!.i).toBe(42);
|
||||
expect(rows[0]!.r).toBeCloseTo(3.14);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Error handling
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('throws on invalid SQL', async () => {
|
||||
await expect(db.execute('INVALID SQL STATEMENT')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('throws on constraint violation', async () => {
|
||||
await db.execute('CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT UNIQUE)');
|
||||
await db.execute('INSERT INTO t (val) VALUES (?)', ['unique_val']);
|
||||
await expect(db.execute('INSERT INTO t (val) VALUES (?)', ['unique_val'])).rejects.toThrow();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// DatabaseExecResult contract
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('execute() result always has rowsAffected and lastInsertId as numbers', async () => {
|
||||
await db.execute('CREATE TABLE t (id INTEGER PRIMARY KEY)');
|
||||
const result = await db.execute('INSERT INTO t DEFAULT VALUES');
|
||||
expect(typeof result.rowsAffected).toBe('number');
|
||||
expect(typeof result.lastInsertId).toBe('number');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Full-text search (Turso native FTS, Tantivy-based)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
describe('Full-Text Search', () => {
|
||||
ftsTests(() => db);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Vector search
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
describe('Vector Search', () => {
|
||||
vectorTests(() => db);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { invoke } from '../tauri/tauri-invoke';
|
||||
import { DatabaseService, DatabaseExecResult, DatabaseRow } from '@/types/database';
|
||||
import { baseTests } from './suites/base-tests';
|
||||
import { ftsTests } from './suites/fts-tests';
|
||||
import { vectorTests } from './suites/vector-tests';
|
||||
|
||||
/**
|
||||
* Thin DatabaseService adapter over raw Tauri IPC invoke() calls.
|
||||
* Enables reuse of the shared test suites.
|
||||
*/
|
||||
class TauriDatabaseAdapter implements DatabaseService {
|
||||
constructor(private dbPath: string) {}
|
||||
|
||||
async execute(sql: string, params: unknown[] = []): Promise<DatabaseExecResult> {
|
||||
const result = (await invoke('plugin:turso|execute', {
|
||||
db: this.dbPath,
|
||||
query: sql,
|
||||
values: params,
|
||||
})) as { rowsAffected: number; lastInsertId: number };
|
||||
return {
|
||||
rowsAffected: result.rowsAffected,
|
||||
lastInsertId: result.lastInsertId,
|
||||
};
|
||||
}
|
||||
|
||||
async select<T extends DatabaseRow = DatabaseRow>(
|
||||
sql: string,
|
||||
params: unknown[] = [],
|
||||
): Promise<T[]> {
|
||||
return (await invoke('plugin:turso|select', {
|
||||
db: this.dbPath,
|
||||
query: sql,
|
||||
values: params,
|
||||
})) as T[];
|
||||
}
|
||||
|
||||
async batch(statements: string[]): Promise<void> {
|
||||
await invoke('plugin:turso|batch', {
|
||||
db: this.dbPath,
|
||||
queries: statements,
|
||||
});
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await invoke('plugin:turso|close', { db: this.dbPath });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Integration tests for the turso-backed tauri-plugin-turso running inside
|
||||
* the Tauri WebView. Calls plugin IPC commands via __TAURI_INTERNALS__.invoke().
|
||||
*
|
||||
* The database is opened with experimental index_method enabled so that
|
||||
* FTS (Tantivy-based) CREATE INDEX ... USING fts works.
|
||||
*/
|
||||
describe('turso plugin (native Tauri)', () => {
|
||||
const DB_PATH = 'sqlite::memory:';
|
||||
let dbPath: string;
|
||||
let db: DatabaseService;
|
||||
|
||||
beforeEach(async () => {
|
||||
dbPath = (await invoke('plugin:turso|load', {
|
||||
options: { path: DB_PATH, experimental: ['index_method'] },
|
||||
})) as string;
|
||||
db = new TauriDatabaseAdapter(dbPath);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Connection lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('loads an in-memory database and returns a path', () => {
|
||||
expect(typeof dbPath).toBe('string');
|
||||
expect(dbPath.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('get_config returns encryption status', async () => {
|
||||
const config = (await invoke('plugin:turso|get_config')) as { encrypted: boolean };
|
||||
expect(typeof config.encrypted).toBe('boolean');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared test suites
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Base Operations', () => {
|
||||
baseTests(() => db);
|
||||
});
|
||||
|
||||
describe('Full-Text Search', () => {
|
||||
ftsTests(() => db);
|
||||
});
|
||||
|
||||
describe('Vector Search', () => {
|
||||
vectorTests(() => db);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, beforeEach, afterEach } from 'vitest';
|
||||
import { WebDatabaseService } from '@/services/database/webDatabaseService';
|
||||
import { DatabaseService, DatabaseExecResult } from '@/types/database';
|
||||
import { DatabaseService } from '@/types/database';
|
||||
import { baseTests } from './suites/base-tests';
|
||||
import { ftsTests } from './suites/fts-tests';
|
||||
import { vectorTests } from './suites/vector-tests';
|
||||
|
||||
@@ -20,171 +21,14 @@ describe('WebDatabaseService (browser WASM, in-memory SQLite)', () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Schema & basic operations
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('creates a table and inserts a row', async () => {
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
const result: DatabaseExecResult = await db.execute('INSERT INTO items (name) VALUES (?)', [
|
||||
'apple',
|
||||
]);
|
||||
expect(result.rowsAffected).toBe(1);
|
||||
expect(result.lastInsertId).toBe(1);
|
||||
describe('Base Operations', () => {
|
||||
baseTests(() => db);
|
||||
});
|
||||
|
||||
it('inserts multiple rows with auto-incrementing ids', async () => {
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
const r1 = await db.execute('INSERT INTO items (name) VALUES (?)', ['a']);
|
||||
const r2 = await db.execute('INSERT INTO items (name) VALUES (?)', ['b']);
|
||||
const r3 = await db.execute('INSERT INTO items (name) VALUES (?)', ['c']);
|
||||
expect(r1.lastInsertId).toBe(1);
|
||||
expect(r2.lastInsertId).toBe(2);
|
||||
expect(r3.lastInsertId).toBe(3);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SELECT queries
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('select() returns typed rows', async () => {
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT, qty INTEGER)');
|
||||
await db.execute('INSERT INTO items (name, qty) VALUES (?, ?)', ['apple', 10]);
|
||||
await db.execute('INSERT INTO items (name, qty) VALUES (?, ?)', ['banana', 20]);
|
||||
|
||||
const rows = await db.select<{ id: number; name: string; qty: number }>(
|
||||
'SELECT * FROM items ORDER BY id',
|
||||
);
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows[0]!.name).toBe('apple');
|
||||
expect(rows[0]!.qty).toBe(10);
|
||||
expect(rows[1]!.name).toBe('banana');
|
||||
expect(rows[1]!.qty).toBe(20);
|
||||
});
|
||||
|
||||
it('select() with WHERE and params', async () => {
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['apple']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['banana']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['cherry']);
|
||||
|
||||
const rows = await db.select<{ id: number; name: string }>(
|
||||
'SELECT * FROM items WHERE name = ?',
|
||||
['banana'],
|
||||
);
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]!.name).toBe('banana');
|
||||
});
|
||||
|
||||
it('select() returns empty array for no matching rows', async () => {
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
const rows = await db.select('SELECT * FROM items');
|
||||
expect(rows).toEqual([]);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// UPDATE & DELETE
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('execute() UPDATE returns rowsAffected', async () => {
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['old']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['old']);
|
||||
|
||||
const result = await db.execute('UPDATE items SET name = ? WHERE name = ?', ['new', 'old']);
|
||||
expect(result.rowsAffected).toBe(2);
|
||||
});
|
||||
|
||||
it('execute() DELETE returns rowsAffected', async () => {
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['a']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['b']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['c']);
|
||||
|
||||
const result = await db.execute('DELETE FROM items WHERE name IN (?, ?)', ['a', 'c']);
|
||||
expect(result.rowsAffected).toBe(2);
|
||||
|
||||
const remaining = await db.select<{ name: string }>('SELECT name FROM items');
|
||||
expect(remaining).toHaveLength(1);
|
||||
expect(remaining[0]!.name).toBe('b');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Batch execution
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('batch() executes multiple statements atomically', async () => {
|
||||
await db.batch([
|
||||
'CREATE TABLE t1 (id INTEGER PRIMARY KEY, val TEXT)',
|
||||
"INSERT INTO t1 (val) VALUES ('one')",
|
||||
"INSERT INTO t1 (val) VALUES ('two')",
|
||||
"INSERT INTO t1 (val) VALUES ('three')",
|
||||
]);
|
||||
|
||||
const rows = await db.select<{ val: string }>('SELECT val FROM t1 ORDER BY id');
|
||||
expect(rows).toHaveLength(3);
|
||||
expect(rows.map((r) => r.val)).toEqual(['one', 'two', 'three']);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Data types
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('handles NULL values correctly', async () => {
|
||||
await db.execute('CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)');
|
||||
await db.execute('INSERT INTO t (val) VALUES (?)', [null]);
|
||||
|
||||
const rows = await db.select<{ id: number; val: string | null }>('SELECT * FROM t');
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]!.val).toBeNull();
|
||||
});
|
||||
|
||||
it('handles integer and real types', async () => {
|
||||
await db.execute('CREATE TABLE nums (i INTEGER, r REAL)');
|
||||
await db.execute('INSERT INTO nums (i, r) VALUES (?, ?)', [42, 3.14]);
|
||||
|
||||
const rows = await db.select<{ i: number; r: number }>('SELECT * FROM nums');
|
||||
expect(rows[0]!.i).toBe(42);
|
||||
expect(rows[0]!.r).toBeCloseTo(3.14);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Error handling
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('throws on invalid SQL', async () => {
|
||||
await expect(db.execute('INVALID SQL STATEMENT')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('throws on constraint violation', async () => {
|
||||
await db.execute('CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT UNIQUE)');
|
||||
await db.execute('INSERT INTO t (val) VALUES (?)', ['unique_val']);
|
||||
await expect(db.execute('INSERT INTO t (val) VALUES (?)', ['unique_val'])).rejects.toThrow();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// DatabaseExecResult contract
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('execute() result always has rowsAffected and lastInsertId as numbers', async () => {
|
||||
await db.execute('CREATE TABLE t (id INTEGER PRIMARY KEY)');
|
||||
const result = await db.execute('INSERT INTO t DEFAULT VALUES');
|
||||
expect(typeof result.rowsAffected).toBe('number');
|
||||
expect(typeof result.lastInsertId).toBe('number');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Full-text search (Turso native FTS, Tantivy-based)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
describe('Full-Text Search', () => {
|
||||
ftsTests(() => db);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Vector search
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
describe('Vector Search', () => {
|
||||
vectorTests(() => db);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Database, QueryResult } from 'tauri-plugin-libsql';
|
||||
import { Database, QueryResult } from 'tauri-plugin-turso';
|
||||
import { DatabaseService, DatabaseExecResult, DatabaseRow, DatabaseOpts } from '@/types/database';
|
||||
|
||||
export class NativeDatabaseService implements DatabaseService {
|
||||
|
||||
@@ -18,7 +18,7 @@ interface TursoDatabase {
|
||||
|
||||
/**
|
||||
* DatabaseService implementation backed by @tursodatabase/database (Node.js native).
|
||||
* Uses the same libsql engine and API surface as the browser-based
|
||||
* Uses the same turso engine and API surface as the browser-based
|
||||
* @tursodatabase/database-wasm used by WebDatabaseService.
|
||||
*/
|
||||
export class NodeDatabaseService implements DatabaseService {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@/components/ui/*": ["./src/components/primitives/*"],
|
||||
"@pdfjs/*": ["./public/vendor/pdfjs/*"],
|
||||
"@simplecc/*": ["./public/vendor/simplecc/*"],
|
||||
"tauri-plugin-libsql": ["./src-tauri/plugins/tauri-plugin-libsql/guest-js"]
|
||||
"tauri-plugin-turso": ["./src-tauri/plugins/tauri-plugin-turso/guest-js"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
||||
Reference in New Issue
Block a user