Skip to main content
Glama
alter-table.test.tsโ€ข14.5 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { AlterTableTool } from '../../tools/alter-table.js'; import type { ToolExecutionContext } from '../../lib/base-tool.js'; import type { DatabaseConnection } from '../../types/index.js'; // Mock the performance utils vi.mock('../../utils/performance.js', () => ({ formatPerformanceMetrics: vi.fn(() => '25ms, 1 affected') })); describe('AlterTableTool', () => { let tool: AlterTableTool; let mockConnection: DatabaseConnection; let mockTransaction: any; let context: ToolExecutionContext; beforeEach(() => { tool = new AlterTableTool(); mockTransaction = { execute: vi.fn(), commit: vi.fn(), rollback: vi.fn() }; mockConnection = { execute: vi.fn() as any, transaction: vi.fn() as any, close: vi.fn() as any, isHealthy: vi.fn() as any } as DatabaseConnection; context = { connection: mockConnection, arguments: {} } as ToolExecutionContext; }); afterEach(() => { vi.clearAllMocks(); }); describe('Tool Metadata', () => { it('should have correct name and description', () => { expect(tool.name).toBe('alter-table'); expect(tool.description).toContain('ALTER TABLE DDL'); expect(tool.description).toContain('adding columns'); expect(tool.description).toContain('transaction support'); }); it('should have correct input schema', () => { expect(tool.inputSchema).toBeDefined(); // The schema should validate ALTER TABLE statements const validInput = { query: 'ALTER TABLE users ADD COLUMN email TEXT', parameters: [], useTransaction: true, ifExists: false }; const result = tool.inputSchema.safeParse(validInput); expect(result.success).toBe(true); }); }); describe('Input Validation', () => { it('should accept valid ALTER TABLE statements', () => { const validQueries = [ 'ALTER TABLE users ADD COLUMN email TEXT', 'ALTER TABLE products ADD age INTEGER DEFAULT 0', 'ALTER TABLE orders RENAME TO customer_orders', 'ALTER TABLE users RENAME COLUMN name TO full_name', 'ALTER TABLE products DROP COLUMN description' ]; validQueries.forEach(query => { const result = tool.inputSchema.safeParse({ query }); expect(result.success).toBe(true); }); }); it('should reject non-ALTER TABLE statements', () => { const invalidQueries = [ 'SELECT * FROM users', 'INSERT INTO users VALUES (1, "test")', 'UPDATE users SET name = "test"', 'DELETE FROM users', 'DROP TABLE users', 'CREATE TABLE users (id INTEGER)' ]; invalidQueries.forEach(query => { const result = tool.inputSchema.safeParse({ query }); expect(result.success).toBe(false); }); }); it('should reject queries with prohibited operations', () => { const prohibitedQueries = [ 'ALTER TABLE users ADD COLUMN email TEXT; DROP TABLE sensitive;', 'ALTER TABLE users ADD COLUMN age INT; PRAGMA table_info(users)', 'ALTER TABLE users ADD COLUMN data TEXT; DELETE FROM sqlite_master;', 'ALTER TABLE users ADD COLUMN info TEXT; ATTACH DATABASE "other.db" AS other;' ]; prohibitedQueries.forEach(query => { const result = tool.inputSchema.safeParse({ query }); expect(result.success).toBe(false); }); }); it('should reject queries accessing system tables', () => { const systemTableQueries = [ 'ALTER TABLE sqlite_master ADD COLUMN test TEXT', 'ALTER TABLE sqlite_sequence RENAME TO backup_sequence', 'ALTER TABLE sqlite_temp_master ADD COLUMN backup TEXT' ]; systemTableQueries.forEach(query => { const result = tool.inputSchema.safeParse({ query }); expect(result.success).toBe(false); }); }); it('should reject malformed ALTER TABLE statements', () => { const malformedQueries = [ 'ALTER TABLE', // No table name or operation 'ALTER TABLE users', // No operation 'ALTER TABLE users ADD', // Incomplete ADD operation 'ALTER TABLE users RENAME', // Incomplete RENAME operation '' // Empty query ]; malformedQueries.forEach(query => { const result = tool.inputSchema.safeParse({ query }); expect(result.success).toBe(false); }); }); it('should apply default values correctly', () => { const input = { query: 'ALTER TABLE users ADD COLUMN email TEXT' }; const result = tool.inputSchema.parse(input); expect(result.parameters).toEqual([]); expect(result.useTransaction).toBe(true); expect(result.ifExists).toBe(false); }); it('should validate parameter limits', () => { const tooManyParams = Array(101).fill('test'); const result = tool.inputSchema.safeParse({ query: 'ALTER TABLE users ADD COLUMN email TEXT', parameters: tooManyParams }); expect(result.success).toBe(false); if (!result.success) { expect(result.error.message).toContain('Too many parameters'); } }); }); describe('Query Execution', () => { it('should execute ALTER TABLE with transaction by default', async () => { const mockResult = { rowsAffected: 1 }; mockConnection.transaction = vi.fn(async callback => { return await callback(mockTransaction); }); (mockTransaction.execute as any).mockResolvedValue(mockResult); context.arguments = { query: 'ALTER TABLE users ADD COLUMN email TEXT', useTransaction: true }; const result = await tool.execute(context); expect(mockConnection.transaction).toHaveBeenCalledOnce(); expect(mockTransaction.execute).toHaveBeenCalledWith( 'ALTER TABLE users ADD COLUMN email TEXT' ); expect(result.content[0].text).toContain('Table altered successfully (with transaction)'); expect(result.content[0].text).toContain('Table: users'); expect(result.content[0].text).toContain('Operation: ADD COLUMN'); }); it('should execute ALTER TABLE without transaction when disabled', async () => { const mockResult = { rowsAffected: 1 }; (mockConnection.execute as any).mockResolvedValue(mockResult); context.arguments = { query: 'ALTER TABLE products RENAME TO items', useTransaction: false }; const result = await tool.execute(context); expect(mockConnection.execute).toHaveBeenCalledWith('ALTER TABLE products RENAME TO items'); expect(mockConnection.transaction).not.toHaveBeenCalled(); expect(result.content[0].text).toContain('Table altered successfully'); expect(result.content[0].text).not.toContain('(with transaction)'); expect(result.content[0].text).toContain('Table: products'); expect(result.content[0].text).toContain('Operation: RENAME TABLE'); }); it('should execute ALTER TABLE with parameters', async () => { const mockResult = { rowsAffected: 1 }; mockConnection.transaction = vi.fn(async callback => { return await callback(mockTransaction); }); (mockTransaction.execute as any).mockResolvedValue(mockResult); context.arguments = { query: 'ALTER TABLE users ADD COLUMN data TEXT DEFAULT ?', parameters: ['default_value'], useTransaction: true }; const result = await tool.execute(context); expect(mockTransaction.execute).toHaveBeenCalledWith({ sql: 'ALTER TABLE users ADD COLUMN data TEXT DEFAULT ?', args: ['default_value'] }); expect(result.content[0].text).toContain('Table altered successfully'); }); it('should handle RENAME COLUMN operation', async () => { const mockResult = { rowsAffected: 1 }; mockConnection.transaction = vi.fn(async callback => { return await callback(mockTransaction); }); (mockTransaction.execute as any).mockResolvedValue(mockResult); context.arguments = { query: 'ALTER TABLE users RENAME COLUMN name TO full_name', useTransaction: true }; const result = await tool.execute(context); expect(result.content[0].text).toContain('Operation: RENAME COLUMN'); expect(result.content[0].text).toContain('Table: users'); }); it('should handle DROP COLUMN operation', async () => { const mockResult = { rowsAffected: 1 }; mockConnection.transaction = vi.fn(async callback => { return await callback(mockTransaction); }); (mockTransaction.execute as any).mockResolvedValue(mockResult); context.arguments = { query: 'ALTER TABLE products DROP COLUMN old_field', useTransaction: true }; const result = await tool.execute(context); expect(result.content[0].text).toContain('Operation: DROP COLUMN'); expect(result.content[0].text).toContain('Table: products'); }); it('should note SQLite limitation for DROP COLUMN with ifExists', async () => { const mockResult = { rowsAffected: 1 }; mockConnection.transaction = vi.fn(async callback => { return await callback(mockTransaction); }); (mockTransaction.execute as any).mockResolvedValue(mockResult); context.arguments = { query: 'ALTER TABLE users DROP COLUMN temp_field', ifExists: true, useTransaction: true }; const result = await tool.execute(context); expect(result.content[0].text).toContain('SQLite does not support IF EXISTS for DROP COLUMN'); }); }); describe('Error Handling', () => { it('should handle database execution errors with transaction', async () => { const error = new Error('Column already exists'); (mockConnection.transaction as any) = vi.fn().mockRejectedValue(error); context.arguments = { query: 'ALTER TABLE users ADD COLUMN email TEXT', useTransaction: true }; const result = await tool.execute(context); expect(result.isError).toBe(true); expect(result.content[0].text).toContain( 'Error altering table: Column already exists (transaction rolled back)' ); }); it('should handle database execution errors without transaction', async () => { const error = new Error('Database connection failed'); (mockConnection.execute as any).mockRejectedValue(error); context.arguments = { query: 'ALTER TABLE users ADD COLUMN email TEXT', useTransaction: false }; const result = await tool.execute(context); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Error altering table: Database connection failed'); expect(result.content[0].text).not.toContain('transaction rolled back'); }); it('should handle runtime validation errors during execution', async () => { const mockResult = { rowsAffected: 1 }; mockConnection.transaction = vi.fn(async callback => { return await callback(mockTransaction); }); (mockTransaction.execute as any).mockResolvedValue(mockResult); // Bypass schema validation by manually calling executeImpl context.arguments = { query: 'ALTER TABLE test ADD COLUMN col1 TEXT TRIGGER bad_trigger', // Should fail runtime validation useTransaction: true }; // @ts-ignore - accessing protected method for testing const result = await tool.executeImpl(context); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Error altering table:'); expect(result.content[0].text).toContain('TRIGGER'); }); }); describe('Output Formatting', () => { it('should include performance metrics in output', async () => { const mockResult = { rowsAffected: 1 }; mockConnection.transaction = vi.fn(async callback => { return await callback(mockTransaction); }); (mockTransaction.execute as any).mockResolvedValue(mockResult); context.arguments = { query: 'ALTER TABLE users ADD COLUMN email TEXT', useTransaction: true }; const result = await tool.execute(context); expect(result.content[0].text).toContain('25ms, 1 affected'); // Mocked performance metrics }); it('should extract and display table information correctly', async () => { const testCases = [ { query: 'ALTER TABLE users ADD COLUMN email TEXT', expectedTable: 'users', expectedOperation: 'ADD COLUMN' }, { query: 'ALTER TABLE "user_profiles" RENAME TO profiles', expectedTable: 'user_profiles', expectedOperation: 'RENAME TABLE' }, { query: 'ALTER TABLE `orders` RENAME COLUMN status TO order_status', expectedTable: 'orders', expectedOperation: 'RENAME COLUMN' }, { query: 'ALTER TABLE [products] DROP COLUMN old_field', expectedTable: 'products', expectedOperation: 'DROP COLUMN' } ]; for (const testCase of testCases) { const mockResult = { rowsAffected: 1 }; mockConnection.transaction = vi.fn(async callback => { return await callback(mockTransaction); }); (mockTransaction.execute as any).mockResolvedValue(mockResult); context.arguments = { query: testCase.query, useTransaction: true }; const result = await tool.execute(context); expect(result.content[0].text).toContain(`Table: ${testCase.expectedTable}`); expect(result.content[0].text).toContain(`Operation: ${testCase.expectedOperation}`); vi.clearAllMocks(); } }); it('should include rows affected in output when available', async () => { const mockResult = { rowsAffected: 1 }; mockConnection.transaction = vi.fn(async callback => { return await callback(mockTransaction); }); (mockTransaction.execute as any).mockResolvedValue(mockResult); context.arguments = { query: 'ALTER TABLE users ADD COLUMN email TEXT', useTransaction: true }; const result = await tool.execute(context); expect(result.content[0].text).toContain('Rows affected: 1'); }); }); });

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/Xexr/mcp-libsql'

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