1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
use super::SourceAlias;

/// Each projection field is defined by
/// 1. a definition
/// 1. an alias
/// 1. an output type
///
/// by example `SELECT a.title as title`
///  * `a.title` is the definition
///  * `title` is the field name
///  * the type here is the same as the input type (most likely text)
///
/// Other example: `count(c.*) as comment_count`
///  * `count(c.*)` is the definition
///  * `comment_count` is the field name
///  * type is int as the SQL `count` aggregate function returns an integer.
pub struct ProjectionField {
    /// Field name alias, this is the output name of the field.
    pub name: String,

    /// Field definition. Some field definitions can be fairly complex like `CASE … WHEN …` or using functions.
    pub definition: String,

    /// This indicates the SQL type of the output data.
    pub output_type: String,
}

impl ProjectionField {
    /// [ProjectionField] constructor
    pub fn new(name: &str, definition: &str, output_type: &str) -> Self {
        Self {
            name: name.to_string(),
            definition: definition.to_string(),
            output_type: output_type.to_string(),
        }
    }
}

/// Projection is a definition of field mapping during a query.
/// Fields come from one or several source structures (can be tables, views or
/// sub queries) and are mapped to a Query as output.
pub struct Projection {
    fields: Vec<ProjectionField>,
}

impl Projection {
    /// Instantiate a new Projection
    pub fn new(fields: Vec<ProjectionField>) -> Self {
        Self { fields }
    }

    /// Create a Projection from a list of tuples `&[(name, definition, sql_type)]`.
    pub fn from(fields: &[(&str, &str, &str)]) -> Self {
        let field_defs: Vec<ProjectionField> = fields
            .iter()
            .map(|(name, definition, sql_type)| ProjectionField::new(name, definition, sql_type))
            .collect();

        Self::new(field_defs)
    }

    /// Add a new field to the definition. This is one of the projection
    /// building tool to create a projection out of an existing structure.
    /// This is a blanket implementation.
    pub fn add_field(&mut self, field_name: &str, definition: &str, output_type: &str) {
        self.fields.push(ProjectionField {
            name: field_name.to_string(),
            definition: definition.to_string(),
            output_type: output_type.to_string(),
        })
    }

    /// Returns the list of the ProjectionFields of this Projection.
    pub fn get_fields(&self) -> &Vec<ProjectionField> {
        &self.fields
    }

    /// Turn the Projection into a string suitable for use in SQL queries.
    pub fn expand(&self, aliases: SourceAlias) -> String {
        let mut fields: String = self
            .get_fields()
            .iter()
            .map(|field| format!("{} as {}", field.definition, field.name))
            .collect::<Vec<String>>()
            .join(", ");

        for (alias, source) in aliases.get_iterator() {
            fields = fields.replace(alias, source);
        }

        fields
    }
}

impl Default for Projection {
    fn default() -> Self {
        Self::new(Vec::new())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn simple_projection() {
        let mut projection = Projection::default();
        projection.add_field("test_id", "{:test:}.test_id", "integer");
        projection.add_field("name", "{:test:}.name", "text");
        projection.add_field("created_at", "{:test:}.created_at", "timestamp");
        projection.add_field("thing_count", "count({:thing:}.*)", "integer");

        let aliases = SourceAlias::new(&[("{:test:}", "pika"), ("{:thing:}", "thing_alias")]);

        assert_eq!(
            "pika.test_id as test_id, pika.name as name, pika.created_at as created_at, count(thing_alias.*) as thing_count".to_string(),
            projection.expand(aliases)
        )
    }

    #[test]
    fn list_constructor() {
        let projection = Projection::from(&[
            ("something_id", "{:test:}.something_id", "integer"),
            ("name", "{:test:}.name", "text"),
            ("created_at", "{:test:}.created_at", "timestamp"),
        ]);

        let aliases = SourceAlias::new(&[("{:test:}", "test")]);

        assert_eq!(
            "test.something_id as something_id, test.name as name, test.created_at as created_at",
            projection.expand(aliases)
        );
    }
}