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 errors
const char* error = chdb_result_error(result);
if (error) {
std::cerr << "Query error: " << error << "\n";
return;
}
// Process results
char* 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 data
std::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 separator
std::string header_sep = "+";
for (size_t width : col_widths) {
header_sep += std::string(width + 2, '-') + "+";
}
std::cout << header_sep << "\n";
// Print header
std::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 rows
for (size_t i = 0; i < std::min(print_count / 2, total_rows); ++i) {
print_row(rows[i], col_widths);
}
// Print ellipsis if truncated
if (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 truncated
if (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 truncated
for (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 names
void 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 empty
if (!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 whitespace
input.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 history
history.push_back(input);
// Handle meta commands
if (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 query
chdb_result* result = execute_query(input);
if (!result) {
std::cerr << "Query execution failed\n";
continue;
}
// For Pretty format, just print the result directly
if (current_format.substr(0, 6) == "Pretty" || current_format == "Vertical" || current_format == "Markdown") {
print_result(result);
} else {
// For other formats, parse and display as table
char* 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 alias
current_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)