Skip to main content
Glama
postgres.integration.test.ts20.5 kB
import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql'; import { PostgresConnector } from '../postgres/index.js'; import { IntegrationTestBase, type TestContainer, type DatabaseTestConfig } from './shared/integration-test-base.js'; import type { Connector } from '../interface.js'; class PostgreSQLTestContainer implements TestContainer { constructor(private container: StartedPostgreSqlContainer) {} getConnectionUri(): string { return this.container.getConnectionUri(); } async stop(): Promise<void> { await this.container.stop(); } } class PostgreSQLIntegrationTest extends IntegrationTestBase<PostgreSQLTestContainer> { constructor() { const config: DatabaseTestConfig = { expectedSchemas: ['public', 'test_schema'], expectedTables: ['users', 'orders'], expectedTestSchemaTable: 'products', testSchema: 'test_schema', supportsStoredProcedures: true, expectedStoredProcedures: ['get_user_count', 'calculate_total_age'] }; super(config); } async createContainer(): Promise<PostgreSQLTestContainer> { const container = await new PostgreSqlContainer('postgres:15-alpine') .withDatabase('testdb') .withUsername('testuser') .withPassword('testpass') .start(); return new PostgreSQLTestContainer(container); } createConnector(): Connector { return new PostgresConnector(); } createSSLTests(): void { describe('SSL Connection Tests', () => { it('should handle SSL mode disable connection', async () => { const baseUri = this.connectionString; const sslDisabledUri = baseUri.includes('?') ? `${baseUri}&sslmode=disable` : `${baseUri}?sslmode=disable`; const sslDisabledConnector = new PostgresConnector(); // Should connect successfully with sslmode=disable await expect(sslDisabledConnector.connect(sslDisabledUri)).resolves.not.toThrow(); // Check SSL status - should be disabled (false) const result = await sslDisabledConnector.executeSQL('SELECT ssl FROM pg_stat_ssl WHERE pid = pg_backend_pid()', {}); expect(result.rows).toHaveLength(1); expect(result.rows[0].ssl).toBe(false); await sslDisabledConnector.disconnect(); }); it('should handle SSL mode require connection', async () => { const baseUri = this.connectionString; const sslRequiredUri = baseUri.includes('?') ? `${baseUri}&sslmode=require` : `${baseUri}?sslmode=require`; const sslRequiredConnector = new PostgresConnector(); // In test containers, SSL may not be supported, so we expect either success or SSL not supported error try { await sslRequiredConnector.connect(sslRequiredUri); // If connection succeeds, check SSL status - should be enabled (true) const result = await sslRequiredConnector.executeSQL('SELECT ssl FROM pg_stat_ssl WHERE pid = pg_backend_pid()', {}); expect(result.rows).toHaveLength(1); expect(result.rows[0].ssl).toBe(true); await sslRequiredConnector.disconnect(); } catch (error) { // If SSL is not supported by the test container, that's expected expect(error instanceof Error).toBe(true); expect((error as Error).message).toMatch(/SSL|does not support SSL/); } }); }); } async setupTestData(connector: Connector): Promise<void> { // Create test schema await connector.executeSQL('CREATE SCHEMA IF NOT EXISTS test_schema', {}); // Create users table await connector.executeSQL(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, age INTEGER ) `, {}); // Create orders table await connector.executeSQL(` CREATE TABLE IF NOT EXISTS orders ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id), total DECIMAL(10,2), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `, {}); // Create products table in test_schema await connector.executeSQL(` CREATE TABLE IF NOT EXISTS test_schema.products ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) ) `, {}); // Insert test data await connector.executeSQL(` INSERT INTO users (name, email, age) VALUES ('John Doe', 'john@example.com', 30), ('Jane Smith', 'jane@example.com', 25), ('Bob Johnson', 'bob@example.com', 35) ON CONFLICT (email) DO NOTHING `, {}); await connector.executeSQL(` INSERT INTO orders (user_id, total) VALUES (1, 99.99), (1, 149.50), (2, 75.25) ON CONFLICT DO NOTHING `, {}); await connector.executeSQL(` INSERT INTO test_schema.products (name, price) VALUES ('Widget A', 19.99), ('Widget B', 29.99) ON CONFLICT DO NOTHING `, {}); // Create test stored procedures using SQL language to avoid dollar quoting await connector.executeSQL(` CREATE OR REPLACE FUNCTION get_user_count() RETURNS INTEGER LANGUAGE SQL AS 'SELECT COUNT(*)::INTEGER FROM users' `, {}); await connector.executeSQL(` CREATE OR REPLACE FUNCTION calculate_total_age() RETURNS INTEGER LANGUAGE SQL AS 'SELECT COALESCE(SUM(age), 0)::INTEGER FROM users WHERE age IS NOT NULL' `, {}); } } // Create the test suite const postgresTest = new PostgreSQLIntegrationTest(); describe('PostgreSQL Connector Integration Tests', () => { beforeAll(async () => { await postgresTest.setup(); }, 120000); afterAll(async () => { await postgresTest.cleanup(); }); // Include all common tests postgresTest.createConnectionTests(); postgresTest.createSchemaTests(); postgresTest.createTableTests(); postgresTest.createSQLExecutionTests(); if (postgresTest.config.supportsStoredProcedures) { postgresTest.createStoredProcedureTests(); } postgresTest.createErrorHandlingTests(); postgresTest.createSSLTests(); describe('PostgreSQL-specific Features', () => { it('should execute multiple statements with transaction support', async () => { const result = await postgresTest.connector.executeSQL(` INSERT INTO users (name, email, age) VALUES ('Multi User 1', 'multi1@example.com', 30); INSERT INTO users (name, email, age) VALUES ('Multi User 2', 'multi2@example.com', 35); SELECT COUNT(*) as total FROM users WHERE email LIKE 'multi%'; `, {}); expect(result.rows).toHaveLength(1); expect(result.rows[0].total).toBe('2'); }); it('should handle PostgreSQL-specific data types', async () => { await postgresTest.connector.executeSQL(` CREATE TABLE IF NOT EXISTS postgres_types_test ( id SERIAL PRIMARY KEY, json_data JSONB, uuid_val UUID DEFAULT gen_random_uuid(), array_val INTEGER[], timestamp_val TIMESTAMP WITH TIME ZONE DEFAULT NOW() ) `, {}); await postgresTest.connector.executeSQL(` INSERT INTO postgres_types_test (json_data, array_val) VALUES ('{"key": "value"}', ARRAY[1,2,3,4,5]) `, {}); const result = await postgresTest.connector.executeSQL( 'SELECT * FROM postgres_types_test ORDER BY id DESC LIMIT 1', {} ); expect(result.rows).toHaveLength(1); expect(result.rows[0].json_data).toBeDefined(); expect(result.rows[0].uuid_val).toBeDefined(); expect(result.rows[0].array_val).toBeDefined(); }); it('should handle PostgreSQL returning clause', async () => { const result = await postgresTest.connector.executeSQL( "INSERT INTO users (name, email, age) VALUES ('Returning Test', 'returning@example.com', 40) RETURNING id, name", {} ); expect(result.rows).toHaveLength(1); expect(result.rows[0].id).toBeDefined(); expect(result.rows[0].name).toBe('Returning Test'); }); it('should work with PostgreSQL-specific functions', async () => { const result = await postgresTest.connector.executeSQL(` SELECT version() as postgres_version, current_database() as current_db, current_user as current_user, now() as current_time, gen_random_uuid() as random_uuid `, {}); expect(result.rows).toHaveLength(1); expect(result.rows[0].postgres_version).toContain('PostgreSQL'); expect(result.rows[0].current_db).toBe('testdb'); expect(result.rows[0].current_user).toBeDefined(); expect(result.rows[0].current_time).toBeDefined(); expect(result.rows[0].random_uuid).toBeDefined(); }); it('should handle PostgreSQL transactions correctly', async () => { // Test rollback on error await expect( postgresTest.connector.executeSQL(` BEGIN; INSERT INTO users (name, email, age) VALUES ('Transaction Test', 'trans@example.com', 40); INSERT INTO users (name, email, age) VALUES ('Transaction Test', 'trans@example.com', 40); -- This should fail due to unique constraint COMMIT; `, {}) ).rejects.toThrow(); // Verify rollback worked const result = await postgresTest.connector.executeSQL( "SELECT COUNT(*) as count FROM users WHERE email = 'trans@example.com'", {} ); expect(result.rows[0].count).toBe('0'); }); it('should handle PostgreSQL window functions', async () => { const result = await postgresTest.connector.executeSQL(` SELECT name, age, ROW_NUMBER() OVER (ORDER BY age DESC) as age_rank, AVG(age) OVER () as avg_age FROM users WHERE age IS NOT NULL ORDER BY age DESC `, {}); expect(result.rows.length).toBeGreaterThan(0); expect(result.rows[0]).toHaveProperty('age_rank'); expect(result.rows[0]).toHaveProperty('avg_age'); }); it('should handle PostgreSQL arrays and JSON operations', async () => { await postgresTest.connector.executeSQL(` CREATE TABLE IF NOT EXISTS json_test ( id SERIAL PRIMARY KEY, data JSONB ) `, {}); await postgresTest.connector.executeSQL(` INSERT INTO json_test (data) VALUES ('{"name": "John", "tags": ["admin", "user"], "settings": {"theme": "dark"}}'), ('{"name": "Jane", "tags": ["user"], "settings": {"theme": "light"}}') `, {}); const result = await postgresTest.connector.executeSQL(` SELECT data->>'name' as name, data->'tags' as tags, data#>>'{settings,theme}' as theme FROM json_test WHERE data @> '{"tags": ["admin"]}' `, {}); expect(result.rows).toHaveLength(1); expect(result.rows[0].name).toBe('John'); expect(result.rows[0].theme).toBe('dark'); }); it('should respect maxRows limit for SELECT queries', async () => { // Test basic SELECT with maxRows limit const result = await postgresTest.connector.executeSQL( 'SELECT * FROM users ORDER BY id', { maxRows: 2 } ); expect(result.rows).toHaveLength(2); expect(result.rows[0]).toHaveProperty('name'); expect(result.rows[1]).toHaveProperty('name'); }); it('should respect existing LIMIT clause when lower than maxRows', async () => { // Test when existing LIMIT is lower than maxRows const result = await postgresTest.connector.executeSQL( 'SELECT * FROM users ORDER BY id LIMIT 1', { maxRows: 3 } ); expect(result.rows).toHaveLength(1); expect(result.rows[0]).toHaveProperty('name'); }); it('should use maxRows when existing LIMIT is higher', async () => { // Test when existing LIMIT is higher than maxRows const result = await postgresTest.connector.executeSQL( 'SELECT * FROM users ORDER BY id LIMIT 10', { maxRows: 2 } ); expect(result.rows).toHaveLength(2); expect(result.rows[0]).toHaveProperty('name'); expect(result.rows[1]).toHaveProperty('name'); }); it('should not affect non-SELECT queries', async () => { // Test that maxRows doesn't affect INSERT/UPDATE/DELETE const insertResult = await postgresTest.connector.executeSQL( "INSERT INTO users (name, email, age) VALUES ('MaxRows Test', 'maxrows@example.com', 25)", { maxRows: 1 } ); expect(insertResult.rows).toHaveLength(0); // INSERTs don't return rows by default // Verify the insert worked const selectResult = await postgresTest.connector.executeSQL( "SELECT * FROM users WHERE email = 'maxrows@example.com'", {} ); expect(selectResult.rows).toHaveLength(1); expect(selectResult.rows[0].name).toBe('MaxRows Test'); }); it('should handle maxRows with RETURNING clause', async () => { // Test maxRows with INSERT...RETURNING (note: maxRows doesn't apply to INSERT/UPDATE/DELETE statements) const insertResult = await postgresTest.connector.executeSQL( "INSERT INTO users (name, email, age) VALUES ('Returning Test 1', 'return1@example.com', 30), ('Returning Test 2', 'return2@example.com', 35) RETURNING id, name", { maxRows: 1 } ); // INSERT...RETURNING returns all inserted rows regardless of maxRows setting expect(insertResult.rows).toHaveLength(2); expect(insertResult.rows[0]).toHaveProperty('id'); expect(insertResult.rows[0]).toHaveProperty('name'); expect(insertResult.rows[1]).toHaveProperty('id'); expect(insertResult.rows[1]).toHaveProperty('name'); }); it('should handle maxRows with complex queries', async () => { // Test maxRows with JOIN queries const result = await postgresTest.connector.executeSQL(` SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id ORDER BY o.total DESC `, { maxRows: 2 }); expect(result.rows.length).toBeLessThanOrEqual(2); expect(result.rows.length).toBeGreaterThan(0); expect(result.rows[0]).toHaveProperty('name'); expect(result.rows[0]).toHaveProperty('total'); }); it('should not apply maxRows to CTE queries (WITH clause)', async () => { // Test that maxRows is not applied to CTE queries (WITH clause) const result = await postgresTest.connector.executeSQL(` WITH user_summary AS ( SELECT name, age FROM users WHERE age IS NOT NULL ) SELECT * FROM user_summary ORDER BY age `, { maxRows: 2 }); // Should return all rows since WITH queries are not limited anymore expect(result.rows.length).toBeGreaterThan(2); expect(result.rows[0]).toHaveProperty('name'); expect(result.rows[0]).toHaveProperty('age'); }); it('should handle maxRows in multi-statement execution with transactions', async () => { // Test maxRows with multiple statements where some are SELECT const result = await postgresTest.connector.executeSQL(` INSERT INTO users (name, email, age) VALUES ('Multi Test 1', 'multi1@test.com', 30); SELECT name FROM users WHERE email LIKE '%@test.com' ORDER BY name; INSERT INTO users (name, email, age) VALUES ('Multi Test 2', 'multi2@test.com', 35); `, { maxRows: 1 }); // Should return only 1 row from the SELECT statement expect(result.rows).toHaveLength(1); expect(result.rows[0]).toHaveProperty('name'); }); it('should handle maxRows with PostgreSQL window functions', async () => { // Test maxRows with window functions const result = await postgresTest.connector.executeSQL(` SELECT name, age, ROW_NUMBER() OVER (ORDER BY age DESC) as age_rank, AVG(age::numeric) OVER () as avg_age FROM users WHERE age IS NOT NULL ORDER BY age DESC `, { maxRows: 2 }); expect(result.rows.length).toBeLessThanOrEqual(2); expect(result.rows.length).toBeGreaterThan(0); expect(result.rows[0]).toHaveProperty('age_rank'); expect(result.rows[0]).toHaveProperty('avg_age'); }); it('should ignore maxRows when not specified', async () => { // Test without maxRows - should return all rows (at least 3) const result = await postgresTest.connector.executeSQL( 'SELECT * FROM users ORDER BY id', {} ); // Should return at least the original 3 users plus any added in previous tests expect(result.rows.length).toBeGreaterThanOrEqual(3); }); }); describe('SDK-Level Readonly Mode Tests', () => { it('should set default_transaction_read_only at connection level', async () => { const readonlyConnector = new PostgresConnector(); try { // Connect with readonly flag await readonlyConnector.connect(postgresTest.connectionString, undefined, { readonly: true }); // Verify the connection has default_transaction_read_only set to 'on' const result = await readonlyConnector.executeSQL( 'SHOW default_transaction_read_only', {} ); expect(result.rows).toHaveLength(1); expect(result.rows[0].default_transaction_read_only).toBe('on'); } finally { await readonlyConnector.disconnect(); } }); it('should allow reads in readonly mode', async () => { const readonlyConnector = new PostgresConnector(); try { await readonlyConnector.connect(postgresTest.connectionString, undefined, { readonly: true }); // Should be able to read data const result = await readonlyConnector.executeSQL( 'SELECT * FROM users ORDER BY id LIMIT 1', {} ); expect(result.rows).toHaveLength(1); expect(result.rows[0].name).toBe('John Doe'); } finally { await readonlyConnector.disconnect(); } }); it('should reject writes in readonly mode', async () => { const readonlyConnector = new PostgresConnector(); try { await readonlyConnector.connect(postgresTest.connectionString, undefined, { readonly: true }); // Should NOT be able to write data (SDK-level enforcement) await expect( readonlyConnector.executeSQL( "INSERT INTO users (name, email) VALUES ('fail', 'fail@test.com')", {} ) ).rejects.toThrow(/read-only/); } finally { await readonlyConnector.disconnect(); } }); it('should reject DDL operations in readonly mode', async () => { const readonlyConnector = new PostgresConnector(); try { await readonlyConnector.connect(postgresTest.connectionString, undefined, { readonly: true }); // Should NOT be able to create tables await expect( readonlyConnector.executeSQL( 'CREATE TABLE should_fail (id INTEGER)', {} ) ).rejects.toThrow(/read-only/); } finally { await readonlyConnector.disconnect(); } }); it('should work normally without readonly flag', async () => { const normalConnector = new PostgresConnector(); try { // Connect WITHOUT readonly flag await normalConnector.connect(postgresTest.connectionString); // Verify default_transaction_read_only is off const showResult = await normalConnector.executeSQL( 'SHOW default_transaction_read_only', {} ); expect(showResult.rows[0].default_transaction_read_only).toBe('off'); // Should be able to write data const insertResult = await normalConnector.executeSQL( "INSERT INTO users (name, email) VALUES ('ReadonlyTest', 'test@readonly.com') RETURNING id", {} ); expect(insertResult.rows).toHaveLength(1); expect(insertResult.rows[0].id).toBeDefined(); } finally { await normalConnector.disconnect(); } }); }); });

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/bytebase/dbhub'

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