@queryleaf/postgres-server
Version:
PostgreSQL wire-compatible server for QueryLeaf
369 lines (301 loc) • 11.5 kB
text/typescript
import { MongoClient } from 'mongodb';
import { GenericContainer } from 'testcontainers';
import debug from 'debug';
import { createConnection, Socket } from 'net';
import portfinder from 'portfinder';
import { createServer, Server } from 'net';
const log = debug('queryleaf:test:direct');
// Mock QueryLeaf class for testing
class MockQueryLeaf {
constructor(public client: any, public dbName: string) {}
execute(query: string): any[] {
log(`Mock executing query: ${query}`);
if (query.includes('test')) {
return [{ test: 'success' }];
}
return [];
}
getDatabase() {
return this.dbName;
}
}
// Function to create raw PostgreSQL startup message
function createStartupMessage(): Buffer {
// Format: int32 length + int32 protocol + params
const buffer = Buffer.alloc(40);
// Length (includes self)
buffer.writeInt32BE(40, 0);
// Protocol version 3.0
buffer.writeInt32BE(196608, 4);
// User param
buffer.write('user', 8, 'utf8');
buffer[12] = 0; // null terminator
buffer.write('test', 13, 'utf8');
buffer[17] = 0; // null terminator
// Database param
buffer.write('database', 18, 'utf8');
buffer[26] = 0; // null terminator
buffer.write('test_db', 27, 'utf8');
buffer[34] = 0; // null terminator
buffer[35] = 0; // extra null terminator for end of params
return buffer;
}
// Function to create a simple query message
function createQueryMessage(query: string): Buffer {
// Format: 'Q' + int32 length + string + null terminator
const queryBuffer = Buffer.from(query + '\0', 'utf8');
const length = 4 + queryBuffer.length; // Length field + query text with null terminator
const buffer = Buffer.alloc(1 + length);
buffer.write('Q', 0, 'utf8'); // Message type
buffer.writeInt32BE(length, 1); // Length (includes self, not type)
queryBuffer.copy(buffer, 5); // Copy query text with null terminator
return buffer;
}
// Create an authentication OK response
function createAuthOkResponse(): Buffer {
// R message type + length + auth type (0 = OK)
const buffer = Buffer.alloc(9);
buffer.write('R', 0);
buffer.writeInt32BE(8, 1); // Message length (includes self)
buffer.writeInt32BE(0, 5); // Auth type 0 = OK
return buffer;
}
// Create parameter status message
function createParameterStatus(name: string, value: string): Buffer {
const nameBuffer = Buffer.from(name + '\0', 'utf8');
const valueBuffer = Buffer.from(value + '\0', 'utf8');
const length = 4 + nameBuffer.length + valueBuffer.length;
const buffer = Buffer.alloc(1 + length);
buffer.write('S', 0);
buffer.writeInt32BE(length, 1);
nameBuffer.copy(buffer, 5);
valueBuffer.copy(buffer, 5 + nameBuffer.length);
return buffer;
}
// Create backend key data
function createBackendKeyData(pid: number, key: number): Buffer {
const buffer = Buffer.alloc(13);
buffer.write('K', 0);
buffer.writeInt32BE(12, 1); // Length
buffer.writeInt32BE(pid, 5); // Process ID
buffer.writeInt32BE(key, 9); // Secret key
return buffer;
}
// Create ready for query message
function createReadyForQuery(status: string): Buffer {
const buffer = Buffer.alloc(6);
buffer.write('Z', 0);
buffer.writeInt32BE(5, 1); // Length
buffer.write(status, 5, 1); // Status (I = idle, T = transaction, E = error)
return buffer;
}
// Create row description message
function createRowDescription(fields: string[]): Buffer {
// Start with a bigger buffer to be safe
const buffer = Buffer.alloc(1000);
let offset = 0;
// Message type
buffer.write('T', offset++);
// Skip length for now
offset += 4;
// Field count
buffer.writeInt16BE(fields.length, offset);
offset += 2;
// Fields
for (const field of fields) {
// Field name
const bytesWritten = buffer.write(field, offset);
offset += bytesWritten;
buffer[offset++] = 0; // Null terminator
// Table OID (4), Column number (2), Data type OID (4), Data type size (2),
// Type modifier (4), Format code (2)
buffer.writeInt32BE(0, offset); // Table OID
offset += 4;
buffer.writeInt16BE(0, offset); // Column number
offset += 2;
buffer.writeInt32BE(25, offset); // Data type OID (25 = TEXT)
offset += 4;
buffer.writeInt16BE(-1, offset); // Data type size
offset += 2;
buffer.writeInt32BE(-1, offset); // Type modifier
offset += 4;
buffer.writeInt16BE(0, offset); // Format code (0 = text)
offset += 2;
}
// Now write the length
buffer.writeInt32BE(offset - 1, 1);
// Return a slice of the buffer with just the data we need
return buffer.slice(0, offset);
}
// Create data row message
function createDataRow(values: string[]): Buffer {
// Use a large buffer to be safe
const buffer = Buffer.alloc(1000);
let offset = 0;
// Message type
buffer.write('D', offset++);
// Skip length for now
offset += 4;
// Column count
buffer.writeInt16BE(values.length, offset);
offset += 2;
// Values
for (const value of values) {
const valueBuffer = Buffer.from(value, 'utf8');
buffer.writeInt32BE(valueBuffer.length, offset);
offset += 4;
valueBuffer.copy(buffer, offset);
offset += valueBuffer.length;
}
// Now write the length
buffer.writeInt32BE(offset - 1, 1);
// Return a slice of the buffer with just the data we need
return buffer.slice(0, offset);
}
// Create command complete message
function createCommandComplete(tag: string): Buffer {
const tagBuffer = Buffer.from(tag + '\0', 'utf8');
const length = 4 + tagBuffer.length;
const buffer = Buffer.alloc(1 + length);
buffer.write('C', 0);
buffer.writeInt32BE(length, 1);
tagBuffer.copy(buffer, 5);
return buffer;
}
// Main function to run the example
async function runProtocolExample() {
log('Starting protocol test...');
// Get a free port
const port = await portfinder.getPortPromise({
port: 5432,
stopPort: 6000
});
log(`Using port ${port}`);
let server: Server;
// Create and start server
server = await new Promise((resolve) => {
// Create a simple TCP server that mimics PostgreSQL protocol
const srv = createServer((socket) => {
log(`New connection from ${socket.remoteAddress}:${socket.remotePort}`);
// Handle client messages
socket.on('data', (data) => {
log(`Received data of length ${data.length}, first few bytes: ${data.slice(0, Math.min(10, data.length)).toString('hex')}`);
// Check if it's a startup message
if (data.length >= 4 && data.readUInt32BE(0) === 196608) {
log('Received startup message');
// Respond with authentication OK
socket.write(createAuthOkResponse());
// Send parameter status messages
socket.write(createParameterStatus('server_version', '14.0'));
socket.write(createParameterStatus('client_encoding', 'UTF8'));
// Send backend key data
socket.write(createBackendKeyData(12345, 67890));
// Send ready for query
socket.write(createReadyForQuery('I'));
}
// Check if it's a query message (starts with 'Q')
else if (data.length >= 1 && data[0] === 81) { // 81 = 'Q'
try {
// Extract query text
const messageLength = data.readInt32BE(1);
const queryText = data.toString('utf8', 5, data.length - 1);
log(`Received query: ${queryText}`);
// Respond to test query
if (queryText.toLowerCase().includes('test')) {
// Send row description
socket.write(createRowDescription(['test']));
// Send data row
socket.write(createDataRow(['success']));
// Send command complete
socket.write(createCommandComplete('SELECT 1'));
// Send ready for query
socket.write(createReadyForQuery('I'));
} else {
// Send empty result
socket.write(createRowDescription([]));
socket.write(createCommandComplete('SELECT 0'));
socket.write(createReadyForQuery('I'));
}
} catch (err) {
log(`Error processing query: ${err instanceof Error ? err.message : String(err)}`);
}
}
});
socket.on('error', (err) => {
log(`Socket error: ${err.message}`);
});
socket.on('close', () => {
log(`Connection closed from ${socket.remoteAddress}:${socket.remotePort}`);
});
});
srv.listen(port, '127.0.0.1', () => {
log(`Server listening on 127.0.0.1:${port}`);
resolve(srv);
});
});
try {
// Create a client connection
log('Creating client connection...');
const client = new Socket();
// Store received data for assertions
const receivedData: Buffer[] = [];
// Set up data handler
client.on('data', (data) => {
log(`Client received data (${data.length} bytes): ${data.slice(0, Math.min(20, data.length)).toString('hex')}`);
receivedData.push(data);
});
// Connect to server
await new Promise<void>((resolve, reject) => {
client.connect(port, '127.0.0.1', () => {
log('Client connected to server');
resolve();
});
client.on('error', (err) => {
log(`Client error: ${err.message}`);
reject(err);
});
});
// Send startup message
log('Sending startup message...');
const startupMessage = createStartupMessage();
client.write(startupMessage);
// Wait for response
await new Promise(resolve => setTimeout(resolve, 1000));
// Send query
log('Sending query message...');
const queryMessage = createQueryMessage('SELECT test');
client.write(queryMessage);
// Wait for response
await new Promise(resolve => setTimeout(resolve, 1000));
// Close connection
log('Closing client connection...');
client.end();
// Wait a bit for everything to complete
await new Promise(resolve => setTimeout(resolve, 500));
// Check that we received responses
if (receivedData.length === 0) {
throw new Error('No data received from server');
}
// Convert all received data to string for easier inspection
const allDataStr = Buffer.concat(receivedData).toString('hex');
log(`All received data: ${allDataStr}`);
// Since the binary protocol might include these characters in various ways,
// we'll just verify we got a response with substantial data
if (allDataStr.length <= 20) {
throw new Error('Received data too short');
}
log('Basic protocol communication verified successfully');
log('Example completed successfully');
} finally {
// Cleanup
server.close();
log('Server closed');
}
}
// Run the example directly when executed
if (require.main === module) {
runProtocolExample().catch(err => {
console.error('Example failed:', err);
process.exit(1);
});
}