mithril_persistence/sqlite/
projection.rs

1use super::SourceAlias;
2
3/// Each projection field is defined by
4/// 1. a definition
5/// 1. an alias
6/// 1. an output type
7///
8/// by example `SELECT a.title as title`
9///  * `a.title` is the definition
10///  * `title` is the field name
11///  * the type here is the same as the input type (most likely text)
12///
13/// Other example: `count(c.*) as comment_count`
14///  * `count(c.*)` is the definition
15///  * `comment_count` is the field name
16///  * type is int as the SQL `count` aggregate function returns an integer.
17pub struct ProjectionField {
18    /// Field name alias, this is the output name of the field.
19    pub name: String,
20
21    /// Field definition. Some field definitions can be fairly complex like `CASE … WHEN …` or using functions.
22    pub definition: String,
23
24    /// This indicates the SQL type of the output data.
25    pub output_type: String,
26}
27
28impl ProjectionField {
29    /// [ProjectionField] constructor
30    pub fn new(name: &str, definition: &str, output_type: &str) -> Self {
31        Self {
32            name: name.to_string(),
33            definition: definition.to_string(),
34            output_type: output_type.to_string(),
35        }
36    }
37}
38
39/// Projection is a definition of field mapping during a query.
40/// Fields come from one or several source structures (can be tables, views or
41/// sub queries) and are mapped to a Query as output.
42pub struct Projection {
43    fields: Vec<ProjectionField>,
44}
45
46impl Projection {
47    /// Instantiate a new Projection
48    pub fn new(fields: Vec<ProjectionField>) -> Self {
49        Self { fields }
50    }
51
52    /// Create a Projection from a list of tuples `&[(name, definition, sql_type)]`.
53    pub fn from(fields: &[(&str, &str, &str)]) -> Self {
54        let field_defs: Vec<ProjectionField> = fields
55            .iter()
56            .map(|(name, definition, sql_type)| ProjectionField::new(name, definition, sql_type))
57            .collect();
58
59        Self::new(field_defs)
60    }
61
62    /// Add a new field to the definition. This is one of the projection
63    /// building tool to create a projection out of an existing structure.
64    /// This is a blanket implementation.
65    pub fn add_field(&mut self, field_name: &str, definition: &str, output_type: &str) {
66        self.fields.push(ProjectionField {
67            name: field_name.to_string(),
68            definition: definition.to_string(),
69            output_type: output_type.to_string(),
70        })
71    }
72
73    /// Returns the list of the ProjectionFields of this Projection.
74    pub fn get_fields(&self) -> &Vec<ProjectionField> {
75        &self.fields
76    }
77
78    /// Turn the Projection into a string suitable for use in SQL queries.
79    pub fn expand(&self, aliases: SourceAlias) -> String {
80        let mut fields: String = self
81            .get_fields()
82            .iter()
83            .map(|field| format!("{} as {}", field.definition, field.name))
84            .collect::<Vec<String>>()
85            .join(", ");
86
87        for (alias, source) in aliases.get_iterator() {
88            fields = fields.replace(alias, source);
89        }
90
91        fields
92    }
93}
94
95impl Default for Projection {
96    fn default() -> Self {
97        Self::new(Vec::new())
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn simple_projection() {
107        let mut projection = Projection::default();
108        projection.add_field("test_id", "{:test:}.test_id", "integer");
109        projection.add_field("name", "{:test:}.name", "text");
110        projection.add_field("created_at", "{:test:}.created_at", "timestamp");
111        projection.add_field("thing_count", "count({:thing:}.*)", "integer");
112
113        let aliases = SourceAlias::new(&[("{:test:}", "pika"), ("{:thing:}", "thing_alias")]);
114
115        assert_eq!(
116            "pika.test_id as test_id, pika.name as name, pika.created_at as created_at, count(thing_alias.*) as thing_count".to_string(),
117            projection.expand(aliases)
118        )
119    }
120
121    #[test]
122    fn list_constructor() {
123        let projection = Projection::from(&[
124            ("something_id", "{:test:}.something_id", "integer"),
125            ("name", "{:test:}.name", "text"),
126            ("created_at", "{:test:}.created_at", "timestamp"),
127        ]);
128
129        let aliases = SourceAlias::new(&[("{:test:}", "test")]);
130
131        assert_eq!(
132            "test.something_id as something_id, test.name as name, test.created_at as created_at",
133            projection.expand(aliases)
134        );
135    }
136}