Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 0 additions & 194 deletions src/common/bnsql_http_routes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -209,200 +209,6 @@ inline std::string settings_to_json(const RuntimeSettingsSnapshot& snap) {
return json.str();
}

// Query callback type: takes SQL string, returns QueryResult
using HttpQueryFn = std::function<bnsql::QueryResult(const std::string& sql)>;

/**
* Set up standard BNSQL HTTP routes on an httplib::Server.
*
* @param svr The httplib server to add routes to
* @param query_fn Query callback (thread-safe — caller must serialize if needed)
* @param auth_token Optional auth token (empty = no auth)
* @param port Port number (for display in welcome message)
*/
inline void setup_http_routes(
httplib::Server& svr,
HttpQueryFn query_fn,
const std::string& auth_token,
int port)
{
// Shared state for queue management
auto query_mutex = std::make_shared<std::timed_mutex>();
auto queue_count = std::make_shared<std::atomic<size_t>>(0);
auto query_fn_ptr = std::make_shared<HttpQueryFn>(std::move(query_fn));
auto auth = std::make_shared<std::string>(auth_token);

auto check_auth = [auth](const httplib::Request& req, httplib::Response& res) -> bool {
if (auth->empty()) return true;
std::string token;
if (req.has_header("X-XSQL-Token")) {
token = req.get_header_value("X-XSQL-Token");
} else if (req.has_header("Authorization")) {
const std::string a = req.get_header_value("Authorization");
if (a.rfind("Bearer ", 0) == 0) token = a.substr(7);
}
if (token == *auth) return true;
res.status = 401;
res.set_content("{\"success\":false,\"error\":\"Unauthorized\"}", "application/json");
return false;
};

// GET / - Welcome message
svr.Get("/", [port](const httplib::Request&, httplib::Response& res) {
std::string welcome = "BNSQL HTTP Server\n\n"
"Endpoints:\n"
" GET /help - API documentation\n"
" POST /query - Execute SQL query\n"
" GET /status - Health check\n"
" GET /settings - Runtime settings\n"
" POST /shutdown - Stop server\n\n"
"Example:\n"
" curl http://localhost:" + std::to_string(port) + "/help\n"
" curl -X POST http://localhost:" + std::to_string(port) + "/query -d \"SELECT name FROM funcs LIMIT 5\"\n";
res.set_content(welcome, "text/plain");
});

// GET /help - API documentation (public, no auth)
svr.Get("/help", [](const httplib::Request&, httplib::Response& res) {
res.set_content(http_help_text(), "text/plain");
});

// POST /query - Execute SQL with queue management
svr.Post("/query", [query_fn_ptr, query_mutex, queue_count, check_auth](
const httplib::Request& req, httplib::Response& res)
{
if (!check_auth(req, res)) return;

std::string sql = req.body;
if (sql.empty()) {
res.status = 400;
res.set_content("{\"success\":false,\"error\":\"Empty query\"}", "application/json");
return;
}

auto& settings = runtime_settings();

// Atomic queue admission via compare-exchange
size_t max_queue = settings.max_queue();
size_t current = queue_count->load();
while (true) {
if (max_queue > 0 && current >= max_queue) {
res.status = 503;
res.set_content("{\"success\":false,\"error\":\"Queue full\"}", "application/json");
return;
}
if (queue_count->compare_exchange_weak(current, current + 1))
break;
}

// Try to acquire lock with admission timeout
int admission_ms = settings.queue_admission_timeout_ms();
bool acquired = false;
if (admission_ms <= 0) {
query_mutex->lock();
acquired = true;
} else {
acquired = query_mutex->try_lock_for(std::chrono::milliseconds(admission_ms));
}

if (!acquired) {
queue_count->fetch_sub(1);
res.status = 408;
res.set_content("{\"success\":false,\"error\":\"Queue admission timeout\"}", "application/json");
return;
}

// Multi-statement: split via xsql::run_script and dispatch each
// statement through the supplied single-statement executor. Single
// statement is just array-of-one in the canonical envelope.
xsql::ScriptOptions opts;
auto cont_it = req.params.find("continue_on_error");
if (cont_it != req.params.end() && cont_it->second == "1") {
opts.continue_on_error = true;
}
auto incl_it = req.params.find("include_sql");
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) {
auto r = (*query_fn_ptr)(stmt);
out.columns = r.columns;
out.rows.reserve(r.rows.size());
for (const auto& row : r.rows) {
out.rows.push_back(row.values);
}
out.elapsed_ms = r.elapsed_ms;
out.success = r.success;
out.error = r.error;
});
query_mutex->unlock();
queue_count->fetch_sub(1);

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
svr.Get("/status", [query_fn_ptr, query_mutex, check_auth](const httplib::Request& req, httplib::Response& res) {
if (!check_auth(req, res)) return;

// Use try_lock to avoid blocking status checks
std::unique_lock<std::timed_mutex> lock(*query_mutex, std::defer_lock);
std::string func_count = "?";
if (lock.try_lock_for(std::chrono::seconds(2))) {
auto result = (*query_fn_ptr)("SELECT COUNT(*) FROM funcs");
if (result.success && !result.empty()) {
func_count = result.rows[0][0];
}
}

auto snap = runtime_settings().snapshot();
std::ostringstream json;
json << "{"
<< "\"success\":true"
<< ",\"status\":\"ok\""
<< ",\"tool\":\"bnsql\""
<< ",\"functions\":" << (func_count == "?" ? "null" : func_count)
<< ",\"settings\":" << settings_to_json(snap)
<< "}";
res.set_content(json.str(), "application/json");
});

// GET /settings - Runtime settings
svr.Get("/settings", [check_auth](const httplib::Request& req, httplib::Response& res) {
if (!check_auth(req, res)) return;
auto snap = runtime_settings().snapshot();
res.set_content(settings_to_json(snap), "application/json");
});

// POST /shutdown - Graceful shutdown
svr.Post("/shutdown", [&svr, check_auth](const httplib::Request& req, httplib::Response& res) {
if (!check_auth(req, res)) return;
res.set_content("{\"success\":true,\"message\":\"Shutting down\"}", "application/json");
std::thread([&svr] {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
svr.stop();
}).detach();
});
}

} // namespace bnsql

Expand Down
Loading