Skip to main content

reinhardt_db/orm/
postgres_features.rs

1//! PostgreSQL-specific advanced features
2//!
3//! This module provides PostgreSQL-specific advanced query features inspired by
4//! Django's `django/contrib/postgres/aggregates/` and `django/contrib/postgres/search/`.
5//!
6//! # Available Features
7//!
8//! - **ArrayAgg**: Array aggregation function
9//! - **JsonbBuildObject**: JSONB object construction
10//! - **FullTextSearch**: Full-text search functionality
11//! - **ArrayOverlap**: Array overlap operations
12//!
13//! # Example
14//!
15//! ```rust
16//! use reinhardt_db::orm::{ArrayAgg, FullTextSearch};
17//!
18//! // Aggregate values into an array
19//! let agg = ArrayAgg::<String>::new("tags".to_string()).distinct();
20//! assert!(agg.to_sql().contains("ARRAY_AGG(DISTINCT"));
21//!
22//! // Full-text search
23//! let search = FullTextSearch::new("content".to_string(), "rust programming".to_string());
24//! assert!(search.to_sql().contains("to_tsvector"));
25//! ```
26
27use serde::{Deserialize, Serialize};
28use std::marker::PhantomData;
29
30/// PostgreSQL ARRAY_AGG aggregation function
31///
32/// Aggregates values into a PostgreSQL array.
33///
34/// # Example
35///
36/// ```rust
37/// use reinhardt_db::orm::ArrayAgg;
38///
39/// let agg = ArrayAgg::<i32>::new("score".to_string());
40/// assert_eq!(agg.to_sql(), "ARRAY_AGG(score)");
41///
42/// let distinct_agg = ArrayAgg::<String>::new("category".to_string()).distinct();
43/// assert_eq!(distinct_agg.to_sql(), "ARRAY_AGG(DISTINCT category)");
44/// ```
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ArrayAgg<T> {
47	field: String,
48	distinct: bool,
49	ordering: Option<Vec<String>>,
50	_phantom: PhantomData<T>,
51}
52
53impl<T> ArrayAgg<T> {
54	/// Create a new ArrayAgg for the specified field
55	///
56	/// # Example
57	///
58	/// ```rust
59	/// use reinhardt_db::orm::ArrayAgg;
60	///
61	/// let agg = ArrayAgg::<String>::new("name".to_string());
62	/// assert_eq!(agg.to_sql(), "ARRAY_AGG(name)");
63	/// ```
64	pub fn new(field: String) -> Self {
65		Self {
66			field,
67			distinct: false,
68			ordering: None,
69			_phantom: PhantomData,
70		}
71	}
72
73	/// Apply DISTINCT to the aggregation
74	///
75	/// # Example
76	///
77	/// ```rust
78	/// use reinhardt_db::orm::ArrayAgg;
79	///
80	/// let agg = ArrayAgg::<i32>::new("id".to_string()).distinct();
81	/// assert!(agg.to_sql().contains("DISTINCT"));
82	/// ```
83	pub fn distinct(mut self) -> Self {
84		self.distinct = true;
85		self
86	}
87
88	/// Add ORDER BY clause to the aggregation
89	///
90	/// # Example
91	///
92	/// ```rust
93	/// use reinhardt_db::orm::ArrayAgg;
94	///
95	/// let agg = ArrayAgg::<String>::new("name".to_string())
96	///     .order_by(vec!["created_at DESC".to_string()]);
97	/// assert!(agg.to_sql().contains("ORDER BY"));
98	/// ```
99	pub fn order_by(mut self, fields: Vec<String>) -> Self {
100		self.ordering = Some(fields);
101		self
102	}
103
104	/// Generate SQL for this aggregation
105	pub fn to_sql(&self) -> String {
106		let mut sql = String::from("ARRAY_AGG(");
107
108		if self.distinct {
109			sql.push_str("DISTINCT ");
110		}
111
112		sql.push_str(&self.field);
113
114		if let Some(ref ordering) = self.ordering {
115			sql.push_str(" ORDER BY ");
116			sql.push_str(&ordering.join(", "));
117		}
118
119		sql.push(')');
120		sql
121	}
122
123	/// Apply a transformation to every field-bearing argument.
124	pub fn map_fields(&mut self, mut map: impl FnMut(&mut String)) {
125		map(&mut self.field);
126		if let Some(ordering) = &mut self.ordering {
127			for field in ordering {
128				map(field);
129			}
130		}
131	}
132}
133
134/// PostgreSQL JSONB_BUILD_OBJECT function
135///
136/// Constructs a JSONB object from key-value pairs.
137///
138/// # Example
139///
140/// ```rust
141/// use reinhardt_db::orm::JsonbBuildObject;
142///
143/// let builder = JsonbBuildObject::new()
144///     .add("id", "user_id")
145///     .add("name", "user_name");
146/// assert!(builder.to_sql().contains("jsonb_build_object"));
147/// ```
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct JsonbBuildObject {
150	pairs: Vec<(String, String)>,
151}
152
153impl JsonbBuildObject {
154	/// Create a new JSONB object builder
155	///
156	/// # Example
157	///
158	/// ```rust
159	/// use reinhardt_db::orm::JsonbBuildObject;
160	///
161	/// let builder = JsonbBuildObject::new();
162	/// assert_eq!(builder.to_sql(), "jsonb_build_object()");
163	/// ```
164	pub fn new() -> Self {
165		Self { pairs: Vec::new() }
166	}
167
168	/// Add a key-value pair to the JSONB object
169	///
170	/// # Example
171	///
172	/// ```rust
173	/// use reinhardt_db::orm::JsonbBuildObject;
174	///
175	/// let builder = JsonbBuildObject::new()
176	///     .add("user_id", "id")
177	///     .add("user_name", "name");
178	/// let sql = builder.to_sql();
179	/// assert!(sql.contains("'user_id'"));
180	/// assert!(sql.contains("id"));
181	/// ```
182	pub fn add(mut self, key: &str, value_field: &str) -> Self {
183		self.pairs.push((key.to_string(), value_field.to_string()));
184		self
185	}
186
187	/// Generate SQL for this JSONB object construction
188	pub fn to_sql(&self) -> String {
189		let mut sql = String::from("jsonb_build_object(");
190
191		let parts: Vec<String> = self
192			.pairs
193			.iter()
194			.flat_map(|(k, v)| vec![format!("'{}'", k), v.clone()])
195			.collect();
196
197		sql.push_str(&parts.join(", "));
198		sql.push(')');
199		sql
200	}
201
202	/// Apply a transformation to every value field in the object.
203	pub fn map_fields(&mut self, mut map: impl FnMut(&mut String)) {
204		for (_, field) in &mut self.pairs {
205			map(field);
206		}
207	}
208}
209
210impl Default for JsonbBuildObject {
211	fn default() -> Self {
212		Self::new()
213	}
214}
215
216/// PostgreSQL Full-Text Search
217///
218/// Provides full-text search capabilities using PostgreSQL's tsvector and tsquery.
219///
220/// # Example
221///
222/// ```rust
223/// use reinhardt_db::orm::FullTextSearch;
224///
225/// let search = FullTextSearch::new("content".to_string(), "rust programming".to_string());
226/// assert!(search.to_sql().contains("to_tsvector"));
227/// assert!(search.to_sql().contains("to_tsquery"));
228/// ```
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct FullTextSearch {
231	vector_field: String,
232	query: String,
233	config: String,
234}
235
236impl FullTextSearch {
237	/// Create a new full-text search with default English configuration
238	///
239	/// # Example
240	///
241	/// ```rust
242	/// use reinhardt_db::orm::FullTextSearch;
243	///
244	/// let search = FullTextSearch::new("title".to_string(), "database".to_string());
245	/// assert_eq!(search.config(), "english");
246	/// ```
247	pub fn new(field: String, query: String) -> Self {
248		Self {
249			vector_field: field,
250			query,
251			config: "english".to_string(),
252		}
253	}
254
255	/// Set a custom text search configuration (language)
256	///
257	/// # Example
258	///
259	/// ```rust
260	/// use reinhardt_db::orm::FullTextSearch;
261	///
262	/// let search = FullTextSearch::new("content".to_string(), "bonjour".to_string())
263	///     .with_config("french".to_string());
264	/// assert_eq!(search.config(), "french");
265	/// ```
266	pub fn with_config(mut self, config: String) -> Self {
267		self.config = config;
268		self
269	}
270
271	/// Get the current configuration
272	pub fn config(&self) -> &str {
273		&self.config
274	}
275
276	/// Generate SQL for this full-text search
277	///
278	/// # Example
279	///
280	/// ```rust
281	/// use reinhardt_db::orm::FullTextSearch;
282	///
283	/// let search = FullTextSearch::new("body".to_string(), "rust".to_string());
284	/// let sql = search.to_sql();
285	/// assert!(sql.contains("to_tsvector('english', body)"));
286	/// assert!(sql.contains("to_tsquery('english', 'rust')"));
287	/// ```
288	pub fn to_sql(&self) -> String {
289		format!(
290			"to_tsvector('{}', {}) @@ to_tsquery('{}', '{}')",
291			self.config, self.vector_field, self.config, self.query
292		)
293	}
294}
295
296/// PostgreSQL STRING_AGG aggregation function
297///
298/// Aggregates string values into a single string with a specified separator.
299///
300/// # Example
301///
302/// ```rust
303/// use reinhardt_db::orm::StringAgg;
304///
305/// let agg = StringAgg::new("name".to_string(), ", ".to_string());
306/// assert_eq!(agg.to_sql(), "STRING_AGG(name, ', ')");
307///
308/// let distinct_agg = StringAgg::new("category".to_string(), "; ".to_string()).distinct();
309/// assert_eq!(distinct_agg.to_sql(), "STRING_AGG(DISTINCT category, '; ')");
310/// ```
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct StringAgg {
313	field: String,
314	separator: String,
315	distinct: bool,
316	ordering: Option<Vec<String>>,
317}
318
319impl StringAgg {
320	/// Create a new StringAgg for the specified field with a separator
321	///
322	/// # Example
323	///
324	/// ```rust
325	/// use reinhardt_db::orm::StringAgg;
326	///
327	/// let agg = StringAgg::new("name".to_string(), ", ".to_string());
328	/// assert_eq!(agg.to_sql(), "STRING_AGG(name, ', ')");
329	/// ```
330	pub fn new(field: String, separator: String) -> Self {
331		Self {
332			field,
333			separator,
334			distinct: false,
335			ordering: None,
336		}
337	}
338
339	/// Apply DISTINCT to the aggregation
340	///
341	/// # Example
342	///
343	/// ```rust
344	/// use reinhardt_db::orm::StringAgg;
345	///
346	/// let agg = StringAgg::new("name".to_string(), ",".to_string()).distinct();
347	/// assert!(agg.to_sql().contains("DISTINCT"));
348	/// ```
349	pub fn distinct(mut self) -> Self {
350		self.distinct = true;
351		self
352	}
353
354	/// Add ORDER BY clause to the aggregation
355	///
356	/// # Example
357	///
358	/// ```rust
359	/// use reinhardt_db::orm::StringAgg;
360	///
361	/// let agg = StringAgg::new("name".to_string(), ", ".to_string())
362	///     .order_by(vec!["name ASC".to_string()]);
363	/// assert!(agg.to_sql().contains("ORDER BY"));
364	/// ```
365	pub fn order_by(mut self, fields: Vec<String>) -> Self {
366		self.ordering = Some(fields);
367		self
368	}
369
370	/// Generate SQL for this aggregation
371	pub fn to_sql(&self) -> String {
372		let mut sql = String::from("STRING_AGG(");
373
374		if self.distinct {
375			sql.push_str("DISTINCT ");
376		}
377
378		sql.push_str(&self.field);
379		sql.push_str(", '");
380		sql.push_str(&self.separator);
381		sql.push('\'');
382
383		if let Some(ref ordering) = self.ordering {
384			sql.push_str(" ORDER BY ");
385			sql.push_str(&ordering.join(", "));
386		}
387
388		sql.push(')');
389		sql
390	}
391
392	/// Apply a transformation to every field-bearing argument.
393	pub fn map_fields(&mut self, mut map: impl FnMut(&mut String)) {
394		map(&mut self.field);
395		if let Some(ordering) = &mut self.ordering {
396			for field in ordering {
397				map(field);
398			}
399		}
400	}
401}
402
403/// PostgreSQL JSONB_AGG aggregation function
404///
405/// Aggregates values into a JSONB array.
406///
407/// # Example
408///
409/// ```rust
410/// use reinhardt_db::orm::JsonbAgg;
411///
412/// let agg = JsonbAgg::new("user_data".to_string());
413/// assert_eq!(agg.to_sql(), "JSONB_AGG(user_data)");
414///
415/// let distinct_agg = JsonbAgg::new("category".to_string()).distinct();
416/// assert_eq!(distinct_agg.to_sql(), "JSONB_AGG(DISTINCT category)");
417/// ```
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct JsonbAgg {
420	expression: String,
421	distinct: bool,
422	ordering: Option<Vec<String>>,
423}
424
425impl JsonbAgg {
426	/// Create a new JsonbAgg for the specified expression
427	///
428	/// # Example
429	///
430	/// ```rust
431	/// use reinhardt_db::orm::JsonbAgg;
432	///
433	/// let agg = JsonbAgg::new("metadata".to_string());
434	/// assert_eq!(agg.to_sql(), "JSONB_AGG(metadata)");
435	/// ```
436	pub fn new(expression: String) -> Self {
437		Self {
438			expression,
439			distinct: false,
440			ordering: None,
441		}
442	}
443
444	/// Apply DISTINCT to the aggregation
445	///
446	/// # Example
447	///
448	/// ```rust
449	/// use reinhardt_db::orm::JsonbAgg;
450	///
451	/// let agg = JsonbAgg::new("data".to_string()).distinct();
452	/// assert!(agg.to_sql().contains("DISTINCT"));
453	/// ```
454	pub fn distinct(mut self) -> Self {
455		self.distinct = true;
456		self
457	}
458
459	/// Add ORDER BY clause to the aggregation
460	///
461	/// # Example
462	///
463	/// ```rust
464	/// use reinhardt_db::orm::JsonbAgg;
465	///
466	/// let agg = JsonbAgg::new("items".to_string())
467	///     .order_by(vec!["created_at DESC".to_string()]);
468	/// assert!(agg.to_sql().contains("ORDER BY"));
469	/// ```
470	pub fn order_by(mut self, fields: Vec<String>) -> Self {
471		self.ordering = Some(fields);
472		self
473	}
474
475	/// Generate SQL for this aggregation
476	pub fn to_sql(&self) -> String {
477		let mut sql = String::from("JSONB_AGG(");
478
479		if self.distinct {
480			sql.push_str("DISTINCT ");
481		}
482
483		sql.push_str(&self.expression);
484
485		if let Some(ref ordering) = self.ordering {
486			sql.push_str(" ORDER BY ");
487			sql.push_str(&ordering.join(", "));
488		}
489
490		sql.push(')');
491		sql
492	}
493
494	/// Apply a transformation to every field-bearing argument.
495	pub fn map_fields(&mut self, mut map: impl FnMut(&mut String)) {
496		map(&mut self.expression);
497		if let Some(ordering) = &mut self.ordering {
498			for field in ordering {
499				map(field);
500			}
501		}
502	}
503}
504
505/// PostgreSQL ts_rank function
506///
507/// Computes a ranking score for full-text search results based on how well
508/// a document matches a tsquery.
509///
510/// # Example
511///
512/// ```rust
513/// use reinhardt_db::orm::TsRank;
514///
515/// let rank = TsRank::new("search_vector".to_string(), "rust & programming".to_string());
516/// assert!(rank.to_sql().contains("ts_rank"));
517/// ```
518#[derive(Debug, Clone, Serialize, Deserialize)]
519pub struct TsRank {
520	vector_field: String,
521	query: String,
522	config: String,
523	normalization: Option<i32>,
524}
525
526impl TsRank {
527	/// Create a new TsRank for the specified tsvector field and query
528	///
529	/// # Example
530	///
531	/// ```rust
532	/// use reinhardt_db::orm::TsRank;
533	///
534	/// let rank = TsRank::new("content_vector".to_string(), "database".to_string());
535	/// let sql = rank.to_sql();
536	/// assert!(sql.contains("ts_rank"));
537	/// ```
538	pub fn new(vector_field: String, query: String) -> Self {
539		Self {
540			vector_field,
541			query,
542			config: "english".to_string(),
543			normalization: None,
544		}
545	}
546
547	/// Set a custom text search configuration (language)
548	///
549	/// # Example
550	///
551	/// ```rust
552	/// use reinhardt_db::orm::TsRank;
553	///
554	/// let rank = TsRank::new("content".to_string(), "bonjour".to_string())
555	///     .with_config("french".to_string());
556	/// let sql = rank.to_sql();
557	/// assert!(sql.contains("french"));
558	/// ```
559	pub fn with_config(mut self, config: String) -> Self {
560		self.config = config;
561		self
562	}
563
564	/// Set normalization option
565	///
566	/// Normalization values:
567	/// - 0: ignore document length
568	/// - 1: divide the rank by 1 + log(document length)
569	/// - 2: divide the rank by the document length
570	/// - 4: divide the rank by the mean harmonic distance between extents
571	/// - 8: divide the rank by the number of unique words in document
572	/// - 16: divide the rank by 1 + log(number of unique words)
573	/// - 32: divide the rank by itself + 1
574	///
575	/// Multiple values can be combined using bitwise OR.
576	///
577	/// # Example
578	///
579	/// ```rust
580	/// use reinhardt_db::orm::TsRank;
581	///
582	/// let rank = TsRank::new("content".to_string(), "rust".to_string())
583	///     .with_normalization(2);
584	/// let sql = rank.to_sql();
585	/// assert!(sql.contains(", 2)"));
586	/// ```
587	pub fn with_normalization(mut self, norm: i32) -> Self {
588		self.normalization = Some(norm);
589		self
590	}
591
592	/// Get the current configuration
593	pub fn config(&self) -> &str {
594		&self.config
595	}
596
597	/// Generate SQL for this ranking function
598	///
599	/// # Example
600	///
601	/// ```rust
602	/// use reinhardt_db::orm::TsRank;
603	///
604	/// let rank = TsRank::new("search_vec".to_string(), "rust".to_string());
605	/// let sql = rank.to_sql();
606	/// assert!(sql.contains("ts_rank(search_vec, to_tsquery('english', 'rust'))"));
607	/// ```
608	pub fn to_sql(&self) -> String {
609		let tsquery = format!("to_tsquery('{}', '{}')", self.config, self.query);
610
611		match self.normalization {
612			Some(norm) => format!("ts_rank({}, {}, {})", self.vector_field, tsquery, norm),
613			None => format!("ts_rank({}, {})", self.vector_field, tsquery),
614		}
615	}
616
617	/// Apply a transformation to the document vector field.
618	pub fn map_fields(&mut self, mut map: impl FnMut(&mut String)) {
619		map(&mut self.vector_field);
620	}
621}
622
623/// PostgreSQL Array Overlap Operator
624///
625/// Tests whether two arrays have any elements in common.
626///
627/// # Example
628///
629/// ```rust
630/// use reinhardt_db::orm::ArrayOverlap;
631///
632/// let overlap = ArrayOverlap::new("tags".to_string(), vec!["rust".to_string(), "web".to_string()]);
633/// assert!(overlap.to_sql().contains("&&"));
634/// ```
635#[derive(Debug, Clone, Serialize, Deserialize)]
636pub struct ArrayOverlap {
637	field: String,
638	values: Vec<String>,
639}
640
641impl ArrayOverlap {
642	/// Create a new array overlap check
643	///
644	/// # Example
645	///
646	/// ```rust
647	/// use reinhardt_db::orm::ArrayOverlap;
648	///
649	/// let overlap = ArrayOverlap::new(
650	///     "categories".to_string(),
651	///     vec!["tech".to_string(), "science".to_string()]
652	/// );
653	/// assert!(overlap.to_sql().contains("ARRAY"));
654	/// ```
655	pub fn new(field: String, values: Vec<String>) -> Self {
656		Self { field, values }
657	}
658
659	/// Generate SQL for the array overlap check
660	pub fn to_sql(&self) -> String {
661		let array_literal = format!(
662			"ARRAY[{}]",
663			self.values
664				.iter()
665				.map(|v| format!("'{}'", v))
666				.collect::<Vec<_>>()
667				.join(", ")
668		);
669		format!("{} && {}", self.field, array_literal)
670	}
671}
672
673#[cfg(test)]
674mod tests {
675	use super::*;
676
677	#[test]
678	fn test_array_agg_basic() {
679		let agg = ArrayAgg::<i32>::new("score".to_string());
680		assert_eq!(agg.to_sql(), "ARRAY_AGG(score)");
681	}
682
683	#[test]
684	fn test_array_agg_distinct() {
685		let agg = ArrayAgg::<String>::new("category".to_string()).distinct();
686		assert_eq!(agg.to_sql(), "ARRAY_AGG(DISTINCT category)");
687	}
688
689	#[test]
690	fn test_array_agg_with_ordering() {
691		let agg =
692			ArrayAgg::<i32>::new("id".to_string()).order_by(vec!["created_at DESC".to_string()]);
693		assert_eq!(agg.to_sql(), "ARRAY_AGG(id ORDER BY created_at DESC)");
694	}
695
696	#[test]
697	fn test_array_agg_distinct_with_ordering() {
698		let agg = ArrayAgg::<String>::new("name".to_string())
699			.distinct()
700			.order_by(vec!["name ASC".to_string(), "id DESC".to_string()]);
701		assert_eq!(
702			agg.to_sql(),
703			"ARRAY_AGG(DISTINCT name ORDER BY name ASC, id DESC)"
704		);
705	}
706
707	#[test]
708	fn test_jsonb_build_object_empty() {
709		let builder = JsonbBuildObject::new();
710		assert_eq!(builder.to_sql(), "jsonb_build_object()");
711	}
712
713	#[test]
714	fn test_jsonb_build_object_single_pair() {
715		let builder = JsonbBuildObject::new().add("id", "user_id");
716		assert_eq!(builder.to_sql(), "jsonb_build_object('id', user_id)");
717	}
718
719	#[test]
720	fn test_jsonb_build_object_multiple_pairs() {
721		let builder = JsonbBuildObject::new()
722			.add("id", "user_id")
723			.add("name", "user_name")
724			.add("email", "user_email");
725		assert_eq!(
726			builder.to_sql(),
727			"jsonb_build_object('id', user_id, 'name', user_name, 'email', user_email)"
728		);
729	}
730
731	#[test]
732	fn test_full_text_search_basic() {
733		let search = FullTextSearch::new("content".to_string(), "rust".to_string());
734		assert_eq!(
735			search.to_sql(),
736			"to_tsvector('english', content) @@ to_tsquery('english', 'rust')"
737		);
738	}
739
740	#[test]
741	fn test_full_text_search_custom_config() {
742		let search = FullTextSearch::new("title".to_string(), "database".to_string())
743			.with_config("french".to_string());
744		assert_eq!(
745			search.to_sql(),
746			"to_tsvector('french', title) @@ to_tsquery('french', 'database')"
747		);
748	}
749
750	#[test]
751	fn test_full_text_search_complex_query() {
752		let search = FullTextSearch::new("body".to_string(), "rust & programming".to_string());
753		let sql = search.to_sql();
754		assert!(sql.contains("to_tsvector('english', body)"));
755		assert!(sql.contains("to_tsquery('english', 'rust & programming')"));
756	}
757
758	#[test]
759	fn test_array_overlap_basic() {
760		let overlap = ArrayOverlap::new(
761			"tags".to_string(),
762			vec!["rust".to_string(), "web".to_string()],
763		);
764		assert_eq!(overlap.to_sql(), "tags && ARRAY['rust', 'web']");
765	}
766
767	#[test]
768	fn test_array_overlap_single_value() {
769		let overlap = ArrayOverlap::new("categories".to_string(), vec!["tech".to_string()]);
770		assert_eq!(overlap.to_sql(), "categories && ARRAY['tech']");
771	}
772
773	#[test]
774	fn test_array_overlap_multiple_values() {
775		let overlap = ArrayOverlap::new(
776			"labels".to_string(),
777			vec![
778				"important".to_string(),
779				"urgent".to_string(),
780				"reviewed".to_string(),
781			],
782		);
783		assert_eq!(
784			overlap.to_sql(),
785			"labels && ARRAY['important', 'urgent', 'reviewed']"
786		);
787	}
788
789	#[test]
790	fn test_array_agg_type_safety() {
791		let int_agg = ArrayAgg::<i32>::new("scores".to_string());
792		let string_agg = ArrayAgg::<String>::new("names".to_string());
793
794		assert_eq!(int_agg.to_sql(), "ARRAY_AGG(scores)");
795		assert_eq!(string_agg.to_sql(), "ARRAY_AGG(names)");
796	}
797
798	#[test]
799	fn test_jsonb_build_object_default() {
800		let builder = JsonbBuildObject::default();
801		assert_eq!(builder.to_sql(), "jsonb_build_object()");
802	}
803
804	#[test]
805	fn test_full_text_search_config_getter() {
806		let search = FullTextSearch::new("text".to_string(), "query".to_string());
807		assert_eq!(search.config(), "english");
808
809		let search_fr = search.with_config("french".to_string());
810		assert_eq!(search_fr.config(), "french");
811	}
812
813	// StringAgg tests
814	#[test]
815	fn test_string_agg_basic() {
816		let agg = StringAgg::new("name".to_string(), ", ".to_string());
817		assert_eq!(agg.to_sql(), "STRING_AGG(name, ', ')");
818	}
819
820	#[test]
821	fn test_string_agg_distinct() {
822		let agg = StringAgg::new("category".to_string(), "; ".to_string()).distinct();
823		assert_eq!(agg.to_sql(), "STRING_AGG(DISTINCT category, '; ')");
824	}
825
826	#[test]
827	fn test_string_agg_with_ordering() {
828		let agg = StringAgg::new("name".to_string(), ", ".to_string())
829			.order_by(vec!["name ASC".to_string()]);
830		assert_eq!(agg.to_sql(), "STRING_AGG(name, ', ' ORDER BY name ASC)");
831	}
832
833	#[test]
834	fn test_string_agg_distinct_with_ordering() {
835		let agg = StringAgg::new("name".to_string(), ",".to_string())
836			.distinct()
837			.order_by(vec!["created_at DESC".to_string()]);
838		assert_eq!(
839			agg.to_sql(),
840			"STRING_AGG(DISTINCT name, ',' ORDER BY created_at DESC)"
841		);
842	}
843
844	// JsonbAgg tests
845	#[test]
846	fn test_jsonb_agg_basic() {
847		let agg = JsonbAgg::new("user_data".to_string());
848		assert_eq!(agg.to_sql(), "JSONB_AGG(user_data)");
849	}
850
851	#[test]
852	fn test_jsonb_agg_distinct() {
853		let agg = JsonbAgg::new("category".to_string()).distinct();
854		assert_eq!(agg.to_sql(), "JSONB_AGG(DISTINCT category)");
855	}
856
857	#[test]
858	fn test_jsonb_agg_with_ordering() {
859		let agg = JsonbAgg::new("items".to_string()).order_by(vec!["created_at DESC".to_string()]);
860		assert_eq!(agg.to_sql(), "JSONB_AGG(items ORDER BY created_at DESC)");
861	}
862
863	#[test]
864	fn test_jsonb_agg_distinct_with_ordering() {
865		let agg = JsonbAgg::new("data".to_string())
866			.distinct()
867			.order_by(vec!["id ASC".to_string(), "name DESC".to_string()]);
868		assert_eq!(
869			agg.to_sql(),
870			"JSONB_AGG(DISTINCT data ORDER BY id ASC, name DESC)"
871		);
872	}
873
874	// TsRank tests
875	#[test]
876	fn test_ts_rank_basic() {
877		let rank = TsRank::new("search_vector".to_string(), "rust".to_string());
878		assert_eq!(
879			rank.to_sql(),
880			"ts_rank(search_vector, to_tsquery('english', 'rust'))"
881		);
882	}
883
884	#[test]
885	fn test_ts_rank_with_config() {
886		let rank = TsRank::new("content".to_string(), "bonjour".to_string())
887			.with_config("french".to_string());
888		assert_eq!(
889			rank.to_sql(),
890			"ts_rank(content, to_tsquery('french', 'bonjour'))"
891		);
892	}
893
894	#[test]
895	fn test_ts_rank_with_normalization() {
896		let rank = TsRank::new("content".to_string(), "rust".to_string()).with_normalization(2);
897		assert_eq!(
898			rank.to_sql(),
899			"ts_rank(content, to_tsquery('english', 'rust'), 2)"
900		);
901	}
902
903	#[test]
904	fn test_ts_rank_with_config_and_normalization() {
905		let rank = TsRank::new("text_vector".to_string(), "database".to_string())
906			.with_config("simple".to_string())
907			.with_normalization(4);
908		assert_eq!(
909			rank.to_sql(),
910			"ts_rank(text_vector, to_tsquery('simple', 'database'), 4)"
911		);
912	}
913
914	#[test]
915	fn test_ts_rank_config_getter() {
916		let rank = TsRank::new("content".to_string(), "query".to_string());
917		assert_eq!(rank.config(), "english");
918
919		let rank_fr = rank.with_config("french".to_string());
920		assert_eq!(rank_fr.config(), "french");
921	}
922}