1use reqwest::header::{HeaderMap, HeaderValue};
2use serde_json::Value as JsonValue;
3
4use crate::sql::{
5 ArrayRangeOperator, CountOption, ExplainOptions, FilterCondition, FilterOperator, IsValue,
6 OrderDirection, ParamStore, PatternOperator, SqlParam, SqlParts, TextSearchType,
7};
8
9pub fn build_postgrest_select(
11 base_url: &str,
12 parts: &SqlParts,
13 params: &ParamStore,
14) -> Result<(String, HeaderMap), String> {
15 let table = &parts.table;
16 let mut url = format!("{}/rest/v1/{}", base_url.trim_end_matches('/'), table);
17 let mut headers = HeaderMap::new();
18 let mut query_params = Vec::new();
19
20 if let Some(ref cols) = parts.select_columns {
22 let cleaned = cols
24 .split(',')
25 .map(|c| c.trim().trim_matches('"'))
26 .collect::<Vec<_>>()
27 .join(",");
28 query_params.push(format!("select={}", cleaned));
29 }
30
31 render_filters_to_params(&parts.filters, params, &mut query_params)?;
33
34 if !parts.orders.is_empty() {
36 let order_parts: Vec<String> = parts
37 .orders
38 .iter()
39 .map(|o| {
40 let dir = match o.direction {
41 OrderDirection::Ascending => "asc",
42 OrderDirection::Descending => "desc",
43 };
44 let nulls = match &o.nulls {
45 Some(crate::sql::NullsPosition::First) => ".nullsfirst",
46 Some(crate::sql::NullsPosition::Last) => ".nullslast",
47 None => "",
48 };
49 format!("{}.{}{}", o.column, dir, nulls)
50 })
51 .collect();
52 query_params.push(format!("order={}", order_parts.join(",")));
53 }
54
55 if let Some(limit) = parts.limit {
57 query_params.push(format!("limit={}", limit));
58 }
59
60 if let Some(offset) = parts.offset {
62 query_params.push(format!("offset={}", offset));
63 }
64
65 if parts.limit.is_some() || parts.offset.is_some() {
67 let from = parts.offset.unwrap_or(0);
68 let to = match parts.limit {
69 Some(limit) => from + limit - 1,
70 None => i64::MAX,
71 };
72 headers.insert(
73 "Range",
74 HeaderValue::from_str(&format!("{}-{}", from, to)).unwrap(),
75 );
76 headers.insert("Range-Unit", HeaderValue::from_static("items"));
77 }
78
79 if parts.single {
81 headers.insert(
82 "Accept",
83 HeaderValue::from_static("application/vnd.pgrst.object+json"),
84 );
85 }
86
87 {
89 let mut prefer_parts = Vec::new();
90 if parts.head {
91 prefer_parts.push("count=exact".to_string());
93 } else if let Some(count_val) = count_option_prefer(parts.count) {
94 prefer_parts.push(count_val.to_string());
95 }
96 if !prefer_parts.is_empty() {
97 headers.insert(
98 "Prefer",
99 HeaderValue::from_str(&prefer_parts.join(",")).unwrap(),
100 );
101 }
102 }
103
104 if let Some(ref opts) = parts.explain {
106 headers.insert("Accept", build_explain_accept(opts));
107 }
108
109 if let Some(ref schema) = parts.schema_override {
111 headers.insert(
112 "Accept-Profile",
113 HeaderValue::from_str(schema).unwrap(),
114 );
115 }
116
117 if !query_params.is_empty() {
119 url.push('?');
120 url.push_str(&query_params.join("&"));
121 }
122
123 Ok((url, headers))
124}
125
126pub fn build_postgrest_insert(
128 base_url: &str,
129 parts: &SqlParts,
130 params: &ParamStore,
131) -> Result<(String, HeaderMap, JsonValue), String> {
132 let table = &parts.table;
133 let url = format!("{}/rest/v1/{}", base_url.trim_end_matches('/'), table);
134 let mut headers = HeaderMap::new();
135
136 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
137
138 {
140 let mut prefer_parts = Vec::new();
141 if parts.returning.is_some() {
142 prefer_parts.push("return=representation");
143 } else {
144 prefer_parts.push("return=minimal");
145 }
146 if let Some(count_val) = count_option_prefer(parts.count) {
147 prefer_parts.push(count_val);
148 }
149 headers.insert(
150 "Prefer",
151 HeaderValue::from_str(&prefer_parts.join(",")).unwrap(),
152 );
153 }
154
155 if let Some(ref schema) = parts.schema_override {
157 headers.insert(
158 "Content-Profile",
159 HeaderValue::from_str(schema).unwrap(),
160 );
161 }
162
163 let body = build_insert_body(parts, params)?;
165
166 Ok((url, headers, body))
167}
168
169pub fn build_postgrest_update(
171 base_url: &str,
172 parts: &SqlParts,
173 params: &ParamStore,
174) -> Result<(String, HeaderMap, JsonValue), String> {
175 let table = &parts.table;
176 let mut url = format!("{}/rest/v1/{}", base_url.trim_end_matches('/'), table);
177 let mut headers = HeaderMap::new();
178 let mut query_params = Vec::new();
179
180 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
181
182 {
184 let mut prefer_parts = Vec::new();
185 if parts.returning.is_some() {
186 prefer_parts.push("return=representation");
187 } else {
188 prefer_parts.push("return=minimal");
189 }
190 if let Some(count_val) = count_option_prefer(parts.count) {
191 prefer_parts.push(count_val);
192 }
193 headers.insert(
194 "Prefer",
195 HeaderValue::from_str(&prefer_parts.join(",")).unwrap(),
196 );
197 }
198
199 if let Some(ref schema) = parts.schema_override {
201 headers.insert(
202 "Content-Profile",
203 HeaderValue::from_str(schema).unwrap(),
204 );
205 }
206
207 render_filters_to_params(&parts.filters, params, &mut query_params)?;
209
210 if !query_params.is_empty() {
211 url.push('?');
212 url.push_str(&query_params.join("&"));
213 }
214
215 let body = build_set_body(&parts.set_clauses, params)?;
217
218 Ok((url, headers, body))
219}
220
221pub fn build_postgrest_delete(
223 base_url: &str,
224 parts: &SqlParts,
225 params: &ParamStore,
226) -> Result<(String, HeaderMap), String> {
227 let table = &parts.table;
228 let mut url = format!("{}/rest/v1/{}", base_url.trim_end_matches('/'), table);
229 let mut headers = HeaderMap::new();
230 let mut query_params = Vec::new();
231
232 {
234 let mut prefer_parts = Vec::new();
235 if parts.returning.is_some() {
236 prefer_parts.push("return=representation");
237 } else {
238 prefer_parts.push("return=minimal");
239 }
240 if let Some(count_val) = count_option_prefer(parts.count) {
241 prefer_parts.push(count_val);
242 }
243 headers.insert(
244 "Prefer",
245 HeaderValue::from_str(&prefer_parts.join(",")).unwrap(),
246 );
247 }
248
249 if let Some(ref schema) = parts.schema_override {
251 headers.insert(
252 "Content-Profile",
253 HeaderValue::from_str(schema).unwrap(),
254 );
255 }
256
257 render_filters_to_params(&parts.filters, params, &mut query_params)?;
259
260 if !query_params.is_empty() {
261 url.push('?');
262 url.push_str(&query_params.join("&"));
263 }
264
265 Ok((url, headers))
266}
267
268pub fn build_postgrest_upsert(
270 base_url: &str,
271 parts: &SqlParts,
272 params: &ParamStore,
273) -> Result<(String, HeaderMap, JsonValue), String> {
274 let table = &parts.table;
275 let mut url = format!("{}/rest/v1/{}", base_url.trim_end_matches('/'), table);
276 let mut headers = HeaderMap::new();
277
278 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
279
280 let mut prefer_parts: Vec<&str> = Vec::new();
282
283 if parts.ignore_duplicates {
284 prefer_parts.push("resolution=ignore-duplicates");
285 } else {
286 prefer_parts.push("resolution=merge-duplicates");
287 }
288
289 if parts.returning.is_some() {
290 prefer_parts.push("return=representation");
291 } else {
292 prefer_parts.push("return=minimal");
293 }
294
295 let count_str = count_option_prefer(parts.count);
297 if let Some(cv) = count_str {
298 prefer_parts.push(cv);
299 }
300
301 headers.insert(
302 "Prefer",
303 HeaderValue::from_str(&prefer_parts.join(",")).unwrap(),
304 );
305
306 if !parts.conflict_columns.is_empty() {
308 let conflict = parts.conflict_columns.join(",");
309 url.push_str(&format!(
310 "{}on_conflict={}",
311 if url.contains('?') { "&" } else { "?" },
312 conflict
313 ));
314 }
315
316 if let Some(ref schema) = parts.schema_override {
318 headers.insert(
319 "Content-Profile",
320 HeaderValue::from_str(schema).unwrap(),
321 );
322 }
323
324 let body = build_insert_body(parts, params)?;
326
327 Ok((url, headers, body))
328}
329
330pub fn build_postgrest_rpc(
332 base_url: &str,
333 function: &str,
334 args: &JsonValue,
335 rollback: bool,
336) -> (String, HeaderMap, JsonValue) {
337 let url = format!("{}/rest/v1/rpc/{}", base_url.trim_end_matches('/'), function);
338 let mut headers = HeaderMap::new();
339 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
340
341 if rollback {
342 headers.insert("Prefer", HeaderValue::from_static("tx=rollback"));
343 }
344
345 let body = if args.is_null() {
346 JsonValue::Object(serde_json::Map::new())
347 } else {
348 args.clone()
349 };
350
351 (url, headers, body)
352}
353
354fn count_option_prefer(option: CountOption) -> Option<&'static str> {
358 match option {
359 CountOption::None => None,
360 CountOption::Exact => Some("count=exact"),
361 CountOption::Planned => Some("count=planned"),
362 CountOption::Estimated => Some("count=estimated"),
363 }
364}
365
366pub fn render_param_value(param: &SqlParam) -> String {
368 match param {
369 SqlParam::Null => "null".to_string(),
370 SqlParam::Bool(b) => b.to_string(),
371 SqlParam::I16(n) => n.to_string(),
372 SqlParam::I32(n) => n.to_string(),
373 SqlParam::I64(n) => n.to_string(),
374 SqlParam::F32(n) => n.to_string(),
375 SqlParam::F64(n) => n.to_string(),
376 SqlParam::Text(s) => s.clone(),
377 SqlParam::Uuid(u) => u.to_string(),
378 SqlParam::Timestamp(t) => t.to_string(),
379 SqlParam::TimestampTz(t) => t.to_rfc3339(),
380 SqlParam::Date(d) => d.to_string(),
381 SqlParam::Time(t) => t.to_string(),
382 SqlParam::Json(v) => v.to_string(),
383 SqlParam::ByteArray(b) => format!("\\x{}", hex_encode(b)),
384 SqlParam::TextArray(arr) => format!("{{{}}}", arr.iter().map(|s| format!("\"{}\"", s)).collect::<Vec<_>>().join(",")),
385 SqlParam::I32Array(arr) => format!("{{{}}}", arr.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(",")),
386 SqlParam::I64Array(arr) => format!("{{{}}}", arr.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(",")),
387 }
388}
389
390fn param_to_json(param: &SqlParam) -> JsonValue {
392 match param {
393 SqlParam::Null => JsonValue::Null,
394 SqlParam::Bool(b) => JsonValue::Bool(*b),
395 SqlParam::I16(n) => JsonValue::Number((*n as i64).into()),
396 SqlParam::I32(n) => JsonValue::Number((*n as i64).into()),
397 SqlParam::I64(n) => JsonValue::Number((*n).into()),
398 SqlParam::F32(n) => serde_json::Number::from_f64(*n as f64)
399 .map(JsonValue::Number)
400 .unwrap_or(JsonValue::Null),
401 SqlParam::F64(n) => serde_json::Number::from_f64(*n)
402 .map(JsonValue::Number)
403 .unwrap_or(JsonValue::Null),
404 SqlParam::Text(s) => JsonValue::String(s.clone()),
405 SqlParam::Uuid(u) => JsonValue::String(u.to_string()),
406 SqlParam::Timestamp(t) => JsonValue::String(t.to_string()),
407 SqlParam::TimestampTz(t) => JsonValue::String(t.to_rfc3339()),
408 SqlParam::Date(d) => JsonValue::String(d.to_string()),
409 SqlParam::Time(t) => JsonValue::String(t.to_string()),
410 SqlParam::Json(v) => v.clone(),
411 SqlParam::ByteArray(b) => JsonValue::String(format!("\\x{}", hex_encode(b))),
412 SqlParam::TextArray(arr) => JsonValue::Array(arr.iter().map(|s| JsonValue::String(s.clone())).collect()),
413 SqlParam::I32Array(arr) => JsonValue::Array(arr.iter().map(|n| JsonValue::Number((*n as i64).into())).collect()),
414 SqlParam::I64Array(arr) => JsonValue::Array(arr.iter().map(|n| JsonValue::Number((*n).into())).collect()),
415 }
416}
417
418fn hex_encode(bytes: &[u8]) -> String {
419 bytes.iter().map(|b| format!("{:02x}", b)).collect()
420}
421
422fn render_filter(
424 condition: &FilterCondition,
425 params: &ParamStore,
426 output: &mut Vec<String>,
427) -> Result<(), String> {
428 match condition {
429 FilterCondition::Comparison { column, operator, param_index } => {
430 let op = match operator {
431 FilterOperator::Eq => "eq",
432 FilterOperator::Neq => "neq",
433 FilterOperator::Gt => "gt",
434 FilterOperator::Gte => "gte",
435 FilterOperator::Lt => "lt",
436 FilterOperator::Lte => "lte",
437 };
438 let param = params.get(*param_index - 1)
439 .ok_or_else(|| format!("Missing param at index {}", param_index))?;
440 let val = render_param_value(param);
441 output.push(format!("{}={}.{}", column, op, val));
442 }
443 FilterCondition::Is { column, value } => {
444 let val = match value {
445 IsValue::Null => "null",
446 IsValue::NotNull => "not.null", IsValue::True => "true",
448 IsValue::False => "false",
449 };
450 output.push(format!("{}=is.{}", column, val));
451 }
452 FilterCondition::In { column, param_indices } => {
453 let vals: Result<Vec<String>, String> = param_indices
454 .iter()
455 .map(|idx| {
456 let param = params.get(*idx - 1)
457 .ok_or_else(|| format!("Missing param at index {}", idx))?;
458 Ok(render_param_value(param))
459 })
460 .collect();
461 let val_list = vals?.join(",");
462 output.push(format!("{}=in.({})", column, val_list));
463 }
464 FilterCondition::Pattern { column, operator, param_index } => {
465 let op = match operator {
466 PatternOperator::Like => "like",
467 PatternOperator::ILike => "ilike",
468 };
469 let param = params.get(*param_index - 1)
470 .ok_or_else(|| format!("Missing param at index {}", param_index))?;
471 let val = render_param_value(param);
472 output.push(format!("{}={}.{}", column, op, val));
473 }
474 FilterCondition::TextSearch { column, query_param_index, config, search_type } => {
475 let op = match search_type {
476 TextSearchType::Plain => "plfts",
477 TextSearchType::Phrase => "phfts",
478 TextSearchType::Websearch => "wfts",
479 };
480 let param = params.get(*query_param_index - 1)
481 .ok_or_else(|| format!("Missing param at index {}", query_param_index))?;
482 let val = render_param_value(param);
483 if let Some(cfg) = config {
484 output.push(format!("{}={}({}).{}", column, op, cfg, val));
485 } else {
486 output.push(format!("{}={}.{}", column, op, val));
487 }
488 }
489 FilterCondition::ArrayRange { column, operator, param_index } => {
490 let op = match operator {
491 ArrayRangeOperator::Contains => "cs",
492 ArrayRangeOperator::ContainedBy => "cd",
493 ArrayRangeOperator::Overlaps => "ov",
494 ArrayRangeOperator::RangeGt => "sl", ArrayRangeOperator::RangeGte => "nxl",
496 ArrayRangeOperator::RangeLt => "sr", ArrayRangeOperator::RangeLte => "nxr",
498 ArrayRangeOperator::RangeAdjacent => "adj",
499 };
500 let param = params.get(*param_index - 1)
501 .ok_or_else(|| format!("Missing param at index {}", param_index))?;
502 let val = render_param_value(param);
503 output.push(format!("{}={}.{}", column, op, val));
504 }
505 FilterCondition::Not(inner) => {
506 let mut inner_params = Vec::new();
508 render_filter(inner, params, &mut inner_params)?;
509 for p in inner_params {
510 if let Some(eq_pos) = p.find('=') {
511 let col = &p[..eq_pos];
512 let rest = &p[eq_pos + 1..];
513 output.push(format!("{}=not.{}", col, rest));
514 }
515 }
516 }
517 FilterCondition::Or(conditions) => {
518 let mut inner_parts = Vec::new();
519 for cond in conditions {
520 let mut sub = Vec::new();
521 render_filter(cond, params, &mut sub)?;
522 inner_parts.extend(sub);
523 }
524 let or_items: Vec<String> = inner_parts
526 .iter()
527 .map(|p| {
528 if let Some(eq_pos) = p.find('=') {
530 let col = &p[..eq_pos];
531 let rest = &p[eq_pos + 1..];
532 format!("{}.{}", col, rest)
533 } else {
534 p.clone()
535 }
536 })
537 .collect();
538 output.push(format!("or=({})", or_items.join(",")));
539 }
540 FilterCondition::And(conditions) => {
541 let mut inner_parts = Vec::new();
542 for cond in conditions {
543 let mut sub = Vec::new();
544 render_filter(cond, params, &mut sub)?;
545 inner_parts.extend(sub);
546 }
547 let and_items: Vec<String> = inner_parts
548 .iter()
549 .map(|p| {
550 if let Some(eq_pos) = p.find('=') {
551 let col = &p[..eq_pos];
552 let rest = &p[eq_pos + 1..];
553 format!("{}.{}", col, rest)
554 } else {
555 p.clone()
556 }
557 })
558 .collect();
559 output.push(format!("and=({})", and_items.join(",")));
560 }
561 FilterCondition::Raw(sql) => {
562 return Err(format!("Raw SQL filter '{}' cannot be used with PostgREST backend", sql));
564 }
565 FilterCondition::Match { conditions } => {
566 for (col, idx) in conditions {
567 let param = params.get(*idx - 1)
568 .ok_or_else(|| format!("Missing param at index {}", idx))?;
569 let val = render_param_value(param);
570 output.push(format!("{}=eq.{}", col, val));
571 }
572 }
573 }
574 Ok(())
575}
576
577fn render_filters_to_params(
578 filters: &[FilterCondition],
579 params: &ParamStore,
580 output: &mut Vec<String>,
581) -> Result<(), String> {
582 for filter in filters {
583 render_filter(filter, params, output)?;
584 }
585 Ok(())
586}
587
588fn build_insert_body(parts: &SqlParts, params: &ParamStore) -> Result<JsonValue, String> {
590 if parts.many_rows.is_empty() {
591 let mut obj = serde_json::Map::new();
593 for (col, idx) in &parts.set_clauses {
594 let param = params.get(*idx - 1)
595 .ok_or_else(|| format!("Missing param at index {}", idx))?;
596 obj.insert(col.clone(), param_to_json(param));
597 }
598 Ok(JsonValue::Object(obj))
599 } else {
600 let rows: Result<Vec<JsonValue>, String> = parts.many_rows.iter().map(|row| {
602 let mut obj = serde_json::Map::new();
603 for (col, idx) in row {
604 let param = params.get(*idx - 1)
605 .ok_or_else(|| format!("Missing param at index {}", idx))?;
606 obj.insert(col.clone(), param_to_json(param));
607 }
608 Ok(JsonValue::Object(obj))
609 }).collect();
610 Ok(JsonValue::Array(rows?))
611 }
612}
613
614fn build_set_body(set_clauses: &[(String, usize)], params: &ParamStore) -> Result<JsonValue, String> {
616 let mut obj = serde_json::Map::new();
617 for (col, idx) in set_clauses {
618 let param = params.get(*idx - 1)
619 .ok_or_else(|| format!("Missing param at index {}", idx))?;
620 obj.insert(col.clone(), param_to_json(param));
621 }
622 Ok(JsonValue::Object(obj))
623}
624
625fn build_explain_accept(opts: &ExplainOptions) -> HeaderValue {
626 let mut parts = vec!["application/vnd.pgrst.plan"];
627 if opts.analyze {
628 parts.push("+json; for=\"application/vnd.pgrst.plan+analyze\"");
629 }
630 HeaderValue::from_static("application/vnd.pgrst.plan+json")
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637 use crate::sql::*;
638
639 fn make_params(values: Vec<SqlParam>) -> ParamStore {
640 let mut store = ParamStore::new();
641 for v in values {
642 store.push(v);
643 }
644 store
645 }
646
647 #[test]
650 fn test_select_simple() {
651 let parts = SqlParts::new(SqlOperation::Select, "public", "cities");
652 let params = ParamStore::new();
653 let (url, _headers) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
654 assert_eq!(url, "http://localhost:64321/rest/v1/cities");
655 }
656
657 #[test]
658 fn test_select_with_columns() {
659 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
660 parts.select_columns = Some("\"name\", \"country_id\"".to_string());
661 let params = ParamStore::new();
662 let (url, _) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
663 assert_eq!(url, "http://localhost:64321/rest/v1/cities?select=name,country_id");
664 }
665
666 #[test]
667 fn test_select_with_eq_filter() {
668 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
669 parts.filters.push(FilterCondition::Comparison {
670 column: "name".to_string(),
671 operator: FilterOperator::Eq,
672 param_index: 1,
673 });
674 let params = make_params(vec![SqlParam::Text("Auckland".to_string())]);
675 let (url, _) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
676 assert_eq!(url, "http://localhost:64321/rest/v1/cities?name=eq.Auckland");
677 }
678
679 #[test]
680 fn test_select_with_multiple_filters() {
681 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
682 parts.filters.push(FilterCondition::Comparison {
683 column: "country_id".to_string(),
684 operator: FilterOperator::Eq,
685 param_index: 1,
686 });
687 parts.filters.push(FilterCondition::Comparison {
688 column: "population".to_string(),
689 operator: FilterOperator::Gt,
690 param_index: 2,
691 });
692 let params = make_params(vec![SqlParam::I32(1), SqlParam::I64(100000)]);
693 let (url, _) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
694 assert!(url.contains("country_id=eq.1"));
695 assert!(url.contains("population=gt.100000"));
696 }
697
698 #[test]
699 fn test_select_with_order() {
700 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
701 parts.orders.push(OrderClause {
702 column: "name".to_string(),
703 direction: OrderDirection::Ascending,
704 nulls: None,
705 });
706 let params = ParamStore::new();
707 let (url, _) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
708 assert_eq!(url, "http://localhost:64321/rest/v1/cities?order=name.asc");
709 }
710
711 #[test]
712 fn test_select_with_order_nulls() {
713 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
714 parts.orders.push(OrderClause {
715 column: "name".to_string(),
716 direction: OrderDirection::Descending,
717 nulls: Some(NullsPosition::Last),
718 });
719 let params = ParamStore::new();
720 let (url, _) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
721 assert!(url.contains("order=name.desc.nullslast"));
722 }
723
724 #[test]
725 fn test_select_with_limit() {
726 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
727 parts.limit = Some(10);
728 let params = ParamStore::new();
729 let (url, headers) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
730 assert!(url.contains("limit=10"));
731 assert!(headers.contains_key("Range"));
732 }
733
734 #[test]
735 fn test_select_with_limit_offset() {
736 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
737 parts.limit = Some(10);
738 parts.offset = Some(5);
739 let params = ParamStore::new();
740 let (url, headers) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
741 assert!(url.contains("limit=10"));
742 assert!(url.contains("offset=5"));
743 assert_eq!(headers.get("Range").unwrap(), "5-14");
744 }
745
746 #[test]
747 fn test_select_single() {
748 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
749 parts.single = true;
750 let params = ParamStore::new();
751 let (_, headers) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
752 assert_eq!(
753 headers.get("Accept").unwrap(),
754 "application/vnd.pgrst.object+json"
755 );
756 }
757
758 #[test]
759 fn test_select_count() {
760 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
761 parts.count = CountOption::Exact;
762 let params = ParamStore::new();
763 let (_, headers) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
764 assert_eq!(headers.get("Prefer").unwrap(), "count=exact");
765 }
766
767 #[test]
768 fn test_select_head_mode() {
769 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
770 parts.head = true;
771 let params = ParamStore::new();
772 let (_, headers) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
773 assert_eq!(headers.get("Prefer").unwrap(), "count=exact");
774 }
775
776 #[test]
779 fn test_filter_is_null() {
780 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
781 parts.filters.push(FilterCondition::Is {
782 column: "deleted_at".to_string(),
783 value: IsValue::Null,
784 });
785 let params = ParamStore::new();
786 let (url, _) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
787 assert!(url.contains("deleted_at=is.null"));
788 }
789
790 #[test]
791 fn test_filter_in() {
792 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
793 parts.filters.push(FilterCondition::In {
794 column: "id".to_string(),
795 param_indices: vec![1, 2, 3],
796 });
797 let params = make_params(vec![
798 SqlParam::I32(1),
799 SqlParam::I32(2),
800 SqlParam::I32(3),
801 ]);
802 let (url, _) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
803 assert!(url.contains("id=in.(1,2,3)"));
804 }
805
806 #[test]
807 fn test_filter_like() {
808 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
809 parts.filters.push(FilterCondition::Pattern {
810 column: "name".to_string(),
811 operator: PatternOperator::Like,
812 param_index: 1,
813 });
814 let params = make_params(vec![SqlParam::Text("%auck%".to_string())]);
815 let (url, _) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
816 assert!(url.contains("name=like.%auck%"));
817 }
818
819 #[test]
820 fn test_filter_ilike() {
821 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
822 parts.filters.push(FilterCondition::Pattern {
823 column: "name".to_string(),
824 operator: PatternOperator::ILike,
825 param_index: 1,
826 });
827 let params = make_params(vec![SqlParam::Text("%auck%".to_string())]);
828 let (url, _) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
829 assert!(url.contains("name=ilike.%auck%"));
830 }
831
832 #[test]
833 fn test_filter_text_search() {
834 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
835 parts.filters.push(FilterCondition::TextSearch {
836 column: "fts".to_string(),
837 query_param_index: 1,
838 config: Some("english".to_string()),
839 search_type: TextSearchType::Plain,
840 });
841 let params = make_params(vec![SqlParam::Text("hello".to_string())]);
842 let (url, _) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
843 assert!(url.contains("fts=plfts(english).hello"));
844 }
845
846 #[test]
847 fn test_filter_contains() {
848 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
849 parts.filters.push(FilterCondition::ArrayRange {
850 column: "tags".to_string(),
851 operator: ArrayRangeOperator::Contains,
852 param_index: 1,
853 });
854 let params = make_params(vec![SqlParam::TextArray(vec!["a".to_string(), "b".to_string()])]);
855 let (url, _) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
856 assert!(url.contains("tags=cs.{\"a\",\"b\"}"));
857 }
858
859 #[test]
860 fn test_filter_not() {
861 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
862 parts.filters.push(FilterCondition::Not(Box::new(
863 FilterCondition::Comparison {
864 column: "active".to_string(),
865 operator: FilterOperator::Eq,
866 param_index: 1,
867 },
868 )));
869 let params = make_params(vec![SqlParam::Bool(true)]);
870 let (url, _) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
871 assert!(url.contains("active=not.eq.true"));
872 }
873
874 #[test]
875 fn test_filter_or() {
876 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
877 parts.filters.push(FilterCondition::Or(vec![
878 FilterCondition::Comparison {
879 column: "name".to_string(),
880 operator: FilterOperator::Eq,
881 param_index: 1,
882 },
883 FilterCondition::Comparison {
884 column: "name".to_string(),
885 operator: FilterOperator::Eq,
886 param_index: 2,
887 },
888 ]));
889 let params = make_params(vec![
890 SqlParam::Text("Auckland".to_string()),
891 SqlParam::Text("Wellington".to_string()),
892 ]);
893 let (url, _) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
894 assert!(url.contains("or=(name.eq.Auckland,name.eq.Wellington)"));
895 }
896
897 #[test]
898 fn test_filter_match() {
899 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
900 parts.filters.push(FilterCondition::Match {
901 conditions: vec![
902 ("name".to_string(), 1),
903 ("country_id".to_string(), 2),
904 ],
905 });
906 let params = make_params(vec![
907 SqlParam::Text("Auckland".to_string()),
908 SqlParam::I32(1),
909 ]);
910 let (url, _) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
911 assert!(url.contains("name=eq.Auckland"));
912 assert!(url.contains("country_id=eq.1"));
913 }
914
915 #[test]
916 fn test_raw_filter_errors() {
917 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
918 parts.filters.push(FilterCondition::Raw("1=1".to_string()));
919 let params = ParamStore::new();
920 let result = build_postgrest_select("http://localhost:64321", &parts, ¶ms);
921 assert!(result.is_err());
922 }
923
924 #[test]
927 fn test_insert_single() {
928 let mut parts = SqlParts::new(SqlOperation::Insert, "public", "cities");
929 parts.set_clauses = vec![
930 ("name".to_string(), 1),
931 ("country_id".to_string(), 2),
932 ];
933 parts.returning = Some("*".to_string());
934 let params = make_params(vec![
935 SqlParam::Text("Auckland".to_string()),
936 SqlParam::I32(1),
937 ]);
938 let (url, headers, body) = build_postgrest_insert("http://localhost:64321", &parts, ¶ms).unwrap();
939 assert_eq!(url, "http://localhost:64321/rest/v1/cities");
940 assert_eq!(headers.get("Prefer").unwrap(), "return=representation");
941 assert_eq!(body["name"], "Auckland");
942 assert_eq!(body["country_id"], 1);
943 }
944
945 #[test]
946 fn test_insert_many() {
947 let mut parts = SqlParts::new(SqlOperation::Insert, "public", "cities");
948 parts.many_rows = vec![
949 vec![("name".to_string(), 1), ("country_id".to_string(), 2)],
950 vec![("name".to_string(), 3), ("country_id".to_string(), 4)],
951 ];
952 let params = make_params(vec![
953 SqlParam::Text("Auckland".to_string()),
954 SqlParam::I32(1),
955 SqlParam::Text("Wellington".to_string()),
956 SqlParam::I32(1),
957 ]);
958 let (_, _, body) = build_postgrest_insert("http://localhost:64321", &parts, ¶ms).unwrap();
959 assert!(body.is_array());
960 assert_eq!(body.as_array().unwrap().len(), 2);
961 }
962
963 #[test]
966 fn test_update_with_filter() {
967 let mut parts = SqlParts::new(SqlOperation::Update, "public", "cities");
968 parts.set_clauses = vec![("name".to_string(), 1)];
969 parts.filters.push(FilterCondition::Comparison {
970 column: "id".to_string(),
971 operator: FilterOperator::Eq,
972 param_index: 2,
973 });
974 parts.returning = Some("*".to_string());
975 let params = make_params(vec![
976 SqlParam::Text("New Auckland".to_string()),
977 SqlParam::I32(1),
978 ]);
979 let (url, headers, body) = build_postgrest_update("http://localhost:64321", &parts, ¶ms).unwrap();
980 assert!(url.contains("id=eq.1"));
981 assert_eq!(headers.get("Prefer").unwrap(), "return=representation");
982 assert_eq!(body["name"], "New Auckland");
983 }
984
985 #[test]
988 fn test_delete_with_filter() {
989 let mut parts = SqlParts::new(SqlOperation::Delete, "public", "cities");
990 parts.filters.push(FilterCondition::Comparison {
991 column: "id".to_string(),
992 operator: FilterOperator::Eq,
993 param_index: 1,
994 });
995 parts.returning = Some("*".to_string());
996 let params = make_params(vec![SqlParam::I32(1)]);
997 let (url, headers) = build_postgrest_delete("http://localhost:64321", &parts, ¶ms).unwrap();
998 assert!(url.contains("id=eq.1"));
999 assert_eq!(headers.get("Prefer").unwrap(), "return=representation");
1000 }
1001
1002 #[test]
1005 fn test_upsert_merge_duplicates() {
1006 let mut parts = SqlParts::new(SqlOperation::Upsert, "public", "cities");
1007 parts.set_clauses = vec![
1008 ("id".to_string(), 1),
1009 ("name".to_string(), 2),
1010 ];
1011 parts.conflict_columns = vec!["id".to_string()];
1012 parts.returning = Some("*".to_string());
1013 let params = make_params(vec![SqlParam::I32(1), SqlParam::Text("Auckland".to_string())]);
1014 let (url, headers, _) = build_postgrest_upsert("http://localhost:64321", &parts, ¶ms).unwrap();
1015 assert!(url.contains("on_conflict=id"));
1016 let prefer = headers.get("Prefer").unwrap().to_str().unwrap();
1017 assert!(prefer.contains("resolution=merge-duplicates"));
1018 assert!(prefer.contains("return=representation"));
1019 }
1020
1021 #[test]
1022 fn test_upsert_ignore_duplicates() {
1023 let mut parts = SqlParts::new(SqlOperation::Upsert, "public", "cities");
1024 parts.set_clauses = vec![
1025 ("id".to_string(), 1),
1026 ("name".to_string(), 2),
1027 ];
1028 parts.conflict_columns = vec!["id".to_string()];
1029 parts.ignore_duplicates = true;
1030 let params = make_params(vec![SqlParam::I32(1), SqlParam::Text("Auckland".to_string())]);
1031 let (_, headers, _) = build_postgrest_upsert("http://localhost:64321", &parts, ¶ms).unwrap();
1032 let prefer = headers.get("Prefer").unwrap().to_str().unwrap();
1033 assert!(prefer.contains("resolution=ignore-duplicates"));
1034 }
1035
1036 #[test]
1039 fn test_rpc_simple() {
1040 let args = serde_json::json!({"name": "Auckland"});
1041 let (url, headers, body) = build_postgrest_rpc("http://localhost:64321", "get_city", &args, false);
1042 assert_eq!(url, "http://localhost:64321/rest/v1/rpc/get_city");
1043 assert_eq!(headers.get("Content-Type").unwrap(), "application/json");
1044 assert_eq!(body["name"], "Auckland");
1045 }
1046
1047 #[test]
1048 fn test_rpc_no_args() {
1049 let args = serde_json::json!(null);
1050 let (_, _, body) = build_postgrest_rpc("http://localhost:64321", "get_all", &args, false);
1051 assert!(body.is_object());
1052 assert!(body.as_object().unwrap().is_empty());
1053 }
1054
1055 #[test]
1056 fn test_rpc_rollback() {
1057 let args = serde_json::json!({"name": "Auckland"});
1058 let (_, headers, _) = build_postgrest_rpc("http://localhost:64321", "get_city", &args, true);
1059 assert_eq!(headers.get("Prefer").unwrap(), "tx=rollback");
1060 }
1061
1062 #[test]
1063 fn test_rpc_no_rollback_no_prefer() {
1064 let args = serde_json::json!({"name": "Auckland"});
1065 let (_, headers, _) = build_postgrest_rpc("http://localhost:64321", "get_city", &args, false);
1066 assert!(headers.get("Prefer").is_none());
1067 }
1068
1069 #[test]
1072 fn test_render_param_null() {
1073 assert_eq!(render_param_value(&SqlParam::Null), "null");
1074 }
1075
1076 #[test]
1077 fn test_render_param_bool() {
1078 assert_eq!(render_param_value(&SqlParam::Bool(true)), "true");
1079 assert_eq!(render_param_value(&SqlParam::Bool(false)), "false");
1080 }
1081
1082 #[test]
1083 fn test_render_param_numbers() {
1084 assert_eq!(render_param_value(&SqlParam::I32(42)), "42");
1085 assert_eq!(render_param_value(&SqlParam::I64(1000000)), "1000000");
1086 assert_eq!(render_param_value(&SqlParam::F64(3.14)), "3.14");
1087 }
1088
1089 #[test]
1090 fn test_render_param_text() {
1091 assert_eq!(render_param_value(&SqlParam::Text("hello".to_string())), "hello");
1092 }
1093
1094 #[test]
1095 fn test_render_param_uuid() {
1096 let uuid = uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
1097 assert_eq!(
1098 render_param_value(&SqlParam::Uuid(uuid)),
1099 "550e8400-e29b-41d4-a716-446655440000"
1100 );
1101 }
1102
1103 #[test]
1106 fn test_select_count_planned() {
1107 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
1108 parts.count = CountOption::Planned;
1109 let params = ParamStore::new();
1110 let (_, headers) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
1111 assert_eq!(headers.get("Prefer").unwrap(), "count=planned");
1112 }
1113
1114 #[test]
1115 fn test_select_count_estimated() {
1116 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
1117 parts.count = CountOption::Estimated;
1118 let params = ParamStore::new();
1119 let (_, headers) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
1120 assert_eq!(headers.get("Prefer").unwrap(), "count=estimated");
1121 }
1122
1123 #[test]
1124 fn test_select_count_and_head_compose() {
1125 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
1126 parts.count = CountOption::Exact;
1127 parts.head = true;
1128 let params = ParamStore::new();
1129 let (_, headers) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
1130 let prefer = headers.get("Prefer").unwrap().to_str().unwrap();
1132 assert!(prefer.contains("count=exact"));
1133 }
1134
1135 #[test]
1136 fn test_insert_return_and_count_compose() {
1137 let mut parts = SqlParts::new(SqlOperation::Insert, "public", "cities");
1138 parts.set_clauses = vec![("name".to_string(), 1)];
1139 parts.returning = Some("*".to_string());
1140 parts.count = CountOption::Exact;
1141 let params = make_params(vec![SqlParam::Text("Auckland".to_string())]);
1142 let (_, headers, _) = build_postgrest_insert("http://localhost:64321", &parts, ¶ms).unwrap();
1143 let prefer = headers.get("Prefer").unwrap().to_str().unwrap();
1144 assert!(prefer.contains("return=representation"));
1145 assert!(prefer.contains("count=exact"));
1146 }
1147
1148 #[test]
1149 fn test_update_return_and_count_compose() {
1150 let mut parts = SqlParts::new(SqlOperation::Update, "public", "cities");
1151 parts.set_clauses = vec![("name".to_string(), 1)];
1152 parts.returning = Some("*".to_string());
1153 parts.count = CountOption::Planned;
1154 let params = make_params(vec![SqlParam::Text("Auckland".to_string())]);
1155 let (_, headers, _) = build_postgrest_update("http://localhost:64321", &parts, ¶ms).unwrap();
1156 let prefer = headers.get("Prefer").unwrap().to_str().unwrap();
1157 assert!(prefer.contains("return=representation"));
1158 assert!(prefer.contains("count=planned"));
1159 }
1160
1161 #[test]
1162 fn test_delete_return_and_count_compose() {
1163 let mut parts = SqlParts::new(SqlOperation::Delete, "public", "cities");
1164 parts.returning = Some("*".to_string());
1165 parts.count = CountOption::Estimated;
1166 let params = ParamStore::new();
1167 let (_, headers) = build_postgrest_delete("http://localhost:64321", &parts, ¶ms).unwrap();
1168 let prefer = headers.get("Prefer").unwrap().to_str().unwrap();
1169 assert!(prefer.contains("return=representation"));
1170 assert!(prefer.contains("count=estimated"));
1171 }
1172
1173 #[test]
1174 fn test_upsert_with_count() {
1175 let mut parts = SqlParts::new(SqlOperation::Upsert, "public", "cities");
1176 parts.set_clauses = vec![("id".to_string(), 1), ("name".to_string(), 2)];
1177 parts.conflict_columns = vec!["id".to_string()];
1178 parts.returning = Some("*".to_string());
1179 parts.count = CountOption::Exact;
1180 let params = make_params(vec![SqlParam::I32(1), SqlParam::Text("Auckland".to_string())]);
1181 let (_, headers, _) = build_postgrest_upsert("http://localhost:64321", &parts, ¶ms).unwrap();
1182 let prefer = headers.get("Prefer").unwrap().to_str().unwrap();
1183 assert!(prefer.contains("resolution=merge-duplicates"));
1184 assert!(prefer.contains("return=representation"));
1185 assert!(prefer.contains("count=exact"));
1186 }
1187
1188 #[test]
1189 fn test_schema_override_select() {
1190 let mut parts = SqlParts::new(SqlOperation::Select, "public", "cities");
1191 parts.schema_override = Some("custom".to_string());
1192 let params = ParamStore::new();
1193 let (_, headers) = build_postgrest_select("http://localhost:64321", &parts, ¶ms).unwrap();
1194 assert_eq!(headers.get("Accept-Profile").unwrap(), "custom");
1195 }
1196
1197 #[test]
1198 fn test_schema_override_insert() {
1199 let mut parts = SqlParts::new(SqlOperation::Insert, "public", "cities");
1200 parts.schema_override = Some("custom".to_string());
1201 parts.set_clauses = vec![("name".to_string(), 1)];
1202 let params = make_params(vec![SqlParam::Text("Auckland".to_string())]);
1203 let (_, headers, _) = build_postgrest_insert("http://localhost:64321", &parts, ¶ms).unwrap();
1204 assert_eq!(headers.get("Content-Profile").unwrap(), "custom");
1205 }
1206}