UNPKG

@iarayan/ch-orm

Version:

A Developer-First ClickHouse ORM with Powerful CLI Tools

290 lines 10.1 kB
import * as http from "http"; import * as https from "https"; import { URL } from "url"; import { ResultFormat, } from "../types/connection"; import { formatValue } from "../utils/helpers"; /** * Connection class for managing connections to a ClickHouse server * Handles query execution and manages connection state */ export class Connection { /** * Create a new ClickHouse connection * @param config - Connection configuration */ constructor(config) { /** * Default query options */ this.defaultQueryOptions = { format: ResultFormat.JSON, }; // Set default values for configuration this.config = { host: "localhost", port: 8123, database: "default", username: "default", password: "", protocol: "http", timeout: 30000, maxConnections: 10, debug: false, ...config, }; // Initialize the base URL and HTTP module this.baseUrl = `${this.config.protocol}://${this.config.host}:${this.config.port}`; this.httpModule = this.config.protocol === "https" ? https : http; } /** * Execute a raw SQL query * @param sql - SQL query to execute * @param options - Query options * @returns Query result */ async query(sql, options) { // Merge options with defaults const queryOptions = { ...this.defaultQueryOptions, ...options }; // Build URL with query parameters const url = new URL(this.baseUrl); // Set database url.searchParams.append("database", this.config.database); // Add other query parameters if (queryOptions.session_id) { url.searchParams.append("session_id", queryOptions.session_id); } if (queryOptions.timeout_seconds) { url.searchParams.append("timeout_seconds", queryOptions.timeout_seconds.toString()); } if (queryOptions.max_rows_to_read) { url.searchParams.append("max_rows_to_read", queryOptions.max_rows_to_read.toString()); } // Add format url.searchParams.append("default_format", queryOptions.format || ResultFormat.JSON); // Add ClickHouse specific settings if (queryOptions.clickhouse_settings) { Object.entries(queryOptions.clickhouse_settings).forEach(([key, value]) => { url.searchParams.append(key, value.toString()); }); } // Log the query if debug mode is enabled if (this.config.debug) { console.log("ClickHouse Query:", sql); console.log("URL:", url.toString()); } // Execute the HTTP request try { const result = await this.executeRequest(url, sql); // Log the result if debug mode is enabled if (this.config.debug) { console.log("ClickHouse Result:", JSON.stringify(result, null, 2)); } return result; } catch (error) { // Log the error if debug mode is enabled if (this.config.debug) { console.error("ClickHouse Error:", error.message); } throw error; } } /** * Execute a parameterized query with values * @param sql - SQL query with placeholders (?) * @param params - Parameter values * @param options - Query options * @returns Query result */ async execute(sql, params = [], options) { // Replace parameters with formatted values let index = 0; const formattedSql = sql.replace(/\?/g, () => { const value = params[index++]; return formatValue(value); }); return this.query(formattedSql, options); } /** * Execute a simple INSERT query * @param table - Table name * @param data - Data to insert (object or array of objects) * @param options - Query options * @returns Query result */ async insert(table, data, options) { // Convert single object to array const rows = Array.isArray(data) ? data : [data]; if (rows.length === 0) { throw new Error("No data provided for insert"); } // Get column names from the first row const columns = Object.keys(rows[0]); if (columns.length === 0) { throw new Error("No columns found in data"); } // Build the insert query let sql = `INSERT INTO ${table} (${columns.join(", ")}) VALUES `; // Add values for each row const values = rows .map((row) => { const rowValues = columns.map((column) => formatValue(row[column])); return `(${rowValues.join(", ")})`; }) .join(", "); sql += values; return this.query(sql, options); } /** * Execute a HTTP request to ClickHouse * @param url - Request URL * @param body - Request body (SQL query) * @returns Query result */ executeRequest(url, body) { return new Promise((resolve, reject) => { // Request options const options = { method: "POST", timeout: this.config.timeout, auth: `${this.config.username}:${this.config.password}`, }; // Create request const req = this.httpModule.request(url, options, (res) => { let data = ""; // Handle data chunks res.on("data", (chunk) => { data += chunk; }); // Handle end of response res.on("end", () => { // Check for HTTP error if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) { return reject(new Error(`HTTP Error ${res.statusCode}: ${data}`)); } // Check if this is a DDL statement (CREATE, ALTER, DROP, etc.) const isDDL = /^(CREATE|ALTER|DROP|TRUNCATE|RENAME)/i.test(body.trim()); // For DDL statements or empty responses, return success if (isDDL || !data.trim()) { return resolve({ data: [], statistics: { elapsed: 0, rows_read: 0, bytes_read: 0, }, meta: [], }); } try { // Parse response as JSON const result = JSON.parse(data); // Return formatted result resolve({ data: result.data || [], statistics: result.statistics || { elapsed: 0, rows_read: 0, bytes_read: 0, }, meta: result.meta || [], }); } catch (error) { reject(new Error(`Failed to parse ClickHouse response: ${error}`)); } }); }); // Handle request errors req.on("error", (error) => { reject(new Error(`ClickHouse request failed: ${error.message}`)); }); // Handle timeout req.on("timeout", () => { req.destroy(); reject(new Error(`ClickHouse request timed out after ${this.config.timeout}ms`)); }); // Send the query req.write(body); req.end(); }); } /** * Ping the ClickHouse server to test the connection * @returns True if connected successfully */ async ping() { try { await this.query("SELECT 1"); return true; } catch (error) { return false; } } /** * Get information about the ClickHouse server * @returns Server information */ async serverInfo() { const result = await this.query("SELECT * FROM system.build_options"); return result.data; } /** * List databases on the ClickHouse server * @returns Array of database names */ async listDatabases() { const result = await this.query("SHOW DATABASES"); return result.data.map((row) => row.name); } /** * List tables in the current database * @returns Array of table names */ async listTables() { const result = await this.query("SHOW TABLES"); return result.data.map((row) => row.name); } /** * Get table schema information * @param table - Table name * @returns Table schema information */ async describeTable(table) { const result = await this.query(`DESCRIBE TABLE ${table}`); return result.data; } /** * Create a new database * @param database - Database name * @returns Query result */ async createDatabase(database) { return this.query(`CREATE DATABASE IF NOT EXISTS ${database}`); } /** * Drop a database * @param database - Database name * @returns Query result */ async dropDatabase(database) { return this.query(`DROP DATABASE IF EXISTS ${database}`); } /** * Get the current connection configuration * @returns Connection configuration */ getConfig() { return { ...this.config }; } /** * Close the connection and release resources */ async close() { // HTTP connections are stateless, so no cleanup needed return Promise.resolve(); } } //# sourceMappingURL=Connection.js.map