Skip to main content
Glama
IBM
by IBM
duckDBService.test.ts11.8 kB
/** * @fileoverview Tests for the main DuckDB service. * @module tests/services/duck-db/duckDBService.test */ import * as duckdb from "@duckdb/node-api"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { DuckDBService } from "../../../src/services/duck-db/duckDBService.js"; import { DuckDBConnectionManager } from "../../../src/services/duck-db/duckDBConnectionManager.js"; import { DuckDBQueryExecutor } from "../../../src/services/duck-db/duckDBQueryExecutor.js"; import { McpError } from "../../../src/types-global/errors.js"; // Mock the dependencies vi.mock("../../../src/services/duck-db/duckDBConnectionManager.js"); vi.mock("../../../src/services/duck-db/duckDBQueryExecutor.js"); vi.mock("@duckdb/node-api"); describe("DuckDBService", () => { let duckDBService: DuckDBService; let mockConnectionManager: Partial<DuckDBConnectionManager>; let mockQueryExecutor: Partial<DuckDBQueryExecutor>; let mockConnection: Partial<duckdb.DuckDBConnection>; let mockInstance: Partial<duckdb.DuckDBInstance>; beforeEach(() => { vi.clearAllMocks(); // Create mocks mockConnection = { run: vi.fn() }; mockInstance = { connect: vi.fn() }; mockQueryExecutor = { run: vi.fn(), query: vi.fn(), stream: vi.fn(), prepare: vi.fn(), beginTransaction: vi.fn(), commitTransaction: vi.fn(), rollbackTransaction: vi.fn(), }; mockConnectionManager = { initialize: vi.fn(), getConnection: vi.fn().mockReturnValue(mockConnection), getInstance: vi.fn().mockReturnValue(mockInstance), ensureInitialized: vi.fn(), loadExtension: vi.fn(), close: vi.fn(), isServiceInitialized: true, }; // Mock the constructors vi.mocked(DuckDBConnectionManager).mockImplementation( () => mockConnectionManager as DuckDBConnectionManager, ); vi.mocked(DuckDBQueryExecutor).mockImplementation( () => mockQueryExecutor as DuckDBQueryExecutor, ); duckDBService = new DuckDBService(); }); describe("initialize", () => { it("should initialize the service with default configuration", async () => { await duckDBService.initialize(); expect(mockConnectionManager.initialize).toHaveBeenCalledWith(undefined); expect(DuckDBQueryExecutor).toHaveBeenCalledWith(mockConnection); }); it("should initialize the service with custom configuration", async () => { const config = { dbPath: "/tmp/test.db", launchConfig: { allow_unsigned_extensions: "true" }, extensions: ["httpfs"], }; await duckDBService.initialize(config); expect(mockConnectionManager.initialize).toHaveBeenCalledWith(config); }); it("should not reinitialize if already initialized", async () => { await duckDBService.initialize(); vi.clearAllMocks(); await duckDBService.initialize(); expect(mockConnectionManager.initialize).not.toHaveBeenCalled(); }); it("should handle initialization errors", async () => { const error = new Error("Initialization failed"); vi.mocked(mockConnectionManager.initialize!).mockRejectedValue(error); await expect(duckDBService.initialize()).rejects.toThrow(McpError); }); }); describe("run", () => { beforeEach(async () => { await duckDBService.initialize(); }); it("should execute SQL without parameters", async () => { const sql = "CREATE TABLE test (id INTEGER)"; vi.mocked(mockQueryExecutor.run!).mockResolvedValue(undefined); await duckDBService.run(sql); expect(mockConnectionManager.ensureInitialized).toHaveBeenCalled(); expect(mockQueryExecutor.run).toHaveBeenCalledWith(sql, undefined); }); it("should execute SQL with parameters", async () => { const sql = "INSERT INTO test VALUES (?)"; const params = [1]; vi.mocked(mockQueryExecutor.run!).mockResolvedValue(undefined); await duckDBService.run(sql, params); expect(mockQueryExecutor.run).toHaveBeenCalledWith(sql, params); }); it("should throw error for non-array parameters", async () => { const sql = "SELECT * FROM test"; const params = { id: 1 } as unknown as unknown[]; await expect(duckDBService.run(sql, params)).rejects.toThrow(McpError); await expect(duckDBService.run(sql, params)).rejects.toThrow( "DuckDB service only supports array-style parameters", ); }); }); describe("query", () => { beforeEach(async () => { await duckDBService.initialize(); }); it("should execute query and return results", async () => { const sql = "SELECT * FROM test"; const expectedResult = { rows: [{ id: 1, name: "test" }], columnNames: ["id", "name"], columnTypes: [duckdb.DuckDBTypeId.INTEGER, duckdb.DuckDBTypeId.VARCHAR], rowCount: 1, }; vi.mocked(mockQueryExecutor.query!).mockResolvedValue(expectedResult); const result = await duckDBService.query(sql); expect(mockQueryExecutor.query).toHaveBeenCalledWith(sql, undefined); expect(result).toEqual(expectedResult); }); it("should execute query with parameters", async () => { const sql = "SELECT * FROM test WHERE id = ?"; const params = [1]; const expectedResult = { rows: [{ id: 1, name: "test" }], columnNames: ["id", "name"], columnTypes: [duckdb.DuckDBTypeId.INTEGER, duckdb.DuckDBTypeId.VARCHAR], rowCount: 1, }; vi.mocked(mockQueryExecutor.query!).mockResolvedValue(expectedResult); const result = await duckDBService.query(sql, params); expect(mockQueryExecutor.query).toHaveBeenCalledWith(sql, params); expect(result).toEqual(expectedResult); }); it("should validate parameters", async () => { const sql = "SELECT * FROM test"; const params = { id: 1 } as unknown as unknown[]; await expect(duckDBService.query(sql, params)).rejects.toThrow(McpError); }); }); describe("stream", () => { beforeEach(async () => { await duckDBService.initialize(); }); it("should return streaming result", async () => { const sql = "SELECT * FROM large_table"; const mockStreamResult = { stream: "result", } as unknown as duckdb.DuckDBResult; vi.mocked(mockQueryExecutor.stream!).mockResolvedValue(mockStreamResult); const result = await duckDBService.stream(sql); expect(mockQueryExecutor.stream).toHaveBeenCalledWith(sql, undefined); expect(result).toBe(mockStreamResult); }); it("should handle streaming with parameters", async () => { const sql = "SELECT * FROM large_table WHERE id > ?"; const params = [100]; const mockStreamResult = { stream: "result", } as unknown as duckdb.DuckDBResult; vi.mocked(mockQueryExecutor.stream!).mockResolvedValue(mockStreamResult); const result = await duckDBService.stream(sql, params); expect(mockQueryExecutor.stream).toHaveBeenCalledWith(sql, params); expect(result).toBe(mockStreamResult); }); }); describe("prepare", () => { beforeEach(async () => { await duckDBService.initialize(); }); it("should prepare a statement", async () => { const sql = "SELECT * FROM test WHERE id = ?"; const mockPreparedStatement = { prepared: true, } as unknown as duckdb.DuckDBPreparedStatement; vi.mocked(mockQueryExecutor.prepare!).mockResolvedValue( mockPreparedStatement, ); const result = await duckDBService.prepare(sql); expect(mockQueryExecutor.prepare).toHaveBeenCalledWith(sql); expect(result).toBe(mockPreparedStatement); }); }); describe("Transaction Management", () => { beforeEach(async () => { await duckDBService.initialize(); vi.mocked(mockQueryExecutor.beginTransaction!).mockResolvedValue( undefined, ); vi.mocked(mockQueryExecutor.commitTransaction!).mockResolvedValue( undefined, ); vi.mocked(mockQueryExecutor.rollbackTransaction!).mockResolvedValue( undefined, ); }); it("should begin transaction", async () => { await duckDBService.beginTransaction(); expect(mockQueryExecutor.beginTransaction).toHaveBeenCalled(); }); it("should commit transaction", async () => { await duckDBService.commitTransaction(); expect(mockQueryExecutor.commitTransaction).toHaveBeenCalled(); }); it("should rollback transaction", async () => { await duckDBService.rollbackTransaction(); expect(mockQueryExecutor.rollbackTransaction).toHaveBeenCalled(); }); }); describe("loadExtension", () => { beforeEach(async () => { await duckDBService.initialize(); }); it("should load extension", async () => { const extensionName = "spatial"; vi.mocked(mockConnectionManager.loadExtension!).mockResolvedValue( undefined, ); await duckDBService.loadExtension(extensionName); expect(mockConnectionManager.loadExtension).toHaveBeenCalledWith( extensionName, expect.objectContaining({ operation: "DuckDBService.loadExtension" }), ); }); }); describe("close", () => { beforeEach(async () => { await duckDBService.initialize(); }); it("should close the service", async () => { vi.mocked(mockConnectionManager.close!).mockResolvedValue(undefined); await duckDBService.close(); expect(mockConnectionManager.close).toHaveBeenCalled(); }); it("should handle close errors", async () => { const error = new Error("Close failed"); vi.mocked(mockConnectionManager.close!).mockRejectedValue(error); await expect(duckDBService.close()).rejects.toThrow(McpError); }); }); describe("Raw Access Methods", () => { it("should return raw connection when initialized", async () => { await duckDBService.initialize(); const connection = duckDBService.getRawConnection(); expect(connection).toBe(mockConnection); }); it("should return null when not initialized", () => { const uninitializedService = new DuckDBService(); Object.defineProperty(mockConnectionManager, "isServiceInitialized", { get: vi.fn(() => false), configurable: true, }); const connection = uninitializedService.getRawConnection(); expect(connection).toBeNull(); }); it("should return raw instance when initialized", async () => { await duckDBService.initialize(); const instance = duckDBService.getRawInstance(); expect(instance).toBe(mockInstance); }); it("should return null instance when not initialized", () => { const uninitializedService = new DuckDBService(); Object.defineProperty(mockConnectionManager, "isServiceInitialized", { get: vi.fn(() => false), configurable: true, }); const instance = uninitializedService.getRawInstance(); expect(instance).toBeNull(); }); }); describe("Error Handling", () => { it("should handle missing query executor", async () => { const service = new DuckDBService(); // Simulate partially initialized state ( service as unknown as { isInitialized: boolean; queryExecutor: null } ).isInitialized = true; ( service as unknown as { isInitialized: boolean; queryExecutor: null } ).queryExecutor = null; await expect(service.run("SELECT 1")).rejects.toThrow(McpError); await expect(service.run("SELECT 1")).rejects.toThrow( "DuckDBQueryExecutor not available", ); }); }); });

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/IBM/ibmi-mcp'

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