mithril_persistence/sqlite/
query.rs

1use super::{SqLiteEntity, WhereCondition};
2
3/// Define a query to perform on database and return iterator of a defined entity.
4///
5/// Used as a parameter of [fetch][crate::sqlite::ConnectionExtensions::fetch].
6///
7/// It aims at being easily testable and adaptable.
8pub trait Query {
9    /// Entity type returned by the result cursor.
10    type Entity: SqLiteEntity;
11
12    /// Return the filters to apply to the query.
13    fn filters(&self) -> WhereCondition;
14
15    /// Return the definition of this query, ie the actual SQL this query performs.
16    fn get_definition(&self, condition: &str) -> String;
17}
18
19#[cfg(test)]
20mod tests {
21    use sqlite::{Connection, Value};
22
23    use crate::sqlite::{
24        ConnectionExtensions, GetAllCondition, Projection, SourceAlias, SqliteConnection,
25    };
26
27    use super::super::{SqLiteEntity, entity::HydrationError};
28    use super::*;
29
30    #[derive(Debug, PartialEq)]
31    struct TestEntity {
32        text_data: String,
33        real_data: f64,
34        integer_data: i64,
35        maybe_null: Option<i64>,
36    }
37
38    impl SqLiteEntity for TestEntity {
39        fn hydrate(row: sqlite::Row) -> Result<Self, HydrationError> {
40            Ok(TestEntity {
41                text_data: row.read::<&str, _>(0).to_string(),
42                real_data: row.read::<f64, _>(1),
43                integer_data: row.read::<i64, _>(2),
44                maybe_null: row.read::<Option<i64>, _>(3),
45            })
46        }
47
48        fn get_projection() -> Projection {
49            let mut projection = Projection::default();
50
51            projection.add_field("text_data", "{:test:}.text_data", "text");
52            projection.add_field("real_data", "{:test:}.real_data", "real");
53            projection.add_field("integer_data", "{:test:}.integer_data", "integer");
54            projection.add_field("maybe_null", "{:test:}.maybe_null", "integer");
55
56            projection
57        }
58    }
59
60    struct GetTestEntityQuery {
61        condition: WhereCondition,
62    }
63
64    impl GetTestEntityQuery {
65        pub fn new(condition: WhereCondition) -> Self {
66            Self { condition }
67        }
68    }
69
70    impl Query for GetTestEntityQuery {
71        type Entity = TestEntity;
72
73        fn filters(&self) -> WhereCondition {
74            self.condition.clone()
75        }
76
77        fn get_definition(&self, condition: &str) -> String {
78            let aliases = SourceAlias::new(&[("{:test:}", "test")]);
79            let projection = Self::Entity::get_projection().expand(aliases);
80
81            format!("select {projection} from query_test as test where {condition}")
82        }
83    }
84
85    struct UpdateTestEntityQuery {
86        condition: WhereCondition,
87    }
88
89    impl UpdateTestEntityQuery {
90        pub fn new(condition: WhereCondition) -> Self {
91            Self { condition }
92        }
93    }
94
95    impl Query for UpdateTestEntityQuery {
96        type Entity = TestEntity;
97
98        fn filters(&self) -> WhereCondition {
99            self.condition.clone()
100        }
101
102        fn get_definition(&self, _condition: &str) -> String {
103            let aliases = SourceAlias::new(&[("{:test:}", "query_test")]);
104            let projection = Self::Entity::get_projection().expand(aliases);
105
106            format!(
107                r#"
108insert into query_test (text_data, real_data, integer_data, maybe_null) values (?1, ?2, ?3, ?4)
109  on conflict (text_data) do update set
110    real_data = excluded.real_data,
111    integer_data = excluded.integer_data,
112    maybe_null = excluded.maybe_null 
113returning {projection}
114"#
115            )
116        }
117    }
118
119    impl GetAllCondition for GetTestEntityQuery {}
120
121    fn init_database() -> SqliteConnection {
122        let connection = Connection::open_thread_safe(":memory:").unwrap();
123        connection
124            .execute(
125                "
126            drop table if exists query_test;
127            create table query_test(text_data text not null primary key, real_data real not null, integer_data integer not null, maybe_null integer);
128            insert into query_test(text_data, real_data, integer_data, maybe_null) values ('row 1', 1.23, -52, null);
129            insert into query_test(text_data, real_data, integer_data, maybe_null) values ('row 2', 2.34, 1789, 0);
130            ",
131            )
132            .unwrap();
133
134        connection
135    }
136
137    #[test]
138    pub fn simple_test() {
139        let connection = init_database();
140        let mut cursor = connection
141            .fetch(GetTestEntityQuery::new(WhereCondition::default()))
142            .unwrap();
143        let entity = cursor.next().expect("there should be two results, none returned");
144        assert_eq!(
145            TestEntity {
146                text_data: "row 1".to_string(),
147                real_data: 1.23,
148                integer_data: -52,
149                maybe_null: None
150            },
151            entity
152        );
153        let entity = cursor.next().expect("there should be two results, only one returned");
154        assert_eq!(
155            TestEntity {
156                text_data: "row 2".to_string(),
157                real_data: 2.34,
158                integer_data: 1789,
159                maybe_null: Some(0)
160            },
161            entity
162        );
163
164        assert!(cursor.next().is_none(), "there should be no result");
165    }
166
167    #[test]
168    pub fn test_condition() {
169        let connection = init_database();
170        let mut cursor = connection
171            .fetch(GetTestEntityQuery::new(WhereCondition::new(
172                "maybe_null is not null",
173                Vec::new(),
174            )))
175            .unwrap();
176        let entity = cursor.next().expect("there should be one result, none returned");
177        assert_eq!(
178            TestEntity {
179                text_data: "row 2".to_string(),
180                real_data: 2.34,
181                integer_data: 1789,
182                maybe_null: Some(0)
183            },
184            entity
185        );
186        assert!(cursor.next().is_none());
187    }
188
189    #[test]
190    pub fn test_parameters() {
191        let connection = init_database();
192        let mut cursor = connection
193            .fetch(GetTestEntityQuery::new(WhereCondition::new(
194                "text_data like ?",
195                vec![Value::String("%1".to_string())],
196            )))
197            .unwrap();
198        let entity = cursor.next().expect("there should be one result, none returned");
199        assert_eq!(
200            TestEntity {
201                text_data: "row 1".to_string(),
202                real_data: 1.23,
203                integer_data: -52,
204                maybe_null: None
205            },
206            entity
207        );
208        assert!(cursor.next().is_none());
209    }
210
211    #[test]
212    fn test_upsertion() {
213        let connection = init_database();
214        let params = [
215            Value::String("row 1".to_string()),
216            Value::Float(1.234),
217            Value::Integer(0),
218            Value::Null,
219        ]
220        .to_vec();
221        let mut cursor = connection
222            .fetch(UpdateTestEntityQuery::new(WhereCondition::new("", params)))
223            .unwrap();
224
225        let entity = cursor.next().expect("there should be one result, none returned");
226        assert_eq!(
227            TestEntity {
228                text_data: "row 1".to_string(),
229                real_data: 1.234,
230                integer_data: 0,
231                maybe_null: None
232            },
233            entity
234        );
235        assert!(cursor.next().is_none());
236    }
237}