fraiseql_db/postgres/
where_generator.rs1use std::{
8 collections::{HashMap, HashSet},
9 sync::Arc,
10};
11
12use crate::{dialect::PostgresDialect, where_generator::GenericWhereGenerator};
13
14pub type IndexedColumnsCache = HashMap<String, HashSet<String>>;
39
40pub type PostgresWhereGenerator = GenericWhereGenerator<PostgresDialect>;
64
65impl PostgresWhereGenerator {
70 #[must_use]
72 pub const fn postgres_new() -> Self {
73 Self::new(PostgresDialect)
74 }
75
76 #[must_use]
97 pub fn postgres_with_indexed_columns(indexed_columns: Arc<HashSet<String>>) -> Self {
98 Self::new(PostgresDialect).with_indexed_columns(indexed_columns)
99 }
100}
101
102#[cfg(test)]
103#[allow(clippy::unwrap_used)] mod tests {
105 use std::{collections::HashSet, sync::Arc};
106
107 use serde_json::json;
108
109 use super::*;
110 use crate::where_clause::{WhereClause, WhereOperator};
111
112 #[test]
113 fn test_simple_equality() {
114 let gen = PostgresWhereGenerator::new(PostgresDialect);
115 let clause = WhereClause::Field {
116 path: vec!["email".to_string()],
117 operator: WhereOperator::Eq,
118 value: json!("test@example.com"),
119 };
120
121 let (sql, params) = gen.generate(&clause).unwrap();
122 assert_eq!(sql, "data->>'email' = $1");
123 assert_eq!(params, vec![json!("test@example.com")]);
124 }
125
126 #[test]
127 fn test_icontains() {
128 let gen = PostgresWhereGenerator::new(PostgresDialect);
129 let clause = WhereClause::Field {
130 path: vec!["name".to_string()],
131 operator: WhereOperator::Icontains,
132 value: json!("alice"),
133 };
134
135 let (sql, params) = gen.generate(&clause).unwrap();
136 assert_eq!(sql, "data->>'name' ILIKE '%' || $1 || '%'");
137 assert_eq!(params, vec![json!("alice")]);
138 }
139
140 #[test]
141 fn test_and_clause() {
142 let gen = PostgresWhereGenerator::new(PostgresDialect);
143 let clause = WhereClause::And(vec![
144 WhereClause::Field {
145 path: vec!["status".to_string()],
146 operator: WhereOperator::Eq,
147 value: json!("active"),
148 },
149 WhereClause::Field {
150 path: vec!["age".to_string()],
151 operator: WhereOperator::Gte,
152 value: json!(18),
153 },
154 ]);
155
156 let (sql, params) = gen.generate(&clause).unwrap();
157 assert!(sql.contains("AND"), "Expected AND: {sql}");
158 assert_eq!(params.len(), 2);
159 }
160
161 #[test]
162 fn test_indexed_columns() {
163 let mut cols = HashSet::new();
164 cols.insert("items__product__category__code".to_string());
165 let gen = PostgresWhereGenerator::new(PostgresDialect).with_indexed_columns(Arc::new(cols));
166
167 let clause = WhereClause::Field {
168 path: vec![
169 "items".to_string(),
170 "product".to_string(),
171 "category".to_string(),
172 "code".to_string(),
173 ],
174 operator: WhereOperator::Eq,
175 value: json!("BOOK"),
176 };
177
178 let (sql, params) = gen.generate(&clause).unwrap();
179 assert!(
180 sql.contains("\"items__product__category__code\""),
181 "Expected indexed col, got: {sql}"
182 );
183 assert_eq!(params, vec![json!("BOOK")]);
184 }
185
186 #[test]
187 fn test_nested_path() {
188 let gen = PostgresWhereGenerator::new(PostgresDialect);
189 let clause = WhereClause::Field {
190 path: vec!["address".to_string(), "city".to_string()],
191 operator: WhereOperator::Eq,
192 value: json!("Paris"),
193 };
194
195 let (sql, _) = gen.generate(&clause).unwrap();
196 assert!(sql.contains("data->"), "Expected JSONB path: {sql}");
198 assert!(sql.contains("address"), "Expected 'address' segment: {sql}");
199 assert!(sql.contains("city"), "Expected 'city' segment: {sql}");
200 }
201
202 #[test]
203 fn test_is_null() {
204 let gen = PostgresWhereGenerator::new(PostgresDialect);
205 let clause = WhereClause::Field {
206 path: vec!["deleted_at".to_string()],
207 operator: WhereOperator::IsNull,
208 value: json!(true),
209 };
210
211 let (sql, params) = gen.generate(&clause).unwrap();
212 assert_eq!(sql, "data->>'deleted_at' IS NULL");
213 assert!(params.is_empty());
214 }
215
216 #[test]
217 fn test_param_offset() {
218 let gen = PostgresWhereGenerator::new(PostgresDialect);
219 let clause = WhereClause::Field {
220 path: vec!["email".to_string()],
221 operator: WhereOperator::Eq,
222 value: json!("a@b.com"),
223 };
224
225 let (sql, _) = gen.generate_with_param_offset(&clause, 2).unwrap();
226 assert!(sql.contains("$3"), "Expected $3, got: {sql}");
227 }
228
229 #[test]
230 fn test_in_operator() {
231 let gen = PostgresWhereGenerator::new(PostgresDialect);
232 let clause = WhereClause::Field {
233 path: vec!["status".to_string()],
234 operator: WhereOperator::In,
235 value: json!(["active", "pending"]),
236 };
237
238 let (sql, params) = gen.generate(&clause).unwrap();
239 assert_eq!(sql, "data->>'status' IN ($1, $2)");
240 assert_eq!(params.len(), 2);
241 }
242
243 #[test]
244 fn test_vector_cosine_distance() {
245 let gen = PostgresWhereGenerator::new(PostgresDialect);
246 let clause = WhereClause::Field {
247 path: vec!["embedding".to_string()],
248 operator: WhereOperator::CosineDistance,
249 value: json!([0.1, 0.2, 0.3]),
250 };
251
252 let (sql, params) = gen.generate(&clause).unwrap();
253 assert!(sql.contains("<=>"), "Expected <=>: {sql}");
254 assert_eq!(params.len(), 1);
255 }
256
257 #[test]
258 fn test_ltree_ancestor_of() {
259 let gen = PostgresWhereGenerator::new(PostgresDialect);
260 let clause = WhereClause::Field {
261 path: vec!["category_path".to_string()],
262 operator: WhereOperator::AncestorOf,
263 value: json!("europe.france"),
264 };
265
266 let (sql, params) = gen.generate(&clause).unwrap();
267 assert!(sql.contains("@>"), "Expected @>: {sql}");
268 assert!(sql.contains("ltree"), "Expected ::ltree: {sql}");
269 assert_eq!(params.len(), 1);
270 }
271}