UNPKG

@electric-sql/pglite-socket

Version:

A socket implementation for PGlite enabling remote connections

497 lines (427 loc) 14.2 kB
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, } from 'vitest' import postgres from 'postgres' import { PGlite } from '@electric-sql/pglite' import { PGLiteSocketServer } from '../src' /** * Debug configuration for testing * * To test against a real PostgreSQL server: * - Set DEBUG_TESTS=true as an environment variable * - Optionally set DEBUG_TESTS_REAL_SERVER with a connection URL (defaults to localhost) * * Example: * DEBUG_TESTS=true DEBUG_TESTS_REAL_SERVER=postgres://user:pass@host:port/db npm vitest ./tests/query-with-postgres-js.test.ts */ const DEBUG_LOCAL = process.env.DEBUG_LOCAL === 'true' const DEBUG_TESTS = process.env.DEBUG_TESTS === 'true' const DEBUG_TESTS_REAL_SERVER = process.env.DEBUG_TESTS_REAL_SERVER || 'postgres://postgres:postgres@localhost:5432/postgres' const TEST_PORT = 5434 describe(`PGLite Socket Server`, () => { describe('with postgres.js client', () => { let db: PGlite let server: PGLiteSocketServer let sql: ReturnType<typeof postgres> let connectionConfig: any beforeAll(async () => { if (DEBUG_TESTS) { console.log('TESTING WITH REAL POSTGRESQL SERVER') console.log(`Connection URL: ${DEBUG_TESTS_REAL_SERVER}`) } else { console.log('TESTING WITH PGLITE SERVER') // Create a PGlite instance if (DEBUG_LOCAL) db = await PGlite.create({ debug: '1' }) else db = await PGlite.create() // Wait for database to be ready await db.waitReady console.log('PGLite database ready') // Create and start the server with explicit host server = new PGLiteSocketServer({ db, port: TEST_PORT, host: '127.0.0.1', inspect: DEBUG_TESTS || DEBUG_LOCAL, }) // Add event listeners for debugging server.addEventListener('error', (event) => { console.error('Socket server error:', (event as CustomEvent).detail) }) server.addEventListener('connection', (event) => { console.log( 'Socket connection received:', (event as CustomEvent).detail, ) }) await server.start() console.log(`PGLite Socket Server started on port ${TEST_PORT}`) connectionConfig = { host: '127.0.0.1', port: TEST_PORT, database: 'postgres', username: 'postgres', password: 'postgres', idle_timeout: 5, connect_timeout: 10, max: 1, } } }) afterAll(async () => { if (!DEBUG_TESTS) { // Stop server if running if (server) { await server.stop() console.log('PGLite Socket Server stopped') } // Close database if (db) { await db.close() console.log('PGLite database closed') } } }) beforeEach(() => { // Create a postgres client instance before each test if (DEBUG_TESTS) { // Direct connection to real PostgreSQL server using URL sql = postgres(DEBUG_TESTS_REAL_SERVER, { idle_timeout: 5, connect_timeout: 10, max: 1, }) } else { // Connection to PGLite Socket Server sql = postgres(connectionConfig) } }) afterEach(async () => { // Clean up any tables created in tests try { await sql`DROP TABLE IF EXISTS test_users` } catch (e) { console.error('Error cleaning up tables:', e) } // Disconnect the client after each test if (sql) { await sql.end() } }) if (!DEBUG_LOCAL) { it('should execute a basic SELECT query', async () => { const result = await sql`SELECT 1 as one` expect(result[0].one).toBe(1) }) it('should create a table', async () => { await sql` CREATE TABLE test_users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ` // Verify table exists by querying the schema const tableCheck = await sql` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'test_users' ` expect(tableCheck.length).toBe(1) expect(tableCheck[0].table_name).toBe('test_users') }) it('should insert rows into a table', async () => { // Create table await sql` CREATE TABLE test_users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT ) ` // Insert data const insertResult = await sql` INSERT INTO test_users (name, email) VALUES ('Alice', 'alice@example.com'), ('Bob', 'bob@example.com') RETURNING * ` expect(insertResult.length).toBe(2) expect(insertResult[0].name).toBe('Alice') expect(insertResult[1].name).toBe('Bob') // Verify data is there const count = await sql`SELECT COUNT(*)::int as count FROM test_users` expect(count[0].count).toBe(2) }) it('should update rows in a table', async () => { // Create and populate table await sql` CREATE TABLE test_users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT ) ` await sql` INSERT INTO test_users (name, email) VALUES ('Alice', 'alice@example.com') ` // Update const updateResult = await sql` UPDATE test_users SET email = 'alice.new@example.com' WHERE name = 'Alice' RETURNING * ` expect(updateResult.length).toBe(1) expect(updateResult[0].email).toBe('alice.new@example.com') }) it('should delete rows from a table', async () => { // Create and populate table await sql` CREATE TABLE test_users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT ) ` await sql` INSERT INTO test_users (name, email) VALUES ('Alice', 'alice@example.com'), ('Bob', 'bob@example.com') ` // Delete const deleteResult = await sql` DELETE FROM test_users WHERE name = 'Alice' RETURNING * ` expect(deleteResult.length).toBe(1) expect(deleteResult[0].name).toBe('Alice') // Verify only Bob remains const remaining = await sql`SELECT * FROM test_users` expect(remaining.length).toBe(1) expect(remaining[0].name).toBe('Bob') }) it('should execute operations in a transaction', async () => { // Create table await sql` CREATE TABLE test_users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, balance INTEGER DEFAULT 0 ) ` // Insert initial data await sql` INSERT INTO test_users (name, balance) VALUES ('Alice', 100), ('Bob', 50) ` // Start a transaction and perform operations await sql.begin(async (tx) => { // Deduct from Alice await tx` UPDATE test_users SET balance = balance - 30 WHERE name = 'Alice' ` // Add to Bob await tx` UPDATE test_users SET balance = balance + 30 WHERE name = 'Bob' ` }) // Verify both operations succeeded const users = await sql`SELECT name, balance FROM test_users ORDER BY name` expect(users.length).toBe(2) expect(users[0].name).toBe('Alice') expect(users[0].balance).toBe(70) expect(users[1].name).toBe('Bob') expect(users[1].balance).toBe(80) }) it('should rollback a transaction on ROLLBACK', async () => { // Create table await sql` CREATE TABLE test_users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, balance INTEGER DEFAULT 0 ) ` // Insert initial data await sql` INSERT INTO test_users (name, balance) VALUES ('Alice', 100), ('Bob', 50) ` // Get initial balance const initialResult = await sql` SELECT balance FROM test_users WHERE name = 'Alice' ` const initialBalance = initialResult[0].balance // Start a transaction await sql .begin(async (tx) => { // Deduct from Alice await tx` UPDATE test_users SET balance = balance - 30 WHERE name = 'Alice' ` // Verify balance is changed within transaction const midResult = await tx` SELECT balance FROM test_users WHERE name = 'Alice' ` expect(midResult[0].balance).toBe(70) // Explicitly roll back (cancel) the transaction throw new Error('Triggering rollback') }) .catch(() => { // Expected error to trigger rollback console.log('Transaction was rolled back as expected') }) // Verify balance wasn't changed after rollback const finalResult = await sql` SELECT balance FROM test_users WHERE name = 'Alice' ` expect(finalResult[0].balance).toBe(initialBalance) }) it('should rollback a transaction on error', async () => { // Create table await sql` CREATE TABLE test_users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, balance INTEGER DEFAULT 0 ) ` // Insert initial data await sql` INSERT INTO test_users (name, balance) VALUES ('Alice', 100), ('Bob', 50) ` // Start a transaction that will fail try { await sql.begin(async (tx) => { // Deduct from Alice await tx` UPDATE test_users SET balance = balance - 30 WHERE name = 'Alice' ` // This will trigger an error await tx` UPDATE test_users_nonexistent SET balance = balance + 30 WHERE name = 'Bob' ` }) } catch (error) { // Expected to fail } // Verify Alice's balance was not changed due to rollback const users = await sql`SELECT name, balance FROM test_users ORDER BY name` expect(users.length).toBe(2) expect(users[0].name).toBe('Alice') expect(users[0].balance).toBe(100) // Should remain 100 after rollback }) it('should handle a syntax error', async () => { // Expect syntax error let errorMessage = '' try { await sql`THIS IS NOT VALID SQL;` } catch (error) { errorMessage = (error as Error).message } expect(errorMessage).not.toBe('') expect(errorMessage.toLowerCase()).toContain('syntax error') }) it('should support cursor-based pagination', async () => { // Create a test table with many rows await sql` CREATE TABLE test_users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, value INTEGER ) ` // Insert 100 rows using generate_series (server-side generation) await sql` INSERT INTO test_users (name, value) SELECT 'User ' || i as name, i as value FROM generate_series(1, 100) as i ` // Use a cursor to read data in smaller chunks const chunkSize = 10 let results: any[] = [] let page = 0 // Use a transaction for cursor operations (cursors must be in transactions) await sql.begin(async (tx) => { // Declare a cursor await tx`DECLARE user_cursor CURSOR FOR SELECT * FROM test_users ORDER BY id` let hasMoreData = true while (hasMoreData) { // Fetch a batch of results const chunk = await tx`FETCH 10 FROM user_cursor` // If no rows returned, we're done if (chunk.length === 0) { hasMoreData = false continue } // Process this chunk page++ // Add to our results array results = [...results, ...chunk] // Verify each chunk has correct data (except possibly the last one) if (chunk.length === chunkSize) { expect(chunk.length).toBe(chunkSize) expect(chunk[0].id).toBe((page - 1) * chunkSize + 1) } } // Close the cursor await tx`CLOSE user_cursor` }) // Verify we got all 100 records expect(results.length).toBe(100) expect(results[0].name).toBe('User 1') expect(results[99].name).toBe('User 100') // Verify we received the expected number of pages expect(page).toBe(Math.ceil(100 / chunkSize)) }) } else { it('should support LISTEN/NOTIFY for pub/sub messaging', async () => { // Create a promise that will resolve when the notification is received let receivedPayload = '' const notificationPromise = new Promise<void>((resolve) => { // Set up listener for the 'test_channel' notification sql.listen('test_channel', (data) => { receivedPayload = data resolve() }) }) // Small delay to ensure listener is set up // await new Promise((resolve) => setTimeout(resolve, 100)) // Send a notification on the same connection await sql`NOTIFY test_channel, 'Hello from PGlite!'` // Wait for the notification to be received await notificationPromise // Verify the notification was received with the correct payload expect(receivedPayload).toBe('Hello from PGlite!') }) } }) })