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