借助DeepSeek编写输出漂亮表格的chdb客户端
chdb是Auxten Wang创作的clickhouse的嵌入式引擎,它支持python、rust和c/c++语言调用动态链接库。
但官方没有给出c/c++语言调用的例子,在网友无敌大泡泡 的C语言示例启发下,我让DeepSeek改写出了一个简易客户端,
提示词如下:
附件是调用chdb动态链接库的c程序示例,请把它改写成支持输出表格(用|分隔各列,-分隔列标题和数据)和循环接收命令的客户端,.timer on/off命令用来开关计时,.exit用来退出,如果结果集大于10行,输出前后共10行,在中间用…表示,并打印实际行数。用c++实现
第一版的程序有个问题,输出表格缺列标题,误把第一行数据填在列标题的位置,反馈给他后,修改还是原样,于是考虑是否调用函数的结果集就是没有表头。结果在rust示例中看到了format.rs包含种类繁多的输出格式,其中有常见的PrettyCompact,把它和头文件一起提供给DeepSeek, 告诉他Pretty开头的格式原样输出即可。DeepSeek还发现format.rs中有CSVWithNames格式,聪明地改用它来获取表头,输出自定义表格。
首先下载动态链接库压缩包, 解压缩到/par目录。
然后编写c++源文件chsql.cpp,也放在/par目录下,最终的代码如下:
#include <iostream>
#include <string>
#include <vector>
#include <chrono>
#include <algorithm>
#include <iomanip>
#include <map>
#include "chdb.h"class ChDBClient {
private:chdb_connection* conn;bool timer_enabled;std::vector<std::string> history;std::string current_format;std::map<std::string, std::string> format_aliases;void initialize_format_aliases() {format_aliases = {{"csv", "CSVWithNames"},{"json", "JSONEachRow"},{"pretty", "Pretty"},{"vertical", "Vertical"},{"markdown", "Markdown"},{"tsv", "TabSeparatedWithNames"},{"table", "Pretty"} // Alias for Pretty};}chdb_result* execute_query(const std::string& query) {auto start = std::chrono::high_resolution_clock::now();chdb_result* result = chdb_query(*conn, query.c_str(), current_format.c_str());auto end = std::chrono::high_resolution_clock::now();if (timer_enabled) {auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);std::cout << "Query executed in " << duration.count() << " ms\n";}return result;}void print_result(chdb_result* result) {// Check for errorsconst char* error = chdb_result_error(result);if (error) {std::cerr << "Query error: " << error << "\n";return;}// Process resultschar* data = chdb_result_buffer(result);size_t data_len = chdb_result_length(result);size_t rows_read = chdb_result_rows_read(result);double elapsed = chdb_result_elapsed(result);if (data_len > 0) {std::string result_data(data, data_len);std::cout << result_data;if (timer_enabled) {std::cout << "\n(" << rows_read << " rows, " << elapsed << " sec)\n";} else if (rows_read > 0) {std::cout << "\n(" << rows_read << " rows)\n";}} else {std::cout << "Query executed successfully. No results returned.\n";}}void print_table(const std::vector<std::vector<std::string>>& rows, const std::vector<std::string>& headers) {if (rows.empty()) return;// Calculate column widths based on both headers and datastd::vector<size_t> col_widths(headers.size(), 0);for (size_t i = 0; i < headers.size(); ++i) {col_widths[i] = headers[i].size();}for (const auto& row : rows) {for (size_t i = 0; i < row.size() && i < col_widths.size(); ++i) {col_widths[i] = std::max(col_widths[i], row[i].size());}}// Print header separatorstd::string header_sep = "+";for (size_t width : col_widths) {header_sep += std::string(width + 2, '-') + "+";}std::cout << header_sep << "\n";// Print headerstd::cout << "|";for (size_t i = 0; i < headers.size(); ++i) {std::cout << " " << std::left << std::setw(col_widths[i]) << headers[i] << " |";}std::cout << "\n" << header_sep << "\n";// Print rows (with potential truncation)size_t total_rows = rows.size();bool truncated = false;size_t print_count = total_rows;if (total_rows > 10) {print_count = 10;truncated = true;}// Print first 5 rowsfor (size_t i = 0; i < std::min(print_count / 2, total_rows); ++i) {print_row(rows[i], col_widths);}// Print ellipsis if truncatedif (truncated) {std::cout << "|";for (size_t i = 0; i < col_widths.size(); ++i) {std::cout << " " << std::left << std::setw(col_widths[i]) << "..." << " |";}std::cout << "\n";}// Print last 5 rows if truncatedif (truncated) {for (size_t i = std::max(total_rows - print_count / 2, print_count / 2); i < total_rows; ++i) {print_row(rows[i], col_widths);}} else {// Print remaining rows if not truncatedfor (size_t i = print_count / 2; i < total_rows; ++i) {print_row(rows[i], col_widths);}}std::cout << header_sep << "\n";if (truncated) {std::cout << "(" << total_rows << " rows total, showing first and last 5 rows)\n";} else if (total_rows > 0) {std::cout << "(" << total_rows << " rows)\n";}}void print_row(const std::vector<std::string>& row, const std::vector<size_t>& col_widths) {std::cout << "|";for (size_t i = 0; i < row.size() && i < col_widths.size(); ++i) {std::cout << " " << std::left << std::setw(col_widths[i]) << row[i] << " |";}std::cout << "\n";}// Parse CSV results and extract headers from column namesvoid parse_csv(const std::string& csv_data, std::vector<std::string>& headers, std::vector<std::vector<std::string>>& rows) {headers.clear();rows.clear();std::vector<std::string> current_row;std::string current_field;bool in_quotes = false;bool is_first_row = true;for (char c : csv_data) {if (c == '"') {in_quotes = !in_quotes;} else if (c == ',' && !in_quotes) {current_row.push_back(current_field);current_field.clear();} else if (c == '\n' && !in_quotes) {current_row.push_back(current_field);if (is_first_row) {headers = current_row;is_first_row = false;} else {rows.push_back(current_row);}current_row.clear();current_field.clear();} else {current_field += c;}}// Add the last field if not emptyif (!current_field.empty()) {current_row.push_back(current_field);}if (!current_row.empty()) {if (is_first_row) {headers = current_row;} else {rows.push_back(current_row);}}}public:ChDBClient() : conn(nullptr), timer_enabled(false), current_format("CSVWithNames") {initialize_format_aliases();}~ChDBClient() {if (conn) {chdb_close_conn(conn);}}bool connect() {char *argv[] = {"chdb_client","--path=:memory:"};int argc = 2;conn = chdb_connect(argc, argv);if (!conn) {std::cerr << "Failed to create database connection\n";return false;}std::cout << "Connected to in-memory database successfully\n";return true;}void run() {std::cout << "chDB client (type '.exit' to quit, '.help' for help)\n";while (true) {std::string input;std::cout << "chdb> ";std::getline(std::cin, input);// Trim whitespaceinput.erase(0, input.find_first_not_of(" \t\n\r\f\v"));input.erase(input.find_last_not_of(" \t\n\r\f\v") + 1);if (input.empty()) continue;// Add to historyhistory.push_back(input);// Handle meta commandsif (input[0] == '.') {if (input == ".exit") {break;} else if (input == ".timer on") {timer_enabled = true;std::cout << "Query timer enabled\n";continue;} else if (input == ".timer off") {timer_enabled = false;std::cout << "Query timer disabled\n";continue;} else if (input.substr(0, 8) == ".format ") {set_format(input.substr(8));continue;} else if (input == ".help") {print_help();continue;} else if (input == ".history") {print_history();continue;} else {std::cerr << "Unknown command: " << input << "\n";continue;}}// Execute SQL querychdb_result* result = execute_query(input);if (!result) {std::cerr << "Query execution failed\n";continue;}// For Pretty format, just print the result directlyif (current_format.substr(0, 6) == "Pretty" || current_format == "Vertical" || current_format == "Markdown") {print_result(result);} else {// For other formats, parse and display as tablechar* data = chdb_result_buffer(result);size_t data_len = chdb_result_length(result);if (data_len > 0) {std::string result_data(data, data_len);std::vector<std::string> headers;std::vector<std::vector<std::string>> rows;parse_csv(result_data, headers, rows);if (!headers.empty()) {print_table(rows, headers);} else {std::cout << result_data;}size_t rows_read = chdb_result_rows_read(result);double elapsed = chdb_result_elapsed(result);if (timer_enabled) {std::cout << "(" << rows_read << " rows, " << elapsed << " sec)\n";} else if (rows_read > 0) {std::cout << "(" << rows_read << " rows)\n";}} else {std::cout << "Query executed successfully. No results returned.\n";}}chdb_destroy_query_result(result);}}void set_format(const std::string& format) {std::string fmt_lower = format;std::transform(fmt_lower.begin(), fmt_lower.end(), fmt_lower.begin(), ::tolower);if (format_aliases.find(fmt_lower) != format_aliases.end()) {current_format = format_aliases[fmt_lower];std::cout << "Output format set to: " << current_format << "\n";} else {// Try to use the format directly if it's not an aliascurrent_format = format;std::cout << "Output format set to: " << current_format << "\n";}}void print_help() {std::cout << "Available commands:\n"<< " .exit - Exit the client\n"<< " .timer on/off - Enable/disable query timing\n"<< " .format <fmt> - Set output format (csv, json, pretty, vertical, markdown, tsv)\n"<< " .history - Show command history\n"<< " .help - Show this help message\n"<< " SQL queries - Execute SQL queries\n";}void print_history() {std::cout << "Command history:\n";for (size_t i = 0; i < history.size(); ++i) {std::cout << " " << i + 1 << ": " << history[i] << "\n";}}
};int main() {ChDBClient client;if (!client.connect()) {return EXIT_FAILURE;}client.run();std::cout << "Goodbye!\n";return EXIT_SUCCESS;
}
编译命令行如下:
export LD_LIBRARY_PATH=/par:/usr/local/lib
export LIBRARY_PATH=/par:/usr/local/lib
g++ chsql4.cpp -O3 -o chsql4 -I . -lchdb
目前的代码还有冗余,比如计时信息和行数输出了两遍,下次慢慢改
./chsql
Connected to in-memory database successfully
chDB client (type '.exit' to quit, '.help' for help)
chdb> .format PrettyCompactMonoBlock
Output format set to: PrettyCompactMonoBlock
chdb> select * from file('foods.csv')limit 4;┌─category───┬─calories─┬─fats_g─┬─sugars_g─┐
1. │ vegetables │ 45 │ 0.5 │ 2 │
2. │ seafood │ 150 │ 5 │ 0 │
3. │ meat │ 100 │ 5 │ 0 │
4. │ fruit │ 60 │ 0 │ 11 │└────────────┴──────────┴────────┴──────────┘(4 rows)
chdb> .timer on
Query timer enabled
chdb> .format CSVWithNames
Output format set to: CSVWithNames
chdb> select * from file('foods.csv');
Query executed in 24 ms
+------------+----------+--------+----------+
| category | calories | fats_g | sugars_g |
+------------+----------+--------+----------+
| vegetables | 45 | 0.5 | 2 |
| seafood | 150 | 5 | 0 |
| meat | 100 | 5 | 0 |
| fruit | 60 | 0 | 11 |
| seafood | 140 | 5 | 1 |
| ... | ... | ... | ... |
| seafood | 130 | 1.5 | 0 |
| fruit | 130 | 0 | 25 |
| meat | 100 | 7 | 0 |
| vegetables | 30 | 0 | 5 |
| fruit | 50 | 0 | 11 |
+------------+----------+--------+----------+
(27 rows total, showing first and last 5 rows)
(27 rows, 0.0232782 sec)