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)]
63pub struct ListTablesResponse {
64 pub tables: ListEntries,
66 #[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
68 pub next_cursor: Option<Cursor>,
69}
70
71impl ListTablesResponse {
72 #[must_use]
74 pub fn brief(tables: Vec<String>, next_cursor: Option<Cursor>) -> Self {
75 Self {
76 tables: ListEntries::Brief(tables),
77 next_cursor,
78 }
79 }
80
81 #[must_use]
83 pub fn detailed(tables: IndexMap<String, Value>, next_cursor: Option<Cursor>) -> Self {
84 Self {
85 tables: ListEntries::Detailed(tables),
86 next_cursor,
87 }
88 }
89}
90
91#[derive(Debug, Serialize, JsonSchema)]
93pub struct MessageResponse {
94 pub message: String,
96}
97
98#[derive(Debug, Default, Deserialize, JsonSchema)]
100pub struct ListDatabasesRequest {
101 #[serde(default)]
103 pub cursor: Option<Cursor>,
104}
105
106#[derive(Debug, Serialize, JsonSchema)]
108pub struct ListDatabasesResponse {
109 pub databases: Vec<String>,
111 #[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
113 pub next_cursor: Option<Cursor>,
114}
115
116#[derive(Debug, Default, Deserialize, JsonSchema)]
118pub struct CreateDatabaseRequest {
119 pub database: String,
121}
122
123#[derive(Debug, Default, Deserialize, JsonSchema)]
125pub struct DropDatabaseRequest {
126 pub database: String,
128}
129
130#[derive(Debug, Default, Deserialize, JsonSchema)]
132#[schemars(rename = "ListViewsRequest")]
133pub struct PinnedListViewsRequest {
134 #[serde(default)]
136 pub cursor: Option<Cursor>,
137}
138
139#[derive(Debug, Default, Deserialize, JsonSchema)]
141#[schemars(rename = "ListViewsRequest")]
142pub struct UnpinnedListViewsRequest {
143 #[serde(flatten)]
144 pub inner: PinnedListViewsRequest,
145 #[serde(default)]
147 pub database: Option<String>,
148}
149
150#[derive(Debug, Serialize, JsonSchema)]
152pub struct ListViewsResponse {
153 pub views: ListEntries,
155 #[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
157 pub next_cursor: Option<Cursor>,
158}
159
160impl ListViewsResponse {
161 #[must_use]
163 pub fn brief(views: Vec<String>, next_cursor: Option<Cursor>) -> Self {
164 Self {
165 views: ListEntries::Brief(views),
166 next_cursor,
167 }
168 }
169
170 #[must_use]
172 pub fn detailed(views: IndexMap<String, Value>, next_cursor: Option<Cursor>) -> Self {
173 Self {
174 views: ListEntries::Detailed(views),
175 next_cursor,
176 }
177 }
178}
179
180#[derive(Debug, Default, Deserialize, JsonSchema)]
182#[schemars(rename = "ListTriggersRequest")]
183pub struct PinnedListTriggersRequest {
184 #[serde(default)]
186 pub cursor: Option<Cursor>,
187 #[serde(default)]
190 pub search: Option<String>,
191 #[serde(default)]
195 pub detailed: bool,
196}
197
198#[derive(Debug, Default, Deserialize, JsonSchema)]
200#[schemars(rename = "ListTriggersRequest")]
201pub struct UnpinnedListTriggersRequest {
202 #[serde(flatten)]
203 pub inner: PinnedListTriggersRequest,
204 #[serde(default)]
206 pub database: Option<String>,
207}
208
209#[derive(Debug, Serialize, JsonSchema)]
211pub struct ListTriggersResponse {
212 pub triggers: ListEntries,
214 #[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
216 pub next_cursor: Option<Cursor>,
217}
218
219impl ListTriggersResponse {
220 #[must_use]
222 pub fn brief(triggers: Vec<String>, next_cursor: Option<Cursor>) -> Self {
223 Self {
224 triggers: ListEntries::Brief(triggers),
225 next_cursor,
226 }
227 }
228
229 #[must_use]
231 pub fn detailed(triggers: IndexMap<String, Value>, next_cursor: Option<Cursor>) -> Self {
232 Self {
233 triggers: ListEntries::Detailed(triggers),
234 next_cursor,
235 }
236 }
237}
238
239#[derive(Debug, Default, Deserialize, JsonSchema)]
241#[schemars(rename = "ListFunctionsRequest")]
242pub struct PinnedListFunctionsRequest {
243 #[serde(default)]
245 pub cursor: Option<Cursor>,
246}
247
248#[derive(Debug, Default, Deserialize, JsonSchema)]
250#[schemars(rename = "ListFunctionsRequest")]
251pub struct UnpinnedListFunctionsRequest {
252 #[serde(flatten)]
253 pub inner: PinnedListFunctionsRequest,
254 #[serde(default)]
256 pub database: Option<String>,
257}
258
259#[derive(Debug, Serialize, JsonSchema)]
261pub struct ListFunctionsResponse {
262 pub functions: ListEntries,
264 #[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
266 pub next_cursor: Option<Cursor>,
267}
268
269impl ListFunctionsResponse {
270 #[must_use]
272 pub fn brief(functions: Vec<String>, next_cursor: Option<Cursor>) -> Self {
273 Self {
274 functions: ListEntries::Brief(functions),
275 next_cursor,
276 }
277 }
278
279 #[must_use]
281 pub fn detailed(functions: IndexMap<String, Value>, next_cursor: Option<Cursor>) -> Self {
282 Self {
283 functions: ListEntries::Detailed(functions),
284 next_cursor,
285 }
286 }
287}
288
289#[derive(Debug, Serialize, JsonSchema)]
291pub struct ListProceduresResponse {
292 pub procedures: ListEntries,
294 #[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
296 pub next_cursor: Option<Cursor>,
297}
298
299impl ListProceduresResponse {
300 #[must_use]
302 pub fn brief(procedures: Vec<String>, next_cursor: Option<Cursor>) -> Self {
303 Self {
304 procedures: ListEntries::Brief(procedures),
305 next_cursor,
306 }
307 }
308
309 #[must_use]
311 pub fn detailed(procedures: IndexMap<String, Value>, next_cursor: Option<Cursor>) -> Self {
312 Self {
313 procedures: ListEntries::Detailed(procedures),
314 next_cursor,
315 }
316 }
317}
318
319#[derive(Debug, Default, Deserialize, JsonSchema)]
321#[schemars(rename = "QueryRequest")]
322pub struct PinnedQueryRequest {
323 pub query: String,
325}
326
327#[derive(Debug, Default, Deserialize, JsonSchema)]
329#[schemars(rename = "QueryRequest")]
330pub struct UnpinnedQueryRequest {
331 #[serde(flatten)]
332 pub inner: PinnedQueryRequest,
333 #[serde(default)]
335 pub database: Option<String>,
336}
337
338#[derive(Debug, Default, Deserialize, JsonSchema)]
340#[schemars(rename = "ReadQueryRequest")]
341pub struct PinnedReadQueryRequest {
342 pub query: String,
344 #[serde(default)]
346 pub cursor: Option<Cursor>,
347}
348
349#[derive(Debug, Default, Deserialize, JsonSchema)]
351#[schemars(rename = "ReadQueryRequest")]
352pub struct UnpinnedReadQueryRequest {
353 #[serde(flatten)]
354 pub inner: PinnedReadQueryRequest,
355 #[serde(default)]
357 pub database: Option<String>,
358}
359
360#[derive(Debug, Serialize, JsonSchema)]
362pub struct QueryResponse {
363 pub rows: Vec<Value>,
365}
366
367#[derive(Debug, Serialize, JsonSchema)]
369pub struct ReadQueryResponse {
370 pub rows: Vec<Value>,
372 #[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
376 pub next_cursor: Option<Cursor>,
377}
378
379#[derive(Debug, Default, Deserialize, JsonSchema)]
381#[schemars(rename = "ExplainQueryRequest")]
382pub struct PinnedExplainQueryRequest {
383 pub query: String,
385 #[serde(default)]
387 pub analyze: bool,
388}
389
390#[derive(Debug, Default, Deserialize, JsonSchema)]
392#[schemars(rename = "ExplainQueryRequest")]
393pub struct UnpinnedExplainQueryRequest {
394 #[serde(flatten)]
395 pub inner: PinnedExplainQueryRequest,
396 #[serde(default)]
398 pub database: Option<String>,
399}
400
401#[cfg(test)]
402mod tests {
403 use super::{
404 IndexMap, ListEntries, ListFunctionsResponse, ListTablesResponse, ListTriggersResponse,
405 PinnedListTriggersRequest, UnpinnedListTriggersRequest,
406 };
407 use serde_json::{Value, json};
408
409 #[test]
410 fn unpinned_list_triggers_request_defaults_to_brief_mode_without_search() {
411 let req: PinnedListTriggersRequest = serde_json::from_str("{}").expect("empty object should parse");
412 assert!(req.search.is_none());
413 assert!(!req.detailed, "detailed must default to false");
414 }
415
416 #[test]
417 fn unpinned_list_triggers_request_accepts_search_and_detailed() {
418 let req: PinnedListTriggersRequest =
419 serde_json::from_str(r#"{"search": "audit", "detailed": true}"#).expect("parse");
420 assert_eq!(req.search.as_deref(), Some("audit"));
421 assert!(req.detailed);
422 }
423
424 #[test]
425 fn pinned_list_triggers_request_accepts_database_and_inner_fields() {
426 let req: UnpinnedListTriggersRequest =
427 serde_json::from_str(r#"{"database": "mydb", "search": "audit", "detailed": true}"#).expect("parse");
428 assert_eq!(req.database.as_deref(), Some("mydb"));
429 assert_eq!(req.inner.search.as_deref(), Some("audit"));
430 assert!(req.inner.detailed);
431 }
432
433 #[test]
434 fn brief_serializes_as_bare_string_array() {
435 let entries = ListEntries::Brief(vec!["customers".into(), "orders".into()]);
436 assert_eq!(serde_json::to_value(&entries).unwrap(), json!(["customers", "orders"]));
437 }
438
439 #[test]
440 fn detailed_serializes_as_keyed_object() {
441 let entries = ListEntries::Detailed(IndexMap::from([("orders".into(), json!({"kind": "TABLE"}))]));
442 assert_eq!(
443 serde_json::to_value(&entries).unwrap(),
444 json!({"orders": {"kind": "TABLE"}})
445 );
446 }
447
448 #[test]
449 fn brief_empty_serializes_as_empty_array() {
450 assert_eq!(serde_json::to_value(ListEntries::Brief(Vec::new())).unwrap(), json!([]));
451 }
452
453 #[test]
454 fn detailed_empty_serializes_as_empty_object() {
455 assert_eq!(
456 serde_json::to_value(ListEntries::Detailed(IndexMap::new())).unwrap(),
457 json!({})
458 );
459 }
460
461 #[test]
462 fn detailed_preserves_insertion_order() {
463 let map = IndexMap::from([
464 ("c".into(), json!({})),
465 ("a".into(), json!({})),
466 ("b".into(), json!({})),
467 ]);
468 let s = serde_json::to_string(&ListEntries::Detailed(map)).unwrap();
469 let positions = ["\"c\"", "\"a\"", "\"b\""].map(|k| s.find(k).expect(k));
470 assert!(positions.is_sorted(), "insertion order not preserved: {s}");
471 }
472
473 #[test]
474 fn list_tables_response_brief_matches_legacy_wire_shape() {
475 let response = ListTablesResponse {
476 tables: ListEntries::Brief(vec!["a".into()]),
477 next_cursor: None,
478 };
479 assert_eq!(serde_json::to_value(&response).unwrap(), json!({"tables": ["a"]}));
480 }
481
482 #[test]
483 fn list_triggers_response_brief_matches_legacy_wire_shape() {
484 let response = ListTriggersResponse {
485 triggers: ListEntries::Brief(vec!["t1".into()]),
486 next_cursor: None,
487 };
488 assert_eq!(serde_json::to_value(&response).unwrap(), json!({"triggers": ["t1"]}));
489 }
490
491 #[test]
492 fn as_brief_and_as_detailed_unwrap_correct_variant() {
493 let brief = ListEntries::Brief(vec!["a".into()]);
494 assert_eq!(brief.as_brief(), Some(&["a".into()][..]));
495 assert!(brief.as_detailed().is_none());
496
497 let det = ListEntries::Detailed(IndexMap::from([("x".into(), json!(1))]));
498 assert!(det.as_brief().is_none());
499 assert_eq!(det.as_detailed().map(IndexMap::len), Some(1));
500 }
501
502 #[test]
506 fn detailed_payload_strictly_smaller_than_array_form() {
507 let metadata = json!({
508 "schema": "public", "kind": "TABLE", "owner": "app", "comment": null,
509 "columns": [
510 {"name": "id", "dataType": "bigint", "ordinalPosition": 1, "nullable": false, "default": null, "comment": null},
511 {"name": "created_at", "dataType": "timestamptz", "ordinalPosition": 2, "nullable": false, "default": "now()", "comment": null},
512 ],
513 "constraints": [{"name": "pk", "type": "PRIMARY KEY", "columns": ["id"], "definition": "PRIMARY KEY (id)"}],
514 "indexes": [], "triggers": [],
515 });
516 let tables = [
517 "customers",
518 "orders",
519 "items",
520 "products",
521 "inventory",
522 "suppliers",
523 "shipments",
524 "invoices",
525 "payments",
526 "audits",
527 ];
528 let new_map: IndexMap<String, Value> = tables.iter().map(|n| ((*n).into(), metadata.clone())).collect();
529 let old: Vec<Value> = tables
530 .iter()
531 .map(|n| {
532 let mut v = metadata.clone();
533 v["name"] = json!(n);
534 v
535 })
536 .collect();
537 let new_len = serde_json::to_vec(&ListEntries::Detailed(new_map)).unwrap().len();
538 let old_len = serde_json::to_vec(&old).unwrap().len();
539 assert!(new_len < old_len, "payload not smaller: new={new_len} old={old_len}");
540 }
541
542 #[test]
543 fn list_functions_response_brief_constructor_wraps_vec() {
544 let response = ListFunctionsResponse::brief(vec!["calc_total".into()], None);
545 assert!(matches!(response.functions, ListEntries::Brief(ref v) if v == &["calc_total"]));
546 assert!(response.next_cursor.is_none());
547 }
548
549 #[test]
550 fn list_functions_response_detailed_constructor_wraps_indexmap() {
551 let map = IndexMap::from([("calc_total(integer)".into(), json!({"language": "sql"}))]);
552 let response = ListFunctionsResponse::detailed(map, None);
553 assert!(matches!(response.functions, ListEntries::Detailed(_)));
554 }
555
556 #[test]
557 fn list_functions_response_brief_matches_legacy_wire_shape() {
558 let response = ListFunctionsResponse::brief(vec!["audit_user_login".into()], None);
559 assert_eq!(
560 serde_json::to_value(&response).unwrap(),
561 json!({"functions": ["audit_user_login"]})
562 );
563 }
564
565 #[test]
566 fn list_procedures_response_brief_constructor_wraps_vec() {
567 let response = super::ListProceduresResponse::brief(vec!["archive_order".into()], None);
568 assert!(matches!(response.procedures, ListEntries::Brief(ref v) if v == &["archive_order"]));
569 assert!(response.next_cursor.is_none());
570 }
571
572 #[test]
573 fn list_procedures_response_detailed_constructor_wraps_indexmap() {
574 let map = IndexMap::from([("archive_order(integer)".into(), json!({"language": "plpgsql"}))]);
575 let response = super::ListProceduresResponse::detailed(map, None);
576 assert!(matches!(response.procedures, ListEntries::Detailed(_)));
577 }
578
579 #[test]
580 fn list_procedures_response_brief_matches_legacy_wire_shape() {
581 let response = super::ListProceduresResponse::brief(vec!["archive_order".into()], None);
582 assert_eq!(
583 serde_json::to_value(&response).unwrap(),
584 json!({"procedures": ["archive_order"]})
585 );
586 }
587
588 #[test]
589 fn list_views_response_brief_constructor_wraps_vec() {
590 let response = super::ListViewsResponse::brief(vec!["active_users".into()], None);
591 assert!(matches!(response.views, ListEntries::Brief(ref v) if v == &["active_users"]));
592 assert!(response.next_cursor.is_none());
593 }
594
595 #[test]
596 fn list_views_response_detailed_constructor_wraps_indexmap() {
597 let map = IndexMap::from([("active_users".into(), json!({"schema": "public"}))]);
598 let response = super::ListViewsResponse::detailed(map, None);
599 assert!(matches!(response.views, ListEntries::Detailed(_)));
600 }
601
602 #[test]
603 fn list_views_response_brief_matches_legacy_wire_shape() {
604 let response = super::ListViewsResponse::brief(vec!["active_users".into()], None);
605 assert_eq!(
606 serde_json::to_value(&response).unwrap(),
607 json!({"views": ["active_users"]})
608 );
609 }
610}