From 3ac462f455a1838cdd6fb6660648d93a7bc4ed71 Mon Sep 17 00:00:00 2001 From: Elias Bachaalany Date: Tue, 23 Jun 2026 01:59:16 -0500 Subject: [PATCH] feat(http): add ?format=text|csv|tsv output option The /query endpoint defaults to JSON (canonical machine format); add an opt-in query-string 'format' for direct terminal/curl/unix-pipe use: text -> text/plain (ASCII table, via script_result_to_text) csv -> text/csv (RFC 4180) tsv -> text/tab-separated-values Unknown/absent format falls back to JSON. /help and README document it, and the README Response Format example is corrected to the real script envelope. Depends on libxsql script_result_to_csv/tsv (0xeb/libxsql#4). --- README.md | 21 +++++++++++++++++++-- src/common/bnsql_http_routes.hpp | 24 +++++++++++++++++++++--- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bcd8775..3194c6b 100644 --- a/README.md +++ b/README.md @@ -208,13 +208,30 @@ curl -X POST http://localhost:8080/query \ # Check status curl http://localhost:8080/status + +# Plain-text / CSV / TSV output for terminals and unix pipes +curl -X POST "http://localhost:8080/query?format=text" -d "SELECT name, size FROM funcs LIMIT 5" +curl -X POST "http://localhost:8080/query?format=csv" -d "SELECT name, size FROM funcs LIMIT 5" ``` -**Response Format:** +**Response Format:** JSON by default — a script envelope (single statement = array of one): ```json -{"success": true, "columns": ["name", "size"], "rows": [["main", "500"]], "row_count": 1} +{"success": true, "statement_count": 1, + "results": [{"statement_index": 0, "success": true, + "columns": ["name", "size"], "rows": [["_main", "2851"]], + "row_count": 1, "elapsed_ms": 8, "error": null}], + "row_count_total": 1, "elapsed_ms_total": 8, "first_error_index": null} ``` +**Output formats** (query-string `format=`, default `json`): + +| `format` | Content-Type | Use | +|----------|--------------|-----| +| `json` (default) | `application/json` | Programmatic use — **agents should consume this**, not a reformatted view | +| `text` | `text/plain` | Ready-to-read ASCII table for terminals | +| `csv` | `text/csv` | Spreadsheets / RFC-4180 CSV | +| `tsv` | `text/tab-separated-values` | Unix pipes (`cut`/`awk`/`sort`) | + ### MCP Server (Model Context Protocol) For integration with Claude Desktop, Cursor, and other MCP-compatible AI tools: diff --git a/src/common/bnsql_http_routes.hpp b/src/common/bnsql_http_routes.hpp index 22b0cd2..d18bfcf 100644 --- a/src/common/bnsql_http_routes.hpp +++ b/src/common/bnsql_http_routes.hpp @@ -181,7 +181,9 @@ Response Format (canonical script envelope, single = array of one): Splitter failure (e.g. unterminated quote): {"success": false, "statement_count": 0, "results": [], "parse_error": "", ...} - Options (query string): continue_on_error=1, include_sql=1 + Options (query string): continue_on_error=1, include_sql=1, + format=json|text|csv|tsv (default json; text/csv/tsv + are for terminal/pipe use, agents should consume json) Authentication (if enabled): Header: Authorization: Bearer @@ -322,6 +324,13 @@ inline void setup_http_routes( if (incl_it != req.params.end() && incl_it->second == "1") { opts.include_sql = true; } + // Optional output format (default json). text/csv/tsv are for direct + // terminal/curl use; json stays the canonical machine format. + std::string format = "json"; + auto fmt_it = req.params.find("format"); + if (fmt_it != req.params.end() && !fmt_it->second.empty()) { + format = fmt_it->second; + } auto script = xsql::run_script(sql, opts, [&](const std::string& stmt, xsql::ScriptStatementResult& out) { @@ -338,8 +347,17 @@ inline void setup_http_routes( query_mutex->unlock(); queue_count->fetch_sub(1); - res.set_content(xsql::script_result_to_json(script, opts.include_sql), - "application/json"); + if (format == "text") { + res.set_content(xsql::script_result_to_text(script), "text/plain"); + } else if (format == "csv") { + res.set_content(xsql::script_result_to_csv(script), "text/csv"); + } else if (format == "tsv") { + res.set_content(xsql::script_result_to_tsv(script), + "text/tab-separated-values"); + } else { + res.set_content(xsql::script_result_to_json(script, opts.include_sql), + "application/json"); + } }); // GET /status - Health check with runtime settings