1use indexmap::IndexMap;
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10use crate::pagination::Cursor;
11
12#[derive(Debug, Serialize, JsonSchema)]
18#[serde(untagged)]
19pub enum ListEntries {
20 Brief(Vec<String>),
22 Detailed(IndexMap<String, Value>),
24}
25
26impl ListEntries {
27 #[must_use]
29 pub fn len(&self) -> usize {
30 match self {
31 Self::Brief(v) => v.len(),
32 Self::Detailed(m) => m.len(),
33 }
34 }
35
36 #[must_use]
38 pub fn is_empty(&self) -> bool {
39 self.len() == 0
40 }
41
42 #[must_use]
44 pub fn as_brief(&self) -> Option<&[String]> {
45 if let Self::Brief(v) = self { Some(v) } else { None }
46 }
47
48 #[must_use]
50 pub fn as_detailed(&self) -> Option<&IndexMap<String, Value>> {
51 if let Self::Detailed(m) = self { Some(m) } else { None }
52 }
53
54 #[must_use]
56 pub fn into_brief(self) -> Option<Vec<String>> {
57 if let Self::Brief(v) = self { Some(v) } else { None }
58 }
59}
60
61#[derive(Debug, Serialize, JsonSchema)]
63#[serde(rename_all = "camelCase")]
64pub struct ListTablesResponse {
65 pub tables: ListEntries,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub next_cursor: Option<Cursor>,
70}
71
72impl ListTablesResponse {
73 #[must_use]
75 pub fn brief(tables: Vec<String>, next_cursor: Option<Cursor>) -> Self {
76 Self {
77 tables: ListEntries::Brief(tables),
78 next_cursor,
79 }
80 }
81
82 #[must_use]
84 pub fn detailed(tables: IndexMap<String, Value>, next_cursor: Option<Cursor>) -> Self {
85 Self {
86 tables: ListEntries::Detailed(tables),
87 next_cursor,
88 }
89 }
90}
91
92#[derive(Debug, Serialize, JsonSchema)]
94#[serde(rename_all = "camelCase")]
95pub struct MessageResponse {
96 pub message: String,
98}
99
100#[derive(Debug, Default, Deserialize, JsonSchema)]
102#[serde(rename_all = "camelCase")]
103pub struct ListDatabasesRequest {
104 #[serde(default)]
106 pub cursor: Option<Cursor>,
107}
108
109#[derive(Debug, Serialize, JsonSchema)]
111#[serde(rename_all = "camelCase")]
112pub struct ListDatabasesResponse {
113 pub databases: Vec<String>,
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub next_cursor: Option<Cursor>,
118}
119
120#[derive(Debug, Default, Deserialize, JsonSchema)]
122#[serde(rename_all = "camelCase")]
123pub struct CreateDatabaseRequest {
124 pub database: String,
126}
127
128#[derive(Debug, Default, Deserialize, JsonSchema)]
130#[serde(rename_all = "camelCase")]
131pub struct DropDatabaseRequest {
132 pub database: String,
134}
135
136#[derive(Debug, Default, Deserialize, JsonSchema)]
138#[serde(rename_all = "camelCase")]
139pub struct ListViewsRequest {
140 #[serde(default)]
142 pub database: Option<String>,
143 #[serde(default)]
145 pub cursor: Option<Cursor>,
146}
147
148#[derive(Debug, Serialize, JsonSchema)]
150#[serde(rename_all = "camelCase")]
151pub struct ListViewsResponse {
152 pub views: ListEntries,
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub next_cursor: Option<Cursor>,
157}
158
159impl ListViewsResponse {
160 #[must_use]
162 pub fn brief(views: Vec<String>, next_cursor: Option<Cursor>) -> Self {
163 Self {
164 views: ListEntries::Brief(views),
165 next_cursor,
166 }
167 }
168
169 #[must_use]
171 pub fn detailed(views: IndexMap<String, Value>, next_cursor: Option<Cursor>) -> Self {
172 Self {
173 views: ListEntries::Detailed(views),
174 next_cursor,
175 }
176 }
177}
178
179#[derive(Debug, Default, Deserialize, JsonSchema)]
181#[serde(rename_all = "camelCase")]
182pub struct ListTriggersRequest {
183 #[serde(default)]
185 pub database: Option<String>,
186 #[serde(default)]
188 pub cursor: Option<Cursor>,
189 #[serde(default)]
192 pub search: Option<String>,
193 #[serde(default)]
197 pub detailed: bool,
198}
199
200#[derive(Debug, Serialize, JsonSchema)]
202#[serde(rename_all = "camelCase")]
203pub struct ListTriggersResponse {
204 pub triggers: ListEntries,
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub next_cursor: Option<Cursor>,
209}
210
211impl ListTriggersResponse {
212 #[must_use]
214 pub fn brief(triggers: Vec<String>, next_cursor: Option<Cursor>) -> Self {
215 Self {
216 triggers: ListEntries::Brief(triggers),
217 next_cursor,
218 }
219 }
220
221 #[must_use]
223 pub fn detailed(triggers: IndexMap<String, Value>, next_cursor: Option<Cursor>) -> Self {
224 Self {
225 triggers: ListEntries::Detailed(triggers),
226 next_cursor,
227 }
228 }
229}
230
231#[derive(Debug, Default, Deserialize, JsonSchema)]
233#[serde(rename_all = "camelCase")]
234pub struct ListFunctionsRequest {
235 #[serde(default)]
237 pub database: Option<String>,
238 #[serde(default)]
240 pub cursor: Option<Cursor>,
241}
242
243#[derive(Debug, Serialize, JsonSchema)]
245#[serde(rename_all = "camelCase")]
246pub struct ListFunctionsResponse {
247 pub functions: ListEntries,
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub next_cursor: Option<Cursor>,
252}
253
254impl ListFunctionsResponse {
255 #[must_use]
257 pub fn brief(functions: Vec<String>, next_cursor: Option<Cursor>) -> Self {
258 Self {
259 functions: ListEntries::Brief(functions),
260 next_cursor,
261 }
262 }
263
264 #[must_use]
266 pub fn detailed(functions: IndexMap<String, Value>, next_cursor: Option<Cursor>) -> Self {
267 Self {
268 functions: ListEntries::Detailed(functions),
269 next_cursor,
270 }
271 }
272}
273
274#[derive(Debug, Serialize, JsonSchema)]
276#[serde(rename_all = "camelCase")]
277pub struct ListProceduresResponse {
278 pub procedures: ListEntries,
280 #[serde(skip_serializing_if = "Option::is_none")]
282 pub next_cursor: Option<Cursor>,
283}
284
285impl ListProceduresResponse {
286 #[must_use]
288 pub fn brief(procedures: Vec<String>, next_cursor: Option<Cursor>) -> Self {
289 Self {
290 procedures: ListEntries::Brief(procedures),
291 next_cursor,
292 }
293 }
294
295 #[must_use]
297 pub fn detailed(procedures: IndexMap<String, Value>, next_cursor: Option<Cursor>) -> Self {
298 Self {
299 procedures: ListEntries::Detailed(procedures),
300 next_cursor,
301 }
302 }
303}
304
305#[derive(Debug, Default, Deserialize, JsonSchema)]
307#[serde(rename_all = "camelCase")]
308pub struct QueryRequest {
309 pub query: String,
311 #[serde(default)]
313 pub database: Option<String>,
314}
315
316#[derive(Debug, Default, Deserialize, JsonSchema)]
318#[serde(rename_all = "camelCase")]
319pub struct ReadQueryRequest {
320 pub query: String,
322 #[serde(default)]
324 pub database: Option<String>,
325 #[serde(default)]
327 pub cursor: Option<Cursor>,
328}
329
330#[derive(Debug, Serialize, JsonSchema)]
332#[serde(rename_all = "camelCase")]
333pub struct QueryResponse {
334 pub rows: Vec<Value>,
336}
337
338#[derive(Debug, Serialize, JsonSchema)]
340#[serde(rename_all = "camelCase")]
341pub struct ReadQueryResponse {
342 pub rows: Vec<Value>,
344 #[serde(skip_serializing_if = "Option::is_none")]
348 pub next_cursor: Option<Cursor>,
349}
350
351#[derive(Debug, Default, Deserialize, JsonSchema)]
353#[serde(rename_all = "camelCase")]
354pub struct ExplainQueryRequest {
355 #[serde(default)]
357 pub database: Option<String>,
358 pub query: String,
360 #[serde(default)]
362 pub analyze: bool,
363}
364
365#[cfg(test)]
366mod tests {
367 use super::{
368 IndexMap, ListEntries, ListFunctionsResponse, ListTablesResponse, ListTriggersRequest, ListTriggersResponse,
369 };
370 use serde_json::{Value, json};
371
372 #[test]
373 fn list_triggers_request_defaults_to_brief_mode_without_search() {
374 let req: ListTriggersRequest = serde_json::from_str("{}").expect("empty object should parse");
375 assert!(req.search.is_none());
376 assert!(!req.detailed, "detailed must default to false");
377 }
378
379 #[test]
380 fn list_triggers_request_accepts_search_and_detailed() {
381 let req: ListTriggersRequest = serde_json::from_str(r#"{"search": "audit", "detailed": true}"#).expect("parse");
382 assert_eq!(req.search.as_deref(), Some("audit"));
383 assert!(req.detailed);
384 }
385
386 #[test]
387 fn brief_serializes_as_bare_string_array() {
388 let entries = ListEntries::Brief(vec!["customers".into(), "orders".into()]);
389 assert_eq!(serde_json::to_value(&entries).unwrap(), json!(["customers", "orders"]));
390 }
391
392 #[test]
393 fn detailed_serializes_as_keyed_object() {
394 let entries = ListEntries::Detailed(IndexMap::from([("orders".into(), json!({"kind": "TABLE"}))]));
395 assert_eq!(
396 serde_json::to_value(&entries).unwrap(),
397 json!({"orders": {"kind": "TABLE"}})
398 );
399 }
400
401 #[test]
402 fn brief_empty_serializes_as_empty_array() {
403 assert_eq!(serde_json::to_value(ListEntries::Brief(Vec::new())).unwrap(), json!([]));
404 }
405
406 #[test]
407 fn detailed_empty_serializes_as_empty_object() {
408 assert_eq!(
409 serde_json::to_value(ListEntries::Detailed(IndexMap::new())).unwrap(),
410 json!({})
411 );
412 }
413
414 #[test]
415 fn detailed_preserves_insertion_order() {
416 let map = IndexMap::from([
417 ("c".into(), json!({})),
418 ("a".into(), json!({})),
419 ("b".into(), json!({})),
420 ]);
421 let s = serde_json::to_string(&ListEntries::Detailed(map)).unwrap();
422 let positions = ["\"c\"", "\"a\"", "\"b\""].map(|k| s.find(k).expect(k));
423 assert!(positions.is_sorted(), "insertion order not preserved: {s}");
424 }
425
426 #[test]
427 fn list_tables_response_brief_matches_legacy_wire_shape() {
428 let response = ListTablesResponse {
429 tables: ListEntries::Brief(vec!["a".into()]),
430 next_cursor: None,
431 };
432 assert_eq!(serde_json::to_value(&response).unwrap(), json!({"tables": ["a"]}));
433 }
434
435 #[test]
436 fn list_triggers_response_brief_matches_legacy_wire_shape() {
437 let response = ListTriggersResponse {
438 triggers: ListEntries::Brief(vec!["t1".into()]),
439 next_cursor: None,
440 };
441 assert_eq!(serde_json::to_value(&response).unwrap(), json!({"triggers": ["t1"]}));
442 }
443
444 #[test]
445 fn as_brief_and_as_detailed_unwrap_correct_variant() {
446 let brief = ListEntries::Brief(vec!["a".into()]);
447 assert_eq!(brief.as_brief(), Some(&["a".into()][..]));
448 assert!(brief.as_detailed().is_none());
449
450 let det = ListEntries::Detailed(IndexMap::from([("x".into(), json!(1))]));
451 assert!(det.as_brief().is_none());
452 assert_eq!(det.as_detailed().map(IndexMap::len), Some(1));
453 }
454
455 #[test]
459 fn detailed_payload_strictly_smaller_than_array_form() {
460 let metadata = json!({
461 "schema": "public", "kind": "TABLE", "owner": "app", "comment": null,
462 "columns": [
463 {"name": "id", "dataType": "bigint", "ordinalPosition": 1, "nullable": false, "default": null, "comment": null},
464 {"name": "created_at", "dataType": "timestamptz", "ordinalPosition": 2, "nullable": false, "default": "now()", "comment": null},
465 ],
466 "constraints": [{"name": "pk", "type": "PRIMARY KEY", "columns": ["id"], "definition": "PRIMARY KEY (id)"}],
467 "indexes": [], "triggers": [],
468 });
469 let tables = [
470 "customers",
471 "orders",
472 "items",
473 "products",
474 "inventory",
475 "suppliers",
476 "shipments",
477 "invoices",
478 "payments",
479 "audits",
480 ];
481 let new_map: IndexMap<String, Value> = tables.iter().map(|n| ((*n).into(), metadata.clone())).collect();
482 let old: Vec<Value> = tables
483 .iter()
484 .map(|n| {
485 let mut v = metadata.clone();
486 v["name"] = json!(n);
487 v
488 })
489 .collect();
490 let new_len = serde_json::to_vec(&ListEntries::Detailed(new_map)).unwrap().len();
491 let old_len = serde_json::to_vec(&old).unwrap().len();
492 assert!(new_len < old_len, "payload not smaller: new={new_len} old={old_len}");
493 }
494
495 #[test]
496 fn list_functions_response_brief_constructor_wraps_vec() {
497 let response = ListFunctionsResponse::brief(vec!["calc_total".into()], None);
498 assert!(matches!(response.functions, ListEntries::Brief(ref v) if v == &["calc_total"]));
499 assert!(response.next_cursor.is_none());
500 }
501
502 #[test]
503 fn list_functions_response_detailed_constructor_wraps_indexmap() {
504 let map = IndexMap::from([("calc_total(integer)".into(), json!({"language": "sql"}))]);
505 let response = ListFunctionsResponse::detailed(map, None);
506 assert!(matches!(response.functions, ListEntries::Detailed(_)));
507 }
508
509 #[test]
510 fn list_functions_response_brief_matches_legacy_wire_shape() {
511 let response = ListFunctionsResponse::brief(vec!["audit_user_login".into()], None);
512 assert_eq!(
513 serde_json::to_value(&response).unwrap(),
514 json!({"functions": ["audit_user_login"]})
515 );
516 }
517
518 #[test]
519 fn list_procedures_response_brief_constructor_wraps_vec() {
520 let response = super::ListProceduresResponse::brief(vec!["archive_order".into()], None);
521 assert!(matches!(response.procedures, ListEntries::Brief(ref v) if v == &["archive_order"]));
522 assert!(response.next_cursor.is_none());
523 }
524
525 #[test]
526 fn list_procedures_response_detailed_constructor_wraps_indexmap() {
527 let map = IndexMap::from([("archive_order(integer)".into(), json!({"language": "plpgsql"}))]);
528 let response = super::ListProceduresResponse::detailed(map, None);
529 assert!(matches!(response.procedures, ListEntries::Detailed(_)));
530 }
531
532 #[test]
533 fn list_procedures_response_brief_matches_legacy_wire_shape() {
534 let response = super::ListProceduresResponse::brief(vec!["archive_order".into()], None);
535 assert_eq!(
536 serde_json::to_value(&response).unwrap(),
537 json!({"procedures": ["archive_order"]})
538 );
539 }
540
541 #[test]
542 fn list_views_response_brief_constructor_wraps_vec() {
543 let response = super::ListViewsResponse::brief(vec!["active_users".into()], None);
544 assert!(matches!(response.views, ListEntries::Brief(ref v) if v == &["active_users"]));
545 assert!(response.next_cursor.is_none());
546 }
547
548 #[test]
549 fn list_views_response_detailed_constructor_wraps_indexmap() {
550 let map = IndexMap::from([("active_users".into(), json!({"schema": "public"}))]);
551 let response = super::ListViewsResponse::detailed(map, None);
552 assert!(matches!(response.views, ListEntries::Detailed(_)));
553 }
554
555 #[test]
556 fn list_views_response_brief_matches_legacy_wire_shape() {
557 let response = super::ListViewsResponse::brief(vec!["active_users".into()], None);
558 assert_eq!(
559 serde_json::to_value(&response).unwrap(),
560 json!({"views": ["active_users"]})
561 );
562 }
563}