1use super::{Field, Value, WhereOperator};
20use crate::Result;
21use std::collections::HashMap;
22
23pub(crate) fn escape_like_literal(s: &str) -> String {
29 s.replace('\\', "\\\\")
31 .replace('%', "\\%")
32 .replace('_', "\\_")
33}
34
35const fn infer_type_cast(value: &Value) -> &'static str {
39 match value {
40 Value::String(_) => "::text",
41 Value::Number(_) => "::numeric", Value::Bool(_) => "::boolean",
43 Value::Null => "", Value::Array(_) => "", Value::FloatArray(_) => "", Value::RawSql(_) => "", }
48}
49
50pub(crate) fn cidr_containment_check(field_sql: &str, ranges: &[&str], negate: bool) -> String {
70 let conditions: Vec<String> = ranges
71 .iter()
72 .map(|r| format!("{field_sql}::inet << '{r}'::inet"))
73 .collect();
74 let inner = format!("({})", conditions.join(" OR "));
75 if negate {
76 format!("NOT {inner}")
77 } else {
78 inner
79 }
80}
81
82const PRIVATE_RANGES: &[&str] = &["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "fc00::/7"];
84
85const LOOPBACK_RANGES: &[&str] = &["127.0.0.0/8", "::1/128"];
87
88const MULTICAST_RANGES: &[&str] = &["224.0.0.0/4", "ff00::/8"];
90
91const LINK_LOCAL_RANGES: &[&str] = &["169.254.0.0/16", "fe80::/10"];
93
94const DOCUMENTATION_RANGES: &[&str] = &[
96 "192.0.2.0/24",
97 "198.51.100.0/24",
98 "203.0.113.0/24",
99 "2001:db8::/32",
100];
101
102const CARRIER_GRADE_RANGES: &[&str] = &["100.64.0.0/10"];
104
105pub fn generate_where_operator_sql(
135 operator: &WhereOperator,
136 param_index: &mut usize,
137 params: &mut HashMap<usize, Value>,
138) -> Result<String> {
139 operator
140 .validate()
141 .map_err(crate::WireError::InvalidSchema)?;
142
143 match operator {
144 WhereOperator::Eq(field, value) => {
148 let field_sql = field.to_sql();
149 if value.is_null() {
150 Ok(format!("{} IS NULL", field_sql))
151 } else {
152 let param_num = *param_index + 1;
153 *param_index += 1;
154 params.insert(param_num, value.clone());
155 let cast = match field {
157 Field::JsonbField(_) | Field::JsonbPath(_) => infer_type_cast(value),
158 Field::DirectColumn(_) => "", };
160 Ok(format!("{}{} = ${}", field_sql, cast, param_num))
161 }
162 }
163
164 WhereOperator::Neq(field, value) => {
165 let field_sql = field.to_sql();
166 if value.is_null() {
167 Ok(format!("{} IS NOT NULL", field_sql))
168 } else {
169 let param_num = *param_index + 1;
170 *param_index += 1;
171 params.insert(param_num, value.clone());
172 let cast = match field {
173 Field::JsonbField(_) | Field::JsonbPath(_) => infer_type_cast(value),
174 Field::DirectColumn(_) => "",
175 };
176 Ok(format!("{}{} != ${}", field_sql, cast, param_num))
177 }
178 }
179
180 WhereOperator::Gt(field, value) => {
181 let field_sql = field.to_sql();
182 let param_num = *param_index + 1;
183 *param_index += 1;
184 params.insert(param_num, value.clone());
185 let cast = match field {
186 Field::JsonbField(_) | Field::JsonbPath(_) => infer_type_cast(value),
187 Field::DirectColumn(_) => "",
188 };
189 Ok(format!("{}{} > ${}", field_sql, cast, param_num))
190 }
191
192 WhereOperator::Gte(field, value) => {
193 let field_sql = field.to_sql();
194 let param_num = *param_index + 1;
195 *param_index += 1;
196 params.insert(param_num, value.clone());
197 let cast = match field {
198 Field::JsonbField(_) | Field::JsonbPath(_) => infer_type_cast(value),
199 Field::DirectColumn(_) => "",
200 };
201 Ok(format!("{}{} >= ${}", field_sql, cast, param_num))
202 }
203
204 WhereOperator::Lt(field, value) => {
205 let field_sql = field.to_sql();
206 let param_num = *param_index + 1;
207 *param_index += 1;
208 params.insert(param_num, value.clone());
209 let cast = match field {
210 Field::JsonbField(_) | Field::JsonbPath(_) => infer_type_cast(value),
211 Field::DirectColumn(_) => "",
212 };
213 Ok(format!("{}{} < ${}", field_sql, cast, param_num))
214 }
215
216 WhereOperator::Lte(field, value) => {
217 let field_sql = field.to_sql();
218 let param_num = *param_index + 1;
219 *param_index += 1;
220 params.insert(param_num, value.clone());
221 let cast = match field {
222 Field::JsonbField(_) | Field::JsonbPath(_) => infer_type_cast(value),
223 Field::DirectColumn(_) => "",
224 };
225 Ok(format!("{}{} <= ${}", field_sql, cast, param_num))
226 }
227
228 WhereOperator::In(field, values) => {
230 if values.is_empty() {
232 return Ok("FALSE".to_string());
233 }
234 let field_sql = field.to_sql();
235 let placeholders: Vec<String> = values
236 .iter()
237 .map(|v| {
238 let param_num = *param_index + 1;
239 *param_index += 1;
240 params.insert(param_num, v.clone());
241 format!("${}", param_num)
242 })
243 .collect();
244 Ok(format!("{} IN ({})", field_sql, placeholders.join(", ")))
245 }
246
247 WhereOperator::Nin(field, values) => {
248 if values.is_empty() {
250 return Ok("TRUE".to_string());
251 }
252 let field_sql = field.to_sql();
253 let placeholders: Vec<String> = values
254 .iter()
255 .map(|v| {
256 let param_num = *param_index + 1;
257 *param_index += 1;
258 params.insert(param_num, v.clone());
259 format!("${}", param_num)
260 })
261 .collect();
262 Ok(format!(
263 "{} NOT IN ({})",
264 field_sql,
265 placeholders.join(", ")
266 ))
267 }
268
269 WhereOperator::Contains(field, substring) => {
270 let field_sql = field.to_sql();
271 let param_num = *param_index + 1;
272 *param_index += 1;
273 params.insert(param_num, Value::String(escape_like_literal(substring)));
274 Ok(format!(
275 "{} LIKE '%' || ${}::text || '%'",
276 field_sql, param_num
277 ))
278 }
279
280 WhereOperator::ArrayContains(field, value) => {
281 let field_sql = field.to_sql();
282 let param_num = *param_index + 1;
283 *param_index += 1;
284 params.insert(param_num, value.clone());
285 Ok(format!("{} @> ARRAY[${}]", field_sql, param_num))
286 }
287
288 WhereOperator::ArrayContainedBy(field, value) => {
289 let field_sql = field.to_sql();
290 let param_num = *param_index + 1;
291 *param_index += 1;
292 params.insert(param_num, value.clone());
293 Ok(format!("{} <@ ARRAY[${}]", field_sql, param_num))
294 }
295
296 WhereOperator::ArrayOverlaps(field, values) => {
297 let field_sql = field.to_sql();
298 let placeholders: Vec<String> = values
299 .iter()
300 .map(|v| {
301 let param_num = *param_index + 1;
302 *param_index += 1;
303 params.insert(param_num, v.clone());
304 format!("${}", param_num)
305 })
306 .collect();
307 Ok(format!(
308 "{} && ARRAY[{}]",
309 field_sql,
310 placeholders.join(", ")
311 ))
312 }
313
314 WhereOperator::LenEq(field, len) => {
316 let field_sql = field.to_sql();
317 Ok(format!("array_length({}, 1) = {}", field_sql, len))
318 }
319
320 WhereOperator::LenGt(field, len) => {
321 let field_sql = field.to_sql();
322 Ok(format!("array_length({}, 1) > {}", field_sql, len))
323 }
324
325 WhereOperator::LenGte(field, len) => {
326 let field_sql = field.to_sql();
327 Ok(format!("array_length({}, 1) >= {}", field_sql, len))
328 }
329
330 WhereOperator::LenLt(field, len) => {
331 let field_sql = field.to_sql();
332 Ok(format!("array_length({}, 1) < {}", field_sql, len))
333 }
334
335 WhereOperator::LenLte(field, len) => {
336 let field_sql = field.to_sql();
337 Ok(format!("array_length({}, 1) <= {}", field_sql, len))
338 }
339
340 WhereOperator::Icontains(field, substring) => {
342 let field_sql = field.to_sql();
343 let param_num = *param_index + 1;
344 *param_index += 1;
345 params.insert(param_num, Value::String(escape_like_literal(substring)));
346 Ok(format!(
347 "{} ILIKE '%' || ${}::text || '%'",
348 field_sql, param_num
349 ))
350 }
351
352 WhereOperator::Startswith(field, prefix) => {
353 let field_sql = field.to_sql();
354 let param_num = *param_index + 1;
355 *param_index += 1;
356 params.insert(
357 param_num,
358 Value::String(format!("{}%", escape_like_literal(prefix))),
359 );
360 Ok(format!("{} LIKE ${}", field_sql, param_num))
361 }
362
363 WhereOperator::Istartswith(field, prefix) => {
364 let field_sql = field.to_sql();
365 let param_num = *param_index + 1;
366 *param_index += 1;
367 params.insert(
368 param_num,
369 Value::String(format!("{}%", escape_like_literal(prefix))),
370 );
371 Ok(format!("{} ILIKE ${}", field_sql, param_num))
372 }
373
374 WhereOperator::Endswith(field, suffix) => {
375 let field_sql = field.to_sql();
376 let param_num = *param_index + 1;
377 *param_index += 1;
378 params.insert(
379 param_num,
380 Value::String(format!("%{}", escape_like_literal(suffix))),
381 );
382 Ok(format!("{} LIKE ${}", field_sql, param_num))
383 }
384
385 WhereOperator::Iendswith(field, suffix) => {
386 let field_sql = field.to_sql();
387 let param_num = *param_index + 1;
388 *param_index += 1;
389 params.insert(
390 param_num,
391 Value::String(format!("%{}", escape_like_literal(suffix))),
392 );
393 Ok(format!("{} ILIKE ${}", field_sql, param_num))
394 }
395
396 WhereOperator::Like(field, pattern) => {
397 let field_sql = field.to_sql();
398 let param_num = *param_index + 1;
399 *param_index += 1;
400 params.insert(param_num, Value::String(pattern.clone()));
401 Ok(format!("{} LIKE ${}", field_sql, param_num))
402 }
403
404 WhereOperator::Ilike(field, pattern) => {
405 let field_sql = field.to_sql();
406 let param_num = *param_index + 1;
407 *param_index += 1;
408 params.insert(param_num, Value::String(pattern.clone()));
409 Ok(format!("{} ILIKE ${}", field_sql, param_num))
410 }
411
412 WhereOperator::IsNull(field, is_null) => {
414 let field_sql = field.to_sql();
415 if *is_null {
416 Ok(format!("{} IS NULL", field_sql))
417 } else {
418 Ok(format!("{} IS NOT NULL", field_sql))
419 }
420 }
421
422 WhereOperator::L2Distance {
424 field,
425 vector,
426 threshold,
427 } => {
428 let field_sql = field.to_sql();
429 let param_num = *param_index + 1;
430 *param_index += 1;
431 params.insert(param_num, Value::FloatArray(vector.clone()));
432 Ok(format!(
433 "l2_distance({}::vector, ${}::vector) < {}",
434 field_sql, param_num, threshold
435 ))
436 }
437
438 WhereOperator::CosineDistance {
439 field,
440 vector,
441 threshold,
442 } => {
443 let field_sql = field.to_sql();
444 let param_num = *param_index + 1;
445 *param_index += 1;
446 params.insert(param_num, Value::FloatArray(vector.clone()));
447 Ok(format!(
448 "cosine_distance({}::vector, ${}::vector) < {}",
449 field_sql, param_num, threshold
450 ))
451 }
452
453 WhereOperator::InnerProduct {
454 field,
455 vector,
456 threshold,
457 } => {
458 let field_sql = field.to_sql();
459 let param_num = *param_index + 1;
460 *param_index += 1;
461 params.insert(param_num, Value::FloatArray(vector.clone()));
462 Ok(format!(
463 "inner_product({}::vector, ${}::vector) > {}",
464 field_sql, param_num, threshold
465 ))
466 }
467
468 WhereOperator::L1Distance {
469 field,
470 vector,
471 threshold,
472 } => {
473 let field_sql = field.to_sql();
474 let param_num = *param_index + 1;
475 *param_index += 1;
476 params.insert(param_num, Value::FloatArray(vector.clone()));
477 Ok(format!(
478 "l1_distance({}::vector, ${}::vector) < {}",
479 field_sql, param_num, threshold
480 ))
481 }
482
483 WhereOperator::HammingDistance {
484 field,
485 vector,
486 threshold,
487 } => {
488 let field_sql = field.to_sql();
489 let param_num = *param_index + 1;
490 *param_index += 1;
491 params.insert(param_num, Value::FloatArray(vector.clone()));
492 Ok(format!(
493 "hamming_distance({}::bit, ${}::bit) < {}",
494 field_sql, param_num, threshold
495 ))
496 }
497
498 WhereOperator::JaccardDistance {
499 field,
500 set,
501 threshold,
502 } => {
503 let field_sql = field.to_sql();
504 let param_num = *param_index + 1;
505 *param_index += 1;
506 let value_array: Vec<Value> = set.iter().map(|s| Value::String(s.clone())).collect();
507 params.insert(param_num, Value::Array(value_array));
508 Ok(format!(
509 "jaccard_distance({}::text[], ${}::text[]) < {}",
510 field_sql, param_num, threshold
511 ))
512 }
513
514 WhereOperator::Matches {
516 field,
517 query,
518 language,
519 } => {
520 let field_sql = field.to_sql();
521 let param_num = *param_index + 1;
522 *param_index += 1;
523 params.insert(param_num, Value::String(query.clone()));
524 let lang = language.as_deref().unwrap_or("english");
525 Ok(format!(
526 "{} @@ plainto_tsquery('{}', ${})",
527 field_sql, lang, param_num
528 ))
529 }
530
531 WhereOperator::PlainQuery { field, query } => {
532 let field_sql = field.to_sql();
533 let param_num = *param_index + 1;
534 *param_index += 1;
535 params.insert(param_num, Value::String(query.clone()));
536 Ok(format!(
537 "{} @@ plainto_tsquery(${})::tsvector",
538 field_sql, param_num
539 ))
540 }
541
542 WhereOperator::PhraseQuery {
543 field,
544 query,
545 language,
546 } => {
547 let field_sql = field.to_sql();
548 let param_num = *param_index + 1;
549 *param_index += 1;
550 params.insert(param_num, Value::String(query.clone()));
551 let lang = language.as_deref().unwrap_or("english");
552 Ok(format!(
553 "{} @@ phraseto_tsquery('{}', ${})",
554 field_sql, lang, param_num
555 ))
556 }
557
558 WhereOperator::WebsearchQuery {
559 field,
560 query,
561 language,
562 } => {
563 let field_sql = field.to_sql();
564 let param_num = *param_index + 1;
565 *param_index += 1;
566 params.insert(param_num, Value::String(query.clone()));
567 let lang = language.as_deref().unwrap_or("english");
568 Ok(format!(
569 "{} @@ websearch_to_tsquery('{}', ${})",
570 field_sql, lang, param_num
571 ))
572 }
573
574 WhereOperator::IsIPv4(field) => {
576 let field_sql = field.to_sql();
577 Ok(format!("family({}::inet) = 4", field_sql))
578 }
579
580 WhereOperator::IsIPv6(field) => {
581 let field_sql = field.to_sql();
582 Ok(format!("family({}::inet) = 6", field_sql))
583 }
584
585 WhereOperator::IsPrivate { field, value } => {
586 let field_sql = field.to_sql();
587 Ok(cidr_containment_check(&field_sql, PRIVATE_RANGES, !value))
588 }
589
590 WhereOperator::IsLoopback { field, value } => {
591 let field_sql = field.to_sql();
592 Ok(cidr_containment_check(&field_sql, LOOPBACK_RANGES, !value))
593 }
594
595 WhereOperator::IsMulticast { field, value } => {
596 let field_sql = field.to_sql();
597 Ok(cidr_containment_check(&field_sql, MULTICAST_RANGES, !value))
598 }
599
600 WhereOperator::IsLinkLocal { field, value } => {
601 let field_sql = field.to_sql();
602 Ok(cidr_containment_check(
603 &field_sql,
604 LINK_LOCAL_RANGES,
605 !value,
606 ))
607 }
608
609 WhereOperator::IsDocumentation { field, value } => {
610 let field_sql = field.to_sql();
611 Ok(cidr_containment_check(
612 &field_sql,
613 DOCUMENTATION_RANGES,
614 !value,
615 ))
616 }
617
618 WhereOperator::IsCarrierGrade { field, value } => {
619 let field_sql = field.to_sql();
620 Ok(cidr_containment_check(
621 &field_sql,
622 CARRIER_GRADE_RANGES,
623 !value,
624 ))
625 }
626
627 WhereOperator::InSubnet { field, subnet } => {
628 let field_sql = field.to_sql();
629 let param_num = *param_index + 1;
630 *param_index += 1;
631 params.insert(param_num, Value::String(subnet.clone()));
632 Ok(format!("{}::inet << ${}::inet", field_sql, param_num))
633 }
634
635 WhereOperator::ContainsSubnet { field, subnet } => {
636 let field_sql = field.to_sql();
637 let param_num = *param_index + 1;
638 *param_index += 1;
639 params.insert(param_num, Value::String(subnet.clone()));
640 Ok(format!("{}::inet >> ${}::inet", field_sql, param_num))
641 }
642
643 WhereOperator::ContainsIP { field, ip } => {
644 let field_sql = field.to_sql();
645 let param_num = *param_index + 1;
646 *param_index += 1;
647 params.insert(param_num, Value::String(ip.clone()));
648 Ok(format!("{}::inet >> ${}::inet", field_sql, param_num))
649 }
650
651 WhereOperator::IPRangeOverlap { field, range } => {
652 let field_sql = field.to_sql();
653 let param_num = *param_index + 1;
654 *param_index += 1;
655 params.insert(param_num, Value::String(range.clone()));
656 Ok(format!("{}::inet && ${}::inet", field_sql, param_num))
657 }
658
659 WhereOperator::StrictlyContains(field, value) => {
661 let field_sql = field.to_sql();
662 let param_num = *param_index + 1;
663 *param_index += 1;
664 params.insert(param_num, value.clone());
665 Ok(format!("{}::jsonb @> ${}::jsonb", field_sql, param_num))
666 }
667
668 WhereOperator::AncestorOf { field, path } => {
670 let field_sql = field.to_sql();
671 let param_num = *param_index + 1;
672 *param_index += 1;
673 params.insert(param_num, Value::String(path.clone()));
674 Ok(format!("{}::ltree @> ${}::ltree", field_sql, param_num))
675 }
676
677 WhereOperator::DescendantOf { field, path } => {
678 let field_sql = field.to_sql();
679 let param_num = *param_index + 1;
680 *param_index += 1;
681 params.insert(param_num, Value::String(path.clone()));
682 Ok(format!("{}::ltree <@ ${}::ltree", field_sql, param_num))
683 }
684
685 WhereOperator::MatchesLquery { field, pattern } => {
686 let field_sql = field.to_sql();
687 let param_num = *param_index + 1;
688 *param_index += 1;
689 params.insert(param_num, Value::String(pattern.clone()));
690 Ok(format!("{}::ltree ~ ${}::lquery", field_sql, param_num))
691 }
692
693 WhereOperator::MatchesLtxtquery { field, query } => {
694 let field_sql = field.to_sql();
695 let param_num = *param_index + 1;
696 *param_index += 1;
697 params.insert(param_num, Value::String(query.clone()));
698 Ok(format!("{}::ltree @ ${}::ltxtquery", field_sql, param_num))
699 }
700
701 WhereOperator::MatchesAnyLquery { field, patterns } => {
702 let field_sql = field.to_sql();
703 let placeholders: Vec<String> = patterns
704 .iter()
705 .map(|p| {
706 let param_num = *param_index + 1;
707 *param_index += 1;
708 params.insert(param_num, Value::String(p.clone()));
709 format!("${}::lquery", param_num)
710 })
711 .collect();
712 Ok(format!(
713 "{}::ltree ? ARRAY[{}]",
714 field_sql,
715 placeholders.join(", ")
716 ))
717 }
718
719 WhereOperator::DepthEq { field, depth } => {
721 let field_sql = field.to_sql();
722 Ok(format!("nlevel({}::ltree) = {}", field_sql, depth))
723 }
724
725 WhereOperator::DepthNeq { field, depth } => {
726 let field_sql = field.to_sql();
727 Ok(format!("nlevel({}::ltree) != {}", field_sql, depth))
728 }
729
730 WhereOperator::DepthGt { field, depth } => {
731 let field_sql = field.to_sql();
732 Ok(format!("nlevel({}::ltree) > {}", field_sql, depth))
733 }
734
735 WhereOperator::DepthGte { field, depth } => {
736 let field_sql = field.to_sql();
737 Ok(format!("nlevel({}::ltree) >= {}", field_sql, depth))
738 }
739
740 WhereOperator::DepthLt { field, depth } => {
741 let field_sql = field.to_sql();
742 Ok(format!("nlevel({}::ltree) < {}", field_sql, depth))
743 }
744
745 WhereOperator::DepthLte { field, depth } => {
746 let field_sql = field.to_sql();
747 Ok(format!("nlevel({}::ltree) <= {}", field_sql, depth))
748 }
749
750 WhereOperator::Lca { field, paths } => {
752 let field_sql = field.to_sql();
753 let placeholders: Vec<String> = paths
754 .iter()
755 .map(|p| {
756 let param_num = *param_index + 1;
757 *param_index += 1;
758 params.insert(param_num, Value::String(p.clone()));
759 format!("${}::ltree", param_num)
760 })
761 .collect();
762 Ok(format!(
763 "{}::ltree = lca(ARRAY[{}])",
764 field_sql,
765 placeholders.join(", ")
766 ))
767 }
768
769 WhereOperator::DescendantOfId { .. } | WhereOperator::AncestorOfId { .. } => {
774 Err(crate::WireError::InvalidSchema(
775 "ID-based ltree operators require HierarchyContext; use GenericWhereGenerator"
776 .to_string(),
777 ))
778 }
779 }
780}
781
782#[cfg(test)]
783mod tests;