sea_query/query/condition.rs
1use crate::{ExprTrait, expr::Expr, types::LogicalChainOper};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub enum ConditionType {
5 Any,
6 All,
7}
8
9/// Represents the value of an [`Condition::any`] or [`Condition::all`]: a set of disjunctive or conjunctive conditions.
10#[derive(Debug, Clone, PartialEq)]
11pub struct Condition {
12 pub(crate) negate: bool,
13 pub(crate) condition_type: ConditionType,
14 pub(crate) conditions: Vec<ConditionExpression>,
15}
16
17pub type Cond = Condition;
18
19impl From<Expr> for Condition {
20 fn from(expr: Expr) -> Self {
21 Condition {
22 negate: false,
23 condition_type: ConditionType::All,
24 conditions: vec![ConditionExpression::Expr(expr)],
25 }
26 }
27}
28
29impl From<ConditionExpression> for Condition {
30 fn from(ce: ConditionExpression) -> Self {
31 Condition {
32 negate: false,
33 condition_type: ConditionType::All,
34 conditions: vec![ce],
35 }
36 }
37}
38
39/// A helper trait.
40///
41/// You shouldn't implement this manually.
42pub trait IntoCondition: Into<Condition> {
43 #[inline(always)]
44 fn into_condition(self) -> Condition {
45 self.into()
46 }
47}
48
49impl<T> IntoCondition for T where T: Into<Condition> {}
50
51/// An internal representation of conditions.
52/// May be refactored away in the future if we can get our head around it.
53///
54/// It used to be in the public interface of [`Condition::add`] and [`Condition::add_option`],
55/// in order to accept anything resembling a condition or an expression.
56/// Nowadays, we achieve that with traits.
57#[derive(Debug, Clone, PartialEq)]
58pub(crate) enum ConditionExpression {
59 Condition(Condition),
60 Expr(Expr),
61}
62
63#[derive(Default, Debug, Clone, PartialEq)]
64pub enum ConditionHolderContents {
65 #[default]
66 Empty,
67 Chain(Vec<LogicalChainOper>),
68 Condition(Condition),
69}
70
71#[derive(Default, Debug, Clone, PartialEq)]
72pub struct ConditionHolder {
73 pub contents: ConditionHolderContents,
74}
75
76impl Condition {
77 /// Add a condition to the set.
78 ///
79 /// If it's an [`Condition::any`], it will be separated from the others by an `" OR "` in the query. If it's
80 /// an [`Condition::all`], it will be separated by an `" AND "`.
81 ///
82 /// ```
83 /// use sea_query::{tests_cfg::*, *};
84 ///
85 /// let statement = Query::select()
86 /// .column(Glyph::Id)
87 /// .from(Glyph::Table)
88 /// .cond_where(
89 /// Cond::all()
90 /// .add(Expr::col(Glyph::Aspect).eq(0).into_condition().not())
91 /// .add(Expr::col(Glyph::Id).eq(0).into_condition().not()),
92 /// )
93 /// .to_string(PostgresQueryBuilder);
94 /// assert_eq!(
95 /// statement,
96 /// r#"SELECT "id" FROM "glyph" WHERE (NOT "aspect" = 0) AND (NOT "id" = 0)"#
97 /// );
98 /// ```
99 #[allow(clippy::should_implement_trait)]
100 pub fn add<C>(mut self, condition: C) -> Self
101 where
102 C: Into<Condition>,
103 {
104 let condition: Condition = condition.into();
105 let condition: ConditionExpression = condition.into();
106 self.conditions.push(condition);
107 self
108 }
109
110 /// Add an optional condition to the set.
111 ///
112 /// Shorthand for `if o.is_some() { self.add(o) }`
113 ///
114 /// # Examples
115 ///
116 /// ```
117 /// use sea_query::{tests_cfg::*, *};
118 ///
119 /// let query = Query::select()
120 /// .column(Glyph::Image)
121 /// .from(Glyph::Table)
122 /// .cond_where(
123 /// Cond::all()
124 /// .add_option(Some(Expr::col((Glyph::Table, Glyph::Image)).like("A%")))
125 /// .add_option(None::<Expr>),
126 /// )
127 /// .to_owned();
128 ///
129 /// assert_eq!(
130 /// query.to_string(MysqlQueryBuilder),
131 /// r#"SELECT `image` FROM `glyph` WHERE `glyph`.`image` LIKE 'A%'"#
132 /// );
133 /// ```
134 #[allow(clippy::should_implement_trait)]
135 pub fn add_option<C>(self, other: Option<C>) -> Self
136 where
137 C: Into<Condition>,
138 {
139 if let Some(other) = other {
140 self.add(other)
141 } else {
142 self
143 }
144 }
145
146 /// Create a condition that is true if any of the conditions is true.
147 ///
148 /// # Examples
149 ///
150 /// ```
151 /// use sea_query::{*, tests_cfg::*};
152 ///
153 /// let query = Query::select()
154 /// .column(Glyph::Image)
155 /// .from(Glyph::Table)
156 /// .cond_where(
157 /// Cond::any()
158 /// .add(Expr::col((Glyph::Table, Glyph::Aspect)).is_in([3, 4]))
159 /// .add(Expr::col((Glyph::Table, Glyph::Image)).like("A%"))
160 /// )
161 /// .to_owned();
162 ///
163 /// assert_eq!(
164 /// query.to_string(MysqlQueryBuilder),
165 /// r#"SELECT `image` FROM `glyph` WHERE `glyph`.`aspect` IN (3, 4) OR `glyph`.`image` LIKE 'A%'"#
166 /// );
167 /// ```
168 pub fn any() -> Condition {
169 Condition {
170 negate: false,
171 condition_type: ConditionType::Any,
172 conditions: Vec::new(),
173 }
174 }
175
176 /// Create a condition that is false if any of the conditions is false.
177 ///
178 /// # Examples
179 ///
180 /// ```
181 /// use sea_query::{*, tests_cfg::*};
182 ///
183 /// let query = Query::select()
184 /// .column(Glyph::Image)
185 /// .from(Glyph::Table)
186 /// .cond_where(
187 /// Cond::all()
188 /// .add(Expr::col((Glyph::Table, Glyph::Aspect)).is_in([3, 4]))
189 /// .add(Expr::col((Glyph::Table, Glyph::Image)).like("A%"))
190 /// )
191 /// .to_owned();
192 ///
193 /// assert_eq!(
194 /// query.to_string(MysqlQueryBuilder),
195 /// r#"SELECT `image` FROM `glyph` WHERE `glyph`.`aspect` IN (3, 4) AND `glyph`.`image` LIKE 'A%'"#
196 /// );
197 /// ```
198 pub fn all() -> Condition {
199 Condition {
200 negate: false,
201 condition_type: ConditionType::All,
202 conditions: Vec::new(),
203 }
204 }
205
206 /// Negates a condition.
207 ///
208 /// # Examples
209 ///
210 /// ```
211 /// use sea_query::{tests_cfg::*, *};
212 ///
213 /// let query = Query::select()
214 /// .column(Glyph::Image)
215 /// .from(Glyph::Table)
216 /// .cond_where(
217 /// Cond::all()
218 /// .not()
219 /// .add(Expr::col((Glyph::Table, Glyph::Aspect)).is_in([3, 4]))
220 /// .add(Expr::col((Glyph::Table, Glyph::Image)).like("A%"))
221 /// )
222 /// .to_owned();
223 ///
224 /// assert_eq!(
225 /// query.to_string(MysqlQueryBuilder),
226 /// r#"SELECT `image` FROM `glyph` WHERE NOT (`glyph`.`aspect` IN (3, 4) AND `glyph`.`image` LIKE 'A%')"#
227 /// );
228 /// ```
229 ///
230 /// # More Examples
231 ///
232 /// ```
233 /// use sea_query::{tests_cfg::*, *};
234 ///
235 /// let query = Query::select()
236 /// .column(Glyph::Id)
237 /// .cond_where(
238 /// Cond::all()
239 /// .add(
240 /// Cond::all()
241 /// .not()
242 /// .add(Expr::val(1).eq(1))
243 /// .add(Expr::val(2).eq(2)),
244 /// )
245 /// .add(Cond::any().add(Expr::val(3).eq(3)).add(Expr::val(4).eq(4))),
246 /// )
247 /// .to_owned();
248 ///
249 /// assert_eq!(
250 /// query.to_string(MysqlQueryBuilder),
251 /// r#"SELECT `id` WHERE (NOT (1 = 1 AND 2 = 2)) AND (3 = 3 OR 4 = 4)"#
252 /// );
253 /// ```
254 #[allow(clippy::should_implement_trait)]
255 pub fn not(mut self) -> Self {
256 self.negate = !self.negate;
257 self
258 }
259
260 /// Whether or not any condition has been added
261 ///
262 /// # Examples
263 ///
264 /// ```
265 /// use sea_query::{tests_cfg::*, *};
266 ///
267 /// let is_empty = Cond::all().is_empty();
268 ///
269 /// assert!(is_empty);
270 /// ```
271 pub fn is_empty(&self) -> bool {
272 self.conditions.is_empty()
273 }
274
275 /// How many conditions were added
276 ///
277 /// # Examples
278 ///
279 /// ```
280 /// use sea_query::{tests_cfg::*, *};
281 ///
282 /// let len = Cond::all().len();
283 ///
284 /// assert_eq!(len, 0);
285 /// ```
286 pub fn len(&self) -> usize {
287 self.conditions.len()
288 }
289}
290
291impl From<Condition> for Expr {
292 fn from(cond: Condition) -> Self {
293 let mut inner_exprs = vec![];
294 for ce in cond.conditions {
295 inner_exprs.push(match ce {
296 ConditionExpression::Condition(c) => c.into(),
297 ConditionExpression::Expr(e) => e,
298 });
299 }
300 let mut inner_exprs_into_iter = inner_exprs.into_iter();
301 let expr = if let Some(first_expr) = inner_exprs_into_iter.next() {
302 let mut out_expr = first_expr;
303 for e in inner_exprs_into_iter {
304 out_expr = match cond.condition_type {
305 ConditionType::Any => out_expr.or(e),
306 ConditionType::All => out_expr.and(e),
307 };
308 }
309 out_expr
310 } else {
311 Expr::Constant(match cond.condition_type {
312 ConditionType::Any => false.into(),
313 ConditionType::All => true.into(),
314 })
315 };
316 if cond.negate { expr.not() } else { expr }
317 }
318}
319
320impl From<ConditionExpression> for Expr {
321 fn from(ce: ConditionExpression) -> Self {
322 match ce {
323 ConditionExpression::Condition(c) => c.into(),
324 ConditionExpression::Expr(e) => e,
325 }
326 }
327}
328
329impl From<Condition> for ConditionExpression {
330 fn from(condition: Condition) -> Self {
331 ConditionExpression::Condition(condition)
332 }
333}
334
335impl From<Expr> for ConditionExpression {
336 fn from(condition: Expr) -> Self {
337 ConditionExpression::Expr(condition)
338 }
339}
340
341/// Macro to easily create an [`Condition::any`].
342///
343/// # Examples
344///
345/// ```
346/// use sea_query::{*, tests_cfg::*};
347///
348/// let query = Query::select()
349/// .column(Glyph::Image)
350/// .from(Glyph::Table)
351/// .cond_where(
352/// any![
353/// Expr::col((Glyph::Table, Glyph::Aspect)).is_in([3, 4]),
354/// Expr::col((Glyph::Table, Glyph::Image)).like("A%")
355/// ]
356/// )
357/// .to_owned();
358///
359/// assert_eq!(
360/// query.to_string(MysqlQueryBuilder),
361/// r#"SELECT `image` FROM `glyph` WHERE `glyph`.`aspect` IN (3, 4) OR `glyph`.`image` LIKE 'A%'"#
362/// );
363/// ```
364#[macro_export]
365macro_rules! any {
366 ( $( $x:expr ),* $(,)?) => {
367 {
368 let mut tmp = $crate::Condition::any();
369 $(
370 tmp = tmp.add($x);
371 )*
372 tmp
373 }
374 };
375}
376
377/// Macro to easily create an [`Condition::all`].
378///
379/// # Examples
380///
381/// ```
382/// use sea_query::{*, tests_cfg::*};
383///
384/// let query = Query::select()
385/// .column(Glyph::Image)
386/// .from(Glyph::Table)
387/// .cond_where(
388/// all![
389/// Expr::col((Glyph::Table, Glyph::Aspect)).is_in([3, 4]),
390/// Expr::col((Glyph::Table, Glyph::Image)).like("A%")
391/// ]
392/// )
393/// .to_owned();
394///
395/// assert_eq!(
396/// query.to_string(MysqlQueryBuilder),
397/// r#"SELECT `image` FROM `glyph` WHERE `glyph`.`aspect` IN (3, 4) AND `glyph`.`image` LIKE 'A%'"#
398/// );
399#[macro_export]
400macro_rules! all {
401 ( $( $x:expr ),* $(,)?) => {
402 {
403 let mut tmp = $crate::Condition::all();
404 $(
405 tmp = tmp.add($x);
406 )*
407 tmp
408 }
409 };
410}
411
412pub trait ConditionalStatement {
413 /// And where condition.
414 /// Calling `or_where` after `and_where` will panic.
415 ///
416 /// # Examples
417 ///
418 /// ```
419 /// use sea_query::{*, tests_cfg::*};
420 ///
421 /// let query = Query::select()
422 /// .column(Glyph::Image)
423 /// .from(Glyph::Table)
424 /// .and_where(Expr::col((Glyph::Table, Glyph::Aspect)).is_in([3, 4]))
425 /// .and_where(Expr::col((Glyph::Table, Glyph::Image)).like("A%"))
426 /// .to_owned();
427 ///
428 /// assert_eq!(
429 /// query.to_string(MysqlQueryBuilder),
430 /// r#"SELECT `image` FROM `glyph` WHERE `glyph`.`aspect` IN (3, 4) AND `glyph`.`image` LIKE 'A%'"#
431 /// );
432 /// ```
433 fn and_where(&mut self, other: Expr) -> &mut Self {
434 self.cond_where(other)
435 }
436
437 /// Optional and where, short hand for `if c.is_some() q.and_where(c)`.
438 ///
439 /// ```
440 /// use sea_query::{tests_cfg::*, *};
441 ///
442 /// let query = Query::select()
443 /// .column(Glyph::Image)
444 /// .from(Glyph::Table)
445 /// .and_where(Expr::col(Glyph::Aspect).is_in([3, 4]))
446 /// .and_where_option(Some(Expr::col(Glyph::Image).like("A%")))
447 /// .and_where_option(None)
448 /// .to_owned();
449 ///
450 /// assert_eq!(
451 /// query.to_string(MysqlQueryBuilder),
452 /// r#"SELECT `image` FROM `glyph` WHERE `aspect` IN (3, 4) AND `image` LIKE 'A%'"#
453 /// );
454 /// ```
455 fn and_where_option(&mut self, other: Option<Expr>) -> &mut Self {
456 if let Some(other) = other {
457 self.and_where(other);
458 }
459 self
460 }
461
462 #[doc(hidden)]
463 // Trait implementation.
464 fn and_or_where(&mut self, condition: LogicalChainOper) -> &mut Self;
465
466 /// Where condition, expressed with `any` and `all`.
467 /// Calling `cond_where` multiple times will conjoin them.
468 /// Calling `or_where` after `cond_where` will panic.
469 ///
470 /// # Examples
471 ///
472 /// ```
473 /// use sea_query::{*, tests_cfg::*};
474 ///
475 /// let query = Query::select()
476 /// .column(Glyph::Image)
477 /// .from(Glyph::Table)
478 /// .cond_where(
479 /// Cond::all()
480 /// .add(Expr::col((Glyph::Table, Glyph::Aspect)).is_in([3, 4]))
481 /// .add(Cond::any()
482 /// .add(Expr::col((Glyph::Table, Glyph::Image)).like("A%"))
483 /// .add(Expr::col((Glyph::Table, Glyph::Image)).like("B%"))
484 /// )
485 /// )
486 /// .to_owned();
487 ///
488 /// assert_eq!(
489 /// query.to_string(PostgresQueryBuilder),
490 /// r#"SELECT "image" FROM "glyph" WHERE "glyph"."aspect" IN (3, 4) AND ("glyph"."image" LIKE 'A%' OR "glyph"."image" LIKE 'B%')"#
491 /// );
492 /// ```
493 ///
494 /// Using macro
495 ///
496 /// ```
497 /// use sea_query::{*, tests_cfg::*};
498 ///
499 /// let query = Query::select()
500 /// .column(Glyph::Image)
501 /// .from(Glyph::Table)
502 /// .cond_where(
503 /// all![
504 /// Expr::col((Glyph::Table, Glyph::Aspect)).is_in([3, 4]),
505 /// any![
506 /// Expr::col((Glyph::Table, Glyph::Image)).like("A%"),
507 /// Expr::col((Glyph::Table, Glyph::Image)).like("B%"),
508 /// ]
509 /// ])
510 /// .to_owned();
511 ///
512 /// assert_eq!(
513 /// query.to_string(PostgresQueryBuilder),
514 /// r#"SELECT "image" FROM "glyph" WHERE "glyph"."aspect" IN (3, 4) AND ("glyph"."image" LIKE 'A%' OR "glyph"."image" LIKE 'B%')"#
515 /// );
516 /// ```
517 ///
518 /// Calling multiple times; the following two are equivalent:
519 ///
520 /// ```
521 /// use sea_query::{tests_cfg::*, *};
522 ///
523 /// assert_eq!(
524 /// Query::select()
525 /// .column(Glyph::Id)
526 /// .from(Glyph::Table)
527 /// .cond_where(Expr::col(Glyph::Id).eq(1))
528 /// .cond_where(any![Expr::col(Glyph::Id).eq(2), Expr::col(Glyph::Id).eq(3)])
529 /// .to_owned()
530 /// .to_string(PostgresQueryBuilder),
531 /// r#"SELECT "id" FROM "glyph" WHERE "id" = 1 AND ("id" = 2 OR "id" = 3)"#
532 /// );
533 ///
534 /// assert_eq!(
535 /// Query::select()
536 /// .column(Glyph::Id)
537 /// .from(Glyph::Table)
538 /// .cond_where(any![Expr::col(Glyph::Id).eq(2), Expr::col(Glyph::Id).eq(3)])
539 /// .cond_where(Expr::col(Glyph::Id).eq(1))
540 /// .to_owned()
541 /// .to_string(PostgresQueryBuilder),
542 /// r#"SELECT "id" FROM "glyph" WHERE ("id" = 2 OR "id" = 3) AND "id" = 1"#
543 /// );
544 /// ```
545 ///
546 /// Calling multiple times; will be ANDed togother
547 ///
548 /// ```
549 /// use sea_query::{tests_cfg::*, *};
550 ///
551 /// assert_eq!(
552 /// Query::select()
553 /// .column(Glyph::Id)
554 /// .from(Glyph::Table)
555 /// .cond_where(any![Expr::col(Glyph::Id).eq(1), Expr::col(Glyph::Id).eq(2)])
556 /// .cond_where(any![Expr::col(Glyph::Id).eq(3), Expr::col(Glyph::Id).eq(4)])
557 /// .to_owned()
558 /// .to_string(PostgresQueryBuilder),
559 /// r#"SELECT "id" FROM "glyph" WHERE ("id" = 1 OR "id" = 2) AND ("id" = 3 OR "id" = 4)"#
560 /// );
561 ///
562 /// assert_eq!(
563 /// Query::select()
564 /// .column(Glyph::Id)
565 /// .from(Glyph::Table)
566 /// .cond_where(all![Expr::col(Glyph::Id).eq(1), Expr::col(Glyph::Id).eq(2)])
567 /// .cond_where(all![Expr::col(Glyph::Id).eq(3), Expr::col(Glyph::Id).eq(4)])
568 /// .to_owned()
569 /// .to_string(PostgresQueryBuilder),
570 /// r#"SELECT "id" FROM "glyph" WHERE "id" = 1 AND "id" = 2 AND "id" = 3 AND "id" = 4"#
571 /// );
572 /// ```
573 ///
574 /// Some more test cases involving negation
575 ///
576 /// ```
577 /// use sea_query::{tests_cfg::*, *};
578 ///
579 /// assert_eq!(
580 /// Query::select()
581 /// .column(Glyph::Id)
582 /// .from(Glyph::Table)
583 /// .cond_where(
584 /// Cond::all()
585 /// .not()
586 /// .add(Expr::col(Glyph::Id).eq(1))
587 /// .add(Expr::col(Glyph::Id).eq(2)),
588 /// )
589 /// .cond_where(
590 /// Cond::all()
591 /// .add(Expr::col(Glyph::Id).eq(3))
592 /// .add(Expr::col(Glyph::Id).eq(4)),
593 /// )
594 /// .to_owned()
595 /// .to_string(PostgresQueryBuilder),
596 /// r#"SELECT "id" FROM "glyph" WHERE (NOT ("id" = 1 AND "id" = 2)) AND ("id" = 3 AND "id" = 4)"#
597 /// );
598 ///
599 /// assert_eq!(
600 /// Query::select()
601 /// .column(Glyph::Id)
602 /// .from(Glyph::Table)
603 /// .cond_where(
604 /// Cond::all()
605 /// .add(Expr::col(Glyph::Id).eq(3))
606 /// .add(Expr::col(Glyph::Id).eq(4)),
607 /// )
608 /// .cond_where(
609 /// Cond::all()
610 /// .not()
611 /// .add(Expr::col(Glyph::Id).eq(1))
612 /// .add(Expr::col(Glyph::Id).eq(2)),
613 /// )
614 /// .to_owned()
615 /// .to_string(PostgresQueryBuilder),
616 /// r#"SELECT "id" FROM "glyph" WHERE "id" = 3 AND "id" = 4 AND (NOT ("id" = 1 AND "id" = 2))"#
617 /// );
618 /// ```
619 fn cond_where<C>(&mut self, condition: C) -> &mut Self
620 where
621 C: IntoCondition;
622}
623
624impl ConditionHolder {
625 pub fn new() -> Self {
626 Self::default()
627 }
628
629 pub fn new_with_condition(condition: Condition) -> Self {
630 let contents = ConditionHolderContents::Condition(condition);
631 Self { contents }
632 }
633
634 pub fn is_empty(&self) -> bool {
635 match &self.contents {
636 ConditionHolderContents::Empty => true,
637 ConditionHolderContents::Chain(c) => c.is_empty(),
638 ConditionHolderContents::Condition(c) => c.conditions.is_empty(),
639 }
640 }
641
642 pub fn is_one(&self) -> bool {
643 match &self.contents {
644 ConditionHolderContents::Empty => true,
645 ConditionHolderContents::Chain(c) => c.len() == 1,
646 ConditionHolderContents::Condition(c) => c.conditions.len() == 1,
647 }
648 }
649
650 pub fn add_and_or(&mut self, condition: LogicalChainOper) {
651 match &mut self.contents {
652 ConditionHolderContents::Empty => {
653 self.contents = ConditionHolderContents::Chain(vec![condition])
654 }
655 ConditionHolderContents::Chain(c) => c.push(condition),
656 ConditionHolderContents::Condition(_) => {
657 panic!("Cannot mix `and_where`/`or_where` and `cond_where` in statements")
658 }
659 }
660 }
661
662 pub fn add_condition(&mut self, mut addition: Condition) {
663 match std::mem::take(&mut self.contents) {
664 ConditionHolderContents::Empty => {
665 self.contents = ConditionHolderContents::Condition(addition);
666 }
667 ConditionHolderContents::Condition(mut current) => {
668 if current.condition_type == ConditionType::All && !current.negate {
669 if addition.condition_type == ConditionType::All && !addition.negate {
670 current.conditions.append(&mut addition.conditions);
671 self.contents = ConditionHolderContents::Condition(current);
672 } else {
673 self.contents = ConditionHolderContents::Condition(current.add(addition));
674 }
675 } else {
676 self.contents = ConditionHolderContents::Condition(
677 Condition::all().add(current).add(addition),
678 );
679 }
680 }
681 ConditionHolderContents::Chain(_) => {
682 panic!("Cannot mix `and_where`/`or_where` and `cond_where` in statements")
683 }
684 }
685 }
686}
687
688#[cfg(test)]
689mod test {
690 use crate::{tests_cfg::*, *};
691 use pretty_assertions::assert_eq;
692
693 #[test]
694 #[cfg(feature = "backend-mysql")]
695 fn test_blank_condition() {
696 let query = Query::select()
697 .column(Glyph::Image)
698 .from(Glyph::Table)
699 .cond_where(Cond::all())
700 .cond_where(Expr::val(1).eq(1))
701 .cond_where(Expr::val(2).eq(2))
702 .cond_where(Cond::any().add(Expr::val(3).eq(3)).add(Expr::val(4).eq(4)))
703 .to_owned();
704
705 assert_eq!(
706 query.to_string(MysqlQueryBuilder),
707 "SELECT `image` FROM `glyph` WHERE 1 = 1 AND 2 = 2 AND (3 = 3 OR 4 = 4)"
708 );
709 }
710}