Skip to main content
Glama
lancedb-failures.test.js14.2 kB
/** * Negative tests for LanceDB failure handling * Tests graceful handling of table creation, write, and delete failures */ import { describe, it, expect, beforeEach, vi } from 'vitest' describe('LanceDB Failure Handling', () => { describe('table creation failures', () => { const createMockDb = (options = {}) => { const { createTableFails = false, tableExistsFails = false, errorMessage = 'Table creation failed' } = options const tables = new Map() return { tableNames: async () => { if (tableExistsFails) throw new Error('Failed to list tables') return [...tables.keys()] }, createTable: async (name, data) => { if (createTableFails) throw new Error(errorMessage) tables.set(name, { data: [...data] }) return tables.get(name) }, openTable: async (name) => { if (!tables.has(name)) throw new Error(`Table ${name} not found`) return tables.get(name) } } } it('should handle table creation failure', async () => { const db = createMockDb({ createTableFails: true, errorMessage: 'Disk full' }) const initTable = async () => { try { await db.createTable('emails', []) return { success: true } } catch (e) { return { success: false, error: e.message } } } const result = await initTable() expect(result.success).toBe(false) expect(result.error).toBe('Disk full') }) it('should handle table listing failure', async () => { const db = createMockDb({ tableExistsFails: true }) const checkTableExists = async (name) => { try { const tables = await db.tableNames() return { exists: tables.includes(name), error: null } } catch (e) { return { exists: false, error: e.message } } } const result = await checkTableExists('emails') expect(result.error).toBe('Failed to list tables') }) it('should retry table creation on transient failure', async () => { let attempts = 0 const flakyDb = { createTable: async (name, data) => { attempts++ if (attempts < 3) throw new Error('Transient error') return { name, data } } } const createWithRetry = async (name, data, maxRetries = 3) => { for (let i = 0; i < maxRetries; i++) { try { return await flakyDb.createTable(name, data) } catch (e) { if (i === maxRetries - 1) throw e await new Promise(r => setTimeout(r, 10)) } } } const result = await createWithRetry('emails', []) expect(result.name).toBe('emails') expect(attempts).toBe(3) }) }) describe('write failures', () => { const createMockTable = (options = {}) => { const { addFails = false, addFailsAfter = Infinity, errorMessage = 'Write failed' } = options let addCount = 0 const data = [] return { add: async (items) => { addCount++ if (addFails && addCount > addFailsAfter) { throw new Error(errorMessage) } data.push(...items) return { added: items.length } }, getData: () => data, getAddCount: () => addCount } } it('should handle write failure mid-batch', async () => { const table = createMockTable({ addFails: true, addFailsAfter: 1, errorMessage: 'Connection lost' }) const batches = [ [{ id: 1 }, { id: 2 }], [{ id: 3 }, { id: 4 }], [{ id: 5 }, { id: 6 }] ] const writtenBatches = [] const failedBatches = [] for (const batch of batches) { try { await table.add(batch) writtenBatches.push(batch) } catch (e) { failedBatches.push({ batch, error: e.message }) } } expect(writtenBatches).toHaveLength(1) // Only first batch succeeded expect(failedBatches).toHaveLength(2) // Batches 2 and 3 failed }) it('should track partial batch success', async () => { // Simulate a table that fails on specific items const createPartialFailTable = () => { const data = [] return { add: async (items) => { const successful = [] const failed = [] for (const item of items) { if (item.id % 3 === 0) { failed.push({ item, error: 'ID divisible by 3' }) } else { data.push(item) successful.push(item) } } return { added: successful.length, failed } }, getData: () => data } } const table = createPartialFailTable() const items = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }, { id: 6 }] const result = await table.add(items) expect(result.added).toBe(4) // 1, 2, 4, 5 expect(result.failed).toHaveLength(2) // 3, 6 }) it('should handle vector validation failure', async () => { const validateAndAdd = async (table, items) => { const EXPECTED_DIM = 384 const valid = [] const invalid = [] for (const item of items) { if (!item.vector || item.vector.length !== EXPECTED_DIM) { invalid.push({ item, reason: 'invalid_vector' }) } else if (item.vector.some(v => !Number.isFinite(v))) { invalid.push({ item, reason: 'non_finite_values' }) } else { valid.push(item) } } if (valid.length > 0) { await table.add(valid) } return { added: valid.length, invalid } } const mockTable = { add: async (items) => ({ added: items.length }) } const items = [ { id: 1, vector: new Array(384).fill(0.1) }, { id: 2, vector: new Array(100).fill(0.1) }, // Wrong dimension { id: 3, vector: new Array(384).fill(NaN) }, // Invalid values { id: 4, vector: new Array(384).fill(0.2) } ] const result = await validateAndAdd(mockTable, items) expect(result.added).toBe(2) // Items 1 and 4 expect(result.invalid).toHaveLength(2) }) }) describe('delete failures', () => { const createMockTableWithDelete = (options = {}) => { const { deleteFails = false, deleteFailsOn = [], errorMessage = 'Delete failed' } = options const data = new Map() return { add: async (items) => { items.forEach(item => data.set(item.id, item)) }, delete: async (filter) => { if (deleteFails) throw new Error(errorMessage) // Parse simple filter like "id = 'value'" or "id = 'v1' OR id = 'v2'" const matches = filter.match(/id\s*=\s*'([^']+)'/g) || [] const ids = matches.map(m => m.match(/'([^']+)'/)[1]) let deleted = 0 for (const id of ids) { if (deleteFailsOn.includes(id)) { throw new Error(`Cannot delete ${id}`) } if (data.delete(id)) deleted++ } return { deleted } }, getData: () => [...data.values()] } } it('should handle delete failure', async () => { const table = createMockTableWithDelete({ deleteFails: true, errorMessage: 'Table locked' }) await table.add([{ id: 'item1' }, { id: 'item2' }]) const deleteWithFallback = async () => { try { await table.delete("id = 'item1'") return { success: true } } catch (e) { return { success: false, error: e.message } } } const result = await deleteWithFallback() expect(result.success).toBe(false) expect(result.error).toBe('Table locked') }) it('should handle batch delete with partial failure', async () => { const table = createMockTableWithDelete({ deleteFailsOn: ['item2'] }) await table.add([ { id: 'item1' }, { id: 'item2' }, { id: 'item3' } ]) const batchDelete = async (ids) => { const results = { deleted: [], failed: [] } for (const id of ids) { try { await table.delete(`id = '${id}'`) results.deleted.push(id) } catch (e) { results.failed.push({ id, error: e.message }) } } return results } const result = await batchDelete(['item1', 'item2', 'item3']) expect(result.deleted).toContain('item1') expect(result.deleted).toContain('item3') expect(result.failed).toHaveLength(1) expect(result.failed[0].id).toBe('item2') }) it('should continue after delete error in batch OR condition', async () => { // Test the actual batch delete pattern used in indexer.js const executeBatchDelete = async (table, ids, batchSize = 100) => { const results = { success: 0, failed: 0 } for (let i = 0; i < ids.length; i += batchSize) { const batch = ids.slice(i, i + batchSize) const conditions = batch.map(id => `id = '${id}'`).join(' OR ') try { await table.delete(conditions) results.success += batch.length } catch (e) { // Log error but continue with next batch results.failed += batch.length console.error(`Batch delete failed: ${e.message}`) } } return results } let deleteAttempts = 0 const flakyTable = { delete: async (filter) => { deleteAttempts++ if (deleteAttempts === 2) throw new Error('Transient failure') return { deleted: 1 } } } const ids = Array.from({ length: 250 }, (_, i) => `id${i}`) const result = await executeBatchDelete(flakyTable, ids, 100) // 3 batches: batch 1 succeeds (100), batch 2 fails (100), batch 3 succeeds (50) expect(result.success).toBe(150) expect(result.failed).toBe(100) }) }) describe('connection recovery', () => { it('should reconnect after connection drop', async () => { let connectionCount = 0 let isConnected = false const createConnection = async () => { connectionCount++ isConnected = true return { query: async () => { if (!isConnected) throw new Error('Not connected') return [] }, disconnect: () => { isConnected = false } } } const withReconnect = async (operation, maxRetries = 3) => { let connection = await createConnection() for (let i = 0; i < maxRetries; i++) { try { return await operation(connection) } catch (e) { if (e.message === 'Not connected' && i < maxRetries - 1) { connection = await createConnection() } else { throw e } } } } // Simulate connection drop const result = await withReconnect(async (conn) => { if (connectionCount === 1) { conn.disconnect() return conn.query() // Will fail } return conn.query() // Will succeed on reconnect }) expect(connectionCount).toBe(2) expect(result).toEqual([]) }) it('should handle database file moved/deleted', async () => { let dbExists = true const openDb = async (path) => { if (!dbExists) { throw new Error(`ENOENT: no such file or directory '${path}'`) } return { path, tables: [] } } const safeOpen = async (path) => { try { return { db: await openDb(path), error: null } } catch (e) { if (e.message.includes('ENOENT')) { return { db: null, error: 'database_not_found' } } return { db: null, error: e.message } } } // First open succeeds let result = await safeOpen('/path/to/db') expect(result.db).not.toBeNull() // Simulate deletion dbExists = false // Second open fails gracefully result = await safeOpen('/path/to/db') expect(result.db).toBeNull() expect(result.error).toBe('database_not_found') }) }) describe('index corruption recovery', () => { it('should detect and rebuild corrupted index', async () => { const checkIndexHealth = async (table) => { const tests = { canQuery: false, hasValidSchema: false, rowCountMatches: false } try { // Test 1: Can execute simple query await table.search('test').execute() tests.canQuery = true // Test 2: Has expected columns const schema = await table.schema() tests.hasValidSchema = schema.fields.some(f => f.name === 'vector') // Test 3: Row count is reasonable const count = await table.countRows() tests.rowCountMatches = count >= 0 return { healthy: Object.values(tests).every(t => t), tests } } catch (e) { return { healthy: false, error: e.message, tests } } } // Simulate healthy table const healthyTable = { search: () => ({ execute: async () => [] }), schema: async () => ({ fields: [{ name: 'vector' }, { name: 'id' }] }), countRows: async () => 1000 } const healthResult = await checkIndexHealth(healthyTable) expect(healthResult.healthy).toBe(true) // Simulate corrupted table const corruptTable = { search: () => ({ execute: async () => { throw new Error('Index corrupted') } }), schema: async () => ({ fields: [] }), countRows: async () => -1 } const corruptResult = await checkIndexHealth(corruptTable) expect(corruptResult.healthy).toBe(false) }) }) })

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sfls1397/Apple-Tools-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server