POST /v1/query/stream — execute SQL and return results as NDJSON (newline-delimited JSON). Each result row is a separate JSON line terminated by `\n`. Content-Type: application/x-ndjson This is suitable for streaming large result sets without buffering the entire response. Clients can process each line as it arrives.
(
State(state): State<AppState>,
headers: HeaderMap,
QueryParams(db_param): QueryParams<DatabaseQueryParam>,
axum::Json(body): axum::Json<crate::control::server::http::types::HttpQuery
| 323 | /// This is suitable for streaming large result sets without buffering |
| 324 | /// the entire response. Clients can process each line as it arrives. |
| 325 | pub async fn query_ndjson( |
| 326 | State(state): State<AppState>, |
| 327 | headers: HeaderMap, |
| 328 | QueryParams(db_param): QueryParams<DatabaseQueryParam>, |
| 329 | axum::Json(body): axum::Json<crate::control::server::http::types::HttpQueryStreamRequest>, |
| 330 | ) -> impl IntoResponse { |
| 331 | use axum::response::Response; |
| 332 | |
| 333 | let identity = match resolve_identity(&headers, &state, "http") { |
| 334 | Ok(id) => id, |
| 335 | Err(e) => return e.into_response(), |
| 336 | }; |
| 337 | let database_id = match resolve_database_id(&headers, &db_param, &state) { |
| 338 | Ok(id) => id, |
| 339 | Err(e) => return e.into_response(), |
| 340 | }; |
| 341 | |
| 342 | let sql = body.sql.trim(); |
| 343 | if sql.is_empty() { |
| 344 | return (StatusCode::BAD_REQUEST, "empty SQL").into_response(); |
| 345 | } |
| 346 | |
| 347 | let tenant_id = identity.tenant_id; |
| 348 | |
| 349 | // Quota enforcement — reject before any planning or dispatch. |
| 350 | if let Err(e) = state.shared.check_tenant_quota(tenant_id) { |
| 351 | let body = serde_json::json!({ "error": e.to_string() }); |
| 352 | return Response::builder() |
| 353 | .status(StatusCode::TOO_MANY_REQUESTS) |
| 354 | .header("Retry-After", "1") |
| 355 | .header("Content-Type", "application/json") |
| 356 | .body(axum::body::Body::from(body.to_string())) |
| 357 | .unwrap_or_else(|_| { |
| 358 | (StatusCode::INTERNAL_SERVER_ERROR, "encoding error").into_response() |
| 359 | }); |
| 360 | } |
| 361 | |
| 362 | let query_ctx = &state.query_ctx; |
| 363 | |
| 364 | let auth_ctx = crate::control::server::session_auth::build_auth_context(&identity); |
| 365 | let perm_cache = state.shared.permission_cache.read().await; |
| 366 | let sec = crate::control::planner::context::PlanSecurityContext { |
| 367 | identity: &identity, |
| 368 | auth: &auth_ctx, |
| 369 | rls_store: &state.shared.rls, |
| 370 | permissions: &state.shared.permissions, |
| 371 | roles: &state.shared.roles, |
| 372 | permission_cache: Some(&*perm_cache), |
| 373 | }; |
| 374 | let tasks = match query_ctx |
| 375 | .plan_sql_with_rls(sql, tenant_id, database_id, &sec) |
| 376 | .await |
| 377 | { |
| 378 | Ok(t) => t, |
| 379 | Err(e) => return (StatusCode::BAD_REQUEST, e.to_string()).into_response(), |
| 380 | }; |
| 381 | |
| 382 | state.shared.tenant_request_start(tenant_id); |
nothing calls this directly
no test coverage detected