clickhouse-mcp
Version:
An MCP server for ClickHouse in TypeScript
640 lines • 21.7 kB
JavaScript
// import * as fs from "fs";
// import * as path from "path";
// import * as os from "os";
// import { v4 as uuidv4 } from "uuid";
// import { parse as csvParse } from "csv-parse/sync";
// import { ClickHouseClient } from "@clickhouse/client";
// import { Logger } from "../utils/logger.js";
// import {
// ImportCsvOptions,
// TableSettings,
// ColumnDefinition,
// ExportFormatOptions,
// } from "../models/csvModels.js";
// import {
// checkSchemaInferenceSupport,
// inferSchemaWithClickHouse,
// suggestTableSettings,
// generateCreateTableSQL,
// checkTableExists,
// getTableSchema,
// enhanceColumnType,
// sanitizeIdentifier,
// } from "../clickhouse/schema.js";
// import { quoteIdentifier, executeCommand } from "../clickhouse/client.js";
export {};
// const logger = new Logger("csv-handler");
// // In-memory storage for uploaded CSV files with 30-minute expiration
// interface CsvStorageItem {
// filePath: string;
// filename: string;
// timestamp: number;
// size: number;
// }
// const csvStorage: Record<string, CsvStorageItem> = {};
// /**
// * Store CSV content and return a unique ID for later retrieval.
// *
// * @param content - CSV data as string
// * @param filename - Original filename
// * @returns Unique ID for the stored CSV
// */
// export function storeCSVContent(
// content: string,
// filename: string = "uploaded.csv"
// ): string {
// const uploadId = uuidv4();
// const tempDir = os.tmpdir();
// const filePath = path.join(tempDir, `clickhouse-import-${uploadId}.csv`);
// fs.writeFileSync(filePath, content, { encoding: "utf8" });
// csvStorage[uploadId] = {
// filePath,
// filename,
// timestamp: Date.now(),
// size: content.length,
// };
// logger.info(
// `Stored CSV with ID ${uploadId}, size ${content.length} bytes, path: ${filePath}`
// );
// // Set expiration (cleanup after 30 minutes)
// setTimeout(() => {
// const fileInfo = csvStorage[uploadId];
// if (fileInfo && fs.existsSync(fileInfo.filePath)) {
// try {
// fs.unlinkSync(fileInfo.filePath);
// logger.info(`Removed expired CSV file: ${fileInfo.filePath}`);
// } catch (error) {
// logger.warning(`Failed to delete expired CSV file: ${error}`);
// }
// }
// delete csvStorage[uploadId];
// logger.info(`Removed expired CSV entry with ID ${uploadId}`);
// }, 30 * 60 * 1000); // 30 minutes
// return uploadId;
// }
// /**
// * Retrieve stored CSV content by upload ID.
// *
// * @param uploadId - ID of the stored CSV
// * @returns CSV file path or null if not found/expired
// */
// export function retrieveCSVFilePath(uploadId: string): string | null {
// if (!(uploadId in csvStorage)) {
// logger.warning(`CSV with ID ${uploadId} not found`);
// return null;
// }
// const upload = csvStorage[uploadId];
// // Check if expired (30 minutes)
// if (Date.now() - upload.timestamp > 30 * 60 * 1000) {
// logger.info(`CSV with ID ${uploadId} has expired, removing`);
// if (fs.existsSync(upload.filePath)) {
// try {
// fs.unlinkSync(upload.filePath);
// } catch (error) {
// logger.warning(`Failed to delete expired CSV file: ${error}`);
// }
// }
// delete csvStorage[uploadId];
// return null;
// }
// return upload.filePath;
// }
// /**
// * Analyze CSV data and return structure and type information.
// *
// * @param filePath - Path to CSV file
// * @param options - CSV parsing options
// * @returns Analysis results
// */
// export function analyzeCSV(
// filePath: string,
// options: ImportCsvOptions
// ): {
// header: string[];
// sanitizedHeader: string[];
// totalRows: number;
// sampleRows: any[][];
// columnAnalysis: ColumnDefinition[];
// } {
// // Extract options
// const hasHeader = options.has_header_row;
// const delimiter = options.delimiter;
// const quoteChar = options.quote_char;
// // Read file content
// const csvData = fs.readFileSync(filePath, { encoding: "utf8" });
// // Parse CSV
// const parseOptions = {
// delimiter,
// quote: quoteChar,
// columns: hasHeader,
// skip_empty_lines: true,
// skip_records_with_error: true,
// };
// const records = csvParse(csvData, parseOptions);
// // Get header
// let header: string[];
// let dataRows: any[];
// if (hasHeader) {
// // When columns:true is used, records become an array of objects
// header = Object.keys(records[0] || {});
// dataRows = records;
// } else {
// // For no header, generate column names and records are arrays
// dataRows = records;
// const firstRow = dataRows[0] || [];
// header = Array.from(
// { length: firstRow.length },
// (_, i) => `column_${i + 1}`
// );
// }
// // Create sanitized header
// const sanitizedHeader = header.map((h) => sanitizeIdentifier(h));
// // Sample rows for analysis
// const sampleSize = Math.min(100, dataRows.length);
// const sampleRows: any[][] = [];
// for (let i = 0; i < sampleSize; i++) {
// if (hasHeader) {
// // Convert objects to arrays in the same order as header
// sampleRows.push(header.map((h) => dataRows[i][h]));
// } else {
// sampleRows.push(dataRows[i]);
// }
// }
// // Analyze column types
// const columnAnalysis: ColumnDefinition[] = [];
// for (let i = 0; i < header.length; i++) {
// const columnName = header[i];
// const sanitizedName = sanitizedHeader[i];
// // Extract column values for analysis
// const columnValues: string[] = [];
// for (const row of sampleRows) {
// const value = row[i]?.toString() || "";
// columnValues.push(value);
// }
// // Calculate statistics
// const nonNull = columnValues.filter((v) => v.trim() !== "").length;
// const uniqueValues = new Set(columnValues).size;
// // Infer type (basic implementation, for older ClickHouse)
// const inferredType = inferColumnType(columnValues);
// columnAnalysis.push({
// name: sanitizedName,
// inferred_type: inferredType,
// stats: {
// non_null_percent: sampleRows.length
// ? (nonNull / sampleRows.length) * 100
// : 0,
// unique_percent: sampleRows.length
// ? (uniqueValues / sampleRows.length) * 100
// : 0,
// sample_values: columnValues.slice(0, 5),
// },
// });
// }
// return {
// header,
// sanitizedHeader,
// totalRows: dataRows.length,
// sampleRows: sampleRows.slice(0, 5), // First 5 for preview
// columnAnalysis,
// };
// }
// /**
// * Infer ClickHouse data type from sample values.
// * (Fallback method for older ClickHouse versions)
// *
// * @param values - List of sample values
// * @returns ClickHouse data type
// */
// function inferColumnType(values: string[]): string {
// // Remove empty values
// const nonEmpty = values.filter((v) => v.trim() !== "");
// if (nonEmpty.length === 0) {
// return "String";
// }
// // Check for integers
// const intRegex = /^-?\d+$/;
// const allInt = nonEmpty.every((v) => intRegex.test(v));
// if (allInt) {
// // Determine appropriate integer type
// try {
// const nums = nonEmpty.map((v) => parseInt(v, 10));
// const maxVal = Math.max(...nums);
// const minVal = Math.min(...nums);
// if (minVal >= 0) {
// // Unsigned
// if (maxVal < 256) {
// return "UInt8";
// } else if (maxVal < 65536) {
// return "UInt16";
// } else if (maxVal < 4294967296) {
// return "UInt32";
// } else {
// return "UInt64";
// }
// } else {
// // Signed
// if (minVal >= -128 && maxVal < 128) {
// return "Int8";
// } else if (minVal >= -32768 && maxVal < 32768) {
// return "Int16";
// } else if (minVal >= -2147483648 && maxVal < 2147483648) {
// return "Int32";
// } else {
// return "Int64";
// }
// }
// } catch (err) {
// return "String"; // Fallback if parsing fails
// }
// }
// // Check for floating point
// const floatRegex = /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/;
// const allFloat = nonEmpty.every((v) => floatRegex.test(v));
// if (allFloat) {
// return "Float64";
// }
// // Check for dates (YYYY-MM-DD)
// const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
// const allDate = nonEmpty.every((v) => dateRegex.test(v));
// if (allDate) {
// return "Date";
// }
// // Check for datetimes (YYYY-MM-DD HH:MM:SS)
// const datetimeRegex = /^\d{4}-\d{2}-\d{2}(\s|T)\d{2}:\d{2}:\d{2}$/;
// const allDatetime = nonEmpty.every((v) => datetimeRegex.test(v));
// if (allDatetime) {
// return "DateTime";
// }
// // Check for boolean values
// const boolValues = new Set([
// "true",
// "false",
// "yes",
// "no",
// "1",
// "0",
// "t",
// "f",
// "y",
// "n",
// ]);
// const allBool = nonEmpty.every((v) => boolValues.has(v.toLowerCase()));
// if (allBool && new Set(nonEmpty.map((v) => v.toLowerCase())).size <= 2) {
// return "UInt8"; // ClickHouse doesn't have Boolean, uses UInt8
// }
// // Check for low cardinality strings
// const uniquePercent = (new Set(nonEmpty).size / nonEmpty.length) * 100;
// if (uniquePercent < 30) {
// // If less than 30% unique values
// return "LowCardinality(String)";
// }
// // Default to String
// return "String";
// }
// /**
// * Enhance the schema inferred by ClickHouse with additional optimizations.
// *
// * @param inferredSchema - Schema inferred by ClickHouse
// * @param csv - CSV analysis results
// * @returns Enhanced column definitions
// */
// export function enhanceInferredSchema(
// inferredSchema: ColumnDefinition[],
// csv: ReturnType<typeof analyzeCSV>
// ): ColumnDefinition[] {
// // Map column stats to inferred schema
// const columnStats: Record<string, any> = {};
// for (const col of csv.columnAnalysis) {
// columnStats[col.name] = {
// unique_percent: col.stats?.unique_percent || 0,
// non_null_percent: col.stats?.non_null_percent || 0,
// sample_values: col.stats?.sample_values || [],
// };
// }
// // Enhance each column based on statistics
// return inferredSchema.map((col) => {
// // Get stats for this column
// const stats = columnStats[col.name] || {
// unique_percent: 100,
// non_null_percent: 100,
// sample_values: [],
// };
// // Apply optimizations
// const enhancedType = enhanceColumnType(
// col.inferred_type,
// stats.unique_percent,
// stats.sample_values
// );
// return {
// name: col.name,
// inferred_type: enhancedType,
// default_type: col.default_type,
// default_expression: col.default_expression,
// stats: {
// unique_percent: stats.unique_percent,
// non_null_percent: stats.non_null_percent,
// sample_values: stats.sample_values,
// },
// };
// });
// }
// /**
// * Generate an export query for data retrieval.
// *
// * @param database - Database name
// * @param tableName - Table name
// * @param query - Custom query
// * @param columns - Columns to select
// * @param where - WHERE clause
// * @param limit - Row limit
// * @returns SQL query string
// */
// export function generateExportQuery(
// database: string,
// tableName: string,
// query?: string,
// columns?: string[],
// where?: string,
// limit?: number
// ): string {
// if (query) {
// // Use custom query as-is
// return query;
// }
// // Build query from components
// let sql = "SELECT ";
// // Add columns
// if (columns && columns.length > 0) {
// sql += columns.map((col) => quoteIdentifier(col)).join(", ");
// } else {
// sql += "*";
// }
// // Add FROM clause
// sql += ` FROM ${quoteIdentifier(database)}.${quoteIdentifier(tableName)}`;
// // Add WHERE clause if provided
// if (where) {
// sql += ` WHERE ${where}`;
// }
// // Add LIMIT if provided
// if (limit) {
// sql += ` LIMIT ${limit}`;
// }
// return sql;
// }
// /**
// * Import CSV data into a ClickHouse table.
// *
// * @param client - ClickHouse client
// * @param database - Database name
// * @param tableName - Table name
// * @param csvFilePath - Path to CSV file
// * @param options - Import options
// * @param tableSettings - Table creation settings
// * @returns Import results
// */
// export async function importCSVWithClickHouse(
// client: ClickHouseClient,
// database: string,
// tableName: string,
// csvFilePath: string,
// options: ImportCsvOptions,
// tableSettings?: TableSettings
// ): Promise<any> {
// const startTime = Date.now();
// try {
// // Check schema inference support
// const supportsSchemaInference = await checkSchemaInferenceSupport(client);
// logger.info(
// `ClickHouse schema inference support: ${supportsSchemaInference}`
// );
// // Check if table exists
// const tableExists = await checkTableExists(client, database, tableName);
// // If table doesn't exist, we need to create it
// if (!tableExists) {
// let schema: ColumnDefinition[];
// // Use ClickHouse's schema inference if supported
// if (supportsSchemaInference) {
// schema = await inferSchemaWithClickHouse(client, csvFilePath, options);
// // Analyze CSV to get column statistics
// const csvAnalysis = analyzeCSV(csvFilePath, options);
// // Enhance the inferred schema
// schema = enhanceInferredSchema(schema, csvAnalysis);
// } else {
// // Fall back to manual inference
// const csvAnalysis = analyzeCSV(csvFilePath, options);
// schema = csvAnalysis.columnAnalysis;
// }
// // Return schema if auto_create is false
// if (!options.auto_create_table) {
// // Suggest table settings
// const suggestedSettings = suggestTableSettings(schema, tableSettings);
// // Generate CREATE TABLE SQL
// const proposedSql = generateCreateTableSQL(
// database,
// tableName,
// schema,
// suggestedSettings
// );
// return {
// status: "table_needed",
// message: `Table ${database}.${tableName} does not exist. Schema analyzed successfully.`,
// schema,
// proposed_sql: proposedSql,
// suggested_settings: suggestedSettings,
// csv_file_path: csvFilePath,
// supports_schema_inference: supportsSchemaInference,
// };
// }
// // Create the table with the inferred/enhanced schema
// const suggestedSettings = suggestTableSettings(schema, tableSettings);
// const createSql = generateCreateTableSQL(
// database,
// tableName,
// schema,
// suggestedSettings
// );
// logger.info(`Creating table with SQL: ${createSql}`);
// await executeCommand(createSql);
// // Exit early if create_only option is set
// if (options.create_only) {
// return {
// status: "success",
// message: `Table ${database}.${tableName} created successfully. Import skipped as requested.`,
// create_sql: createSql,
// schema,
// suggested_settings: suggestedSettings,
// csv_file_path: csvFilePath,
// execution_time_seconds: (Date.now() - startTime) / 1000,
// };
// }
// }
// // Get table schema
// const tableSchema = await getTableSchema(client, database, tableName);
// // Dry run option - don't actually import
// if (options.dry_run) {
// return {
// status: "dry_run",
// message: `Dry run completed for ${database}.${tableName}`,
// table_schema: tableSchema,
// csv_file_path: csvFilePath,
// execution_time_seconds: (Date.now() - startTime) / 1000,
// };
// }
// // Import data using ClickHouse's native file loading
// const csvFormat = options.has_header_row ? "CSVWithNames" : "CSV";
// // Add format settings for delimiter and quotes
// const formatSettings: string[] = [];
// if (options.delimiter && options.delimiter !== ",") {
// formatSettings.push(`format_csv_delimiter = '${options.delimiter}'`);
// }
// if (options.quote_char && options.quote_char !== '"') {
// formatSettings.push(`format_csv_quote = '${options.quote_char}'`);
// }
// const settingsClause =
// formatSettings.length > 0 ? `SETTINGS ${formatSettings.join(", ")}` : "";
// // Construct the import query
// const importQuery = `
// INSERT INTO ${quoteIdentifier(database)}.${quoteIdentifier(tableName)}
// FROM INFILE '${csvFilePath}'
// FORMAT ${csvFormat}
// ${settingsClause}
// `;
// logger.info(`Executing import query: ${importQuery}`);
// await executeCommand(importQuery);
// // Get row count after import
// const countResult = await client.query({
// query: `SELECT count() AS count FROM ${quoteIdentifier(
// database
// )}.${quoteIdentifier(tableName)}`,
// format: "JSONEachRow",
// });
// const rowCount =
// countResult.rows.length > 0 ? (countResult.rows[0] as any).count : 0;
// // Calculate execution time
// const executionTime = (Date.now() - startTime) / 1000;
// return {
// status: "success",
// message: `Successfully imported data into ${database}.${tableName} using native ClickHouse mechanisms`,
// row_count: rowCount,
// table_schema: tableSchema,
// csv_file_path: csvFilePath,
// execution_time_seconds: executionTime,
// method: "clickhouse_native_import",
// };
// } catch (error) {
// logger.error(`Error in importCSVWithClickHouse: ${error}`);
// return {
// status: "error",
// message: `Import failed: ${error}`,
// csv_file_path: csvFilePath,
// execution_time_seconds: (Date.now() - startTime) / 1000,
// };
// }
// }
// /**
// * Export data from ClickHouse to CSV using native formatting.
// *
// * @param client - ClickHouse client
// * @param database - Database name
// * @param tableName - Table name
// * @param query - Custom query
// * @param columns - Columns to export
// * @param where - WHERE clause
// * @param limit - Row limit
// * @param formatOptions - Format options
// * @returns Exported data, MIME type, and stats
// */
// export async function exportDataToCsvDirect(
// client: ClickHouseClient,
// database: string,
// tableName: string,
// query?: string,
// columns?: string[],
// where?: string,
// limit?: number,
// formatOptions?: ExportFormatOptions
// ): Promise<[string, string, Record<string, any>]> {
// const startTime = Date.now();
// // Use defaults for format options
// const options = formatOptions || {
// format: "CSV",
// delimiter: ",",
// with_names: true,
// with_types: false,
// quote_char: '"',
// };
// // Generate the export query
// const exportQuery = generateExportQuery(
// database,
// tableName,
// query,
// columns,
// where,
// limit
// );
// // Get format options
// const formatType = options.format.toUpperCase();
// const withNames = options.with_names;
// // Determine output format and MIME type
// let outputFormat: string;
// let mimeType: string;
// let settings: Record<string, any> = {};
// if (formatType === "CSV") {
// outputFormat = withNames ? "CSVWithNames" : "CSV";
// settings = {
// format_csv_delimiter: options.delimiter || ",",
// };
// mimeType = "text/csv";
// } else if (formatType === "TSV") {
// outputFormat = withNames ? "TSVWithNames" : "TSV";
// mimeType = "text/tab-separated-values";
// } else if (formatType === "JSONEACHROW") {
// outputFormat = "JSONEachRow";
// mimeType = "application/json";
// } else {
// // Default to CSV
// outputFormat = "CSVWithNames";
// mimeType = "text/csv";
// }
// // Add FORMAT clause
// const queryWithFormat = `${exportQuery} FORMAT ${outputFormat}`;
// // Execute query with format
// logger.info(`Executing export query: ${queryWithFormat}`);
// try {
// // Execute query directly
// const result = await client.query({
// query: queryWithFormat,
// format: "raw",
// // @ts-ignore - settings may not be in type definition but works in practice
// settings,
// });
// const data = result.toString();
// // Calculate stats
// const executionTime = (Date.now() - startTime) / 1000;
// // Count rows (rough estimate by counting newlines)
// let rowCount = (data.match(/\n/g) || []).length;
// if (withNames && rowCount > 0) {
// rowCount -= 1; // Subtract header row
// }
// const stats = {
// status: "success",
// method: "clickhouse_native_export",
// row_count: rowCount,
// execution_time_seconds: executionTime,
// format: outputFormat,
// };
// return [data, mimeType, stats];
// } catch (error) {
// logger.error(`Error in direct export: ${error}`);
// return [
// `Export error: ${error}`,
// "text/plain",
// {
// status: "error",
// method: "clickhouse_native_export",
// error_message: String(error),
// },
// ];
// }
// }
//# sourceMappingURL=csvHandler.js.map