POST /v1/query — execute a SQL/DDL statement. Request body: `{ "sql": "..." }` Response: `{ "status": "ok", "rows": [...] }` or `{ "error": "..." }` Database context (optional): - `X-NodeDB-Database: ` header (highest priority) - `?database= ` query parameter (fallback)
(
headers: HeaderMap,
QueryParams(db_param): QueryParams<DatabaseQueryParam>,
State(state): State<AppState>,
axum::Json(body): axum::Json<HttpQueryRequest>,
)
| 80 | /// - `X-NodeDB-Database: <name>` header (highest priority) |
| 81 | /// - `?database=<name>` query parameter (fallback) |
| 82 | pub async fn query( |
| 83 | headers: HeaderMap, |
| 84 | QueryParams(db_param): QueryParams<DatabaseQueryParam>, |
| 85 | State(state): State<AppState>, |
| 86 | axum::Json(body): axum::Json<HttpQueryRequest>, |
| 87 | ) -> Result<impl IntoResponse, ApiError> { |
| 88 | let identity = resolve_identity(&headers, &state, "http")?; |
| 89 | let database_id = resolve_database_id(&headers, &db_param, &state)?; |
| 90 | let trace_id = crate::control::trace_context::extract_from_headers(&headers); |
| 91 | |
| 92 | let sql = body.sql.as_str(); |
| 93 | |
| 94 | // Try DDL commands first (same as pgwire handler). |
| 95 | if let Some(result) = crate::control::server::pgwire::ddl::dispatch( |
| 96 | &state.shared, |
| 97 | &identity, |
| 98 | sql.trim(), |
| 99 | database_id, |
| 100 | ) |
| 101 | .await |
| 102 | { |
| 103 | return match result { |
| 104 | Ok(responses) => { |
| 105 | let json_rows = responses_to_json(responses); |
| 106 | Ok(axum::Json(HttpQueryResponse::ok(json_rows))) |
| 107 | } |
| 108 | Err(e) => Err(ApiError::BadRequest(e.to_string())), |
| 109 | }; |
| 110 | } |
| 111 | |
| 112 | // Extract per-query ON DENY override + plan SQL with RLS injection. |
| 113 | let tenant_id = identity.tenant_id; |
| 114 | |
| 115 | // Quota enforcement — reject before any planning or dispatch. |
| 116 | state |
| 117 | .shared |
| 118 | .check_tenant_quota(tenant_id) |
| 119 | .map_err(|e| ApiError::RateLimited { |
| 120 | message: e.to_string(), |
| 121 | retry_after_secs: 1, |
| 122 | })?; |
| 123 | |
| 124 | let mut auth_ctx = crate::control::server::session_auth::build_auth_context(&identity); |
| 125 | let clean_sql = |
| 126 | crate::control::server::session_auth::extract_and_apply_on_deny(sql, &mut auth_ctx); |
| 127 | let perm_cache = state.shared.permission_cache.read().await; |
| 128 | let sec = crate::control::planner::context::PlanSecurityContext { |
| 129 | identity: &identity, |
| 130 | auth: &auth_ctx, |
| 131 | rls_store: &state.shared.rls, |
| 132 | permissions: &state.shared.permissions, |
| 133 | roles: &state.shared.roles, |
| 134 | permission_cache: Some(&*perm_cache), |
| 135 | }; |
| 136 | let tasks = state |
| 137 | .query_ctx |
| 138 | .plan_sql_with_rls(&clean_sql, tenant_id, database_id, &sec) |
| 139 | .await |
nothing calls this directly
no test coverage detected