Skip to main content

rorm_sql/
create_index.rs

1use crate::error::Error;
2
3/**
4Representation of a CREATE INDEX builder.
5*/
6pub trait CreateIndex<'until_build> {
7    /**
8    Creates a unique index.
9
10    Null values are considered different from all other null values.
11     */
12    fn unique(self) -> Self;
13
14    /**
15    Creates the index only if it doesn't exist yet.
16     */
17    fn if_not_exists(self) -> Self;
18
19    /**
20    Adds a column to the index.
21
22    **Parameter**:
23    - `column`: String representing the column to index.
24     */
25    fn add_column(self, column: &'until_build str) -> Self;
26
27    /**
28    Sets the condition to apply. This will build a partial index.
29
30    **Parameter**:
31    - `condition`: String representing condition to apply the index to
32     */
33    fn set_condition(self, condition: String) -> Self;
34
35    /**
36    This method is used to build the create index operation
37     */
38    fn build(self) -> Result<String, Error>;
39}
40
41/**
42Representation of a create index operation
43*/
44pub struct CreateIndexData<'until_build> {
45    pub(crate) name: &'until_build str,
46    pub(crate) table_name: &'until_build str,
47    pub(crate) unique: bool,
48    pub(crate) if_not_exists: bool,
49    pub(crate) columns: Vec<&'until_build str>,
50    pub(crate) condition: Option<String>,
51}
52
53/**
54Implementation of database specific implementations of the [CreateIndex] trait.
55
56Should only be constructed via [crate::DBImpl::create_index].
57*/
58pub enum CreateIndexImpl<'until_build> {
59    /**
60    SQLite representation of the CREATE INDEX operation.
61     */
62    #[cfg(feature = "sqlite")]
63    Sqlite(CreateIndexData<'until_build>),
64    /**
65    Postgres representation of the CREATE INDEX operation.
66     */
67    #[cfg(feature = "postgres")]
68    Postgres(CreateIndexData<'until_build>),
69}
70
71impl<'until_build> CreateIndex<'until_build> for CreateIndexImpl<'until_build> {
72    fn unique(mut self) -> Self {
73        match self {
74            #[cfg(feature = "sqlite")]
75            CreateIndexImpl::Sqlite(ref mut d) => d.unique = true,
76            #[cfg(feature = "postgres")]
77            CreateIndexImpl::Postgres(ref mut d) => d.unique = true,
78        };
79        self
80    }
81
82    fn if_not_exists(mut self) -> Self {
83        match self {
84            #[cfg(feature = "sqlite")]
85            CreateIndexImpl::Sqlite(ref mut d) => d.if_not_exists = true,
86            #[cfg(feature = "postgres")]
87            CreateIndexImpl::Postgres(ref mut d) => d.if_not_exists = true,
88        };
89        self
90    }
91
92    fn add_column(mut self, column: &'until_build str) -> Self {
93        match self {
94            #[cfg(feature = "sqlite")]
95            CreateIndexImpl::Sqlite(ref mut d) => d.columns.push(column),
96            #[cfg(feature = "postgres")]
97            CreateIndexImpl::Postgres(ref mut d) => d.columns.push(column),
98        }
99        self
100    }
101
102    fn set_condition(mut self, condition: String) -> Self {
103        match self {
104            #[cfg(feature = "sqlite")]
105            CreateIndexImpl::Sqlite(ref mut d) => d.condition = Some(condition),
106            #[cfg(feature = "postgres")]
107            CreateIndexImpl::Postgres(ref mut d) => d.condition = Some(condition),
108        }
109        self
110    }
111
112    fn build(self) -> Result<String, Error> {
113        match self {
114            #[cfg(feature = "sqlite")]
115            CreateIndexImpl::Sqlite(d) => {
116                if d.columns.is_empty() {
117                    return Err(Error::SQLBuildError(format!(
118                        "Couldn't create index on {}: Missing column(s) to create the index on",
119                        d.table_name
120                    )));
121                }
122
123                Ok(format!(
124                    "CREATE{} INDEX{} \"{}\" ON \"{}\" ({}){};",
125                    if d.unique { " UNIQUE" } else { "" },
126                    if d.if_not_exists {
127                        " IF NOT EXISTS"
128                    } else {
129                        ""
130                    },
131                    d.name,
132                    d.table_name,
133                    quote_columns(&d.columns),
134                    match d.condition {
135                        None => String::from(""),
136                        Some(cond) => format!(" WHERE {}", cond.as_str()),
137                    }
138                ))
139            }
140            #[cfg(feature = "postgres")]
141            CreateIndexImpl::Postgres(d) => {
142                if d.columns.is_empty() {
143                    return Err(Error::SQLBuildError(format!(
144                        "Couldn't create index on {}: Missing column(s) to create the index on",
145                        d.table_name
146                    )));
147                }
148
149                Ok(format!(
150                    "CREATE{} INDEX{} \"{}\" ON \"{}\" ({}){};",
151                    if d.unique { " UNIQUE" } else { "" },
152                    if d.if_not_exists {
153                        " IF NOT EXISTS"
154                    } else {
155                        ""
156                    },
157                    d.name,
158                    d.table_name,
159                    quote_columns(&d.columns),
160                    match d.condition {
161                        None => String::from(""),
162                        Some(cond) => format!(" WHERE {}", cond.as_str()),
163                    }
164                ))
165            }
166        }
167    }
168}
169
170/// Joins the `columns` into the comma separated list of a CREATE INDEX statement
171fn quote_columns(columns: &[&str]) -> String {
172    columns
173        .iter()
174        .map(|column| format!("\"{column}\""))
175        .collect::<Vec<_>>()
176        .join(", ")
177}
178
179#[cfg(test)]
180mod test {
181    use crate::create_index::CreateIndex;
182    use crate::DBImpl;
183
184    #[cfg(feature = "sqlite")]
185    #[test]
186    fn single_column_sqlite() {
187        assert_eq!(
188            DBImpl::SQLite
189                .create_index("user_login_idx", "user")
190                .add_column("login")
191                .build()
192                .unwrap(),
193            r#"CREATE INDEX "user_login_idx" ON "user" ("login");"#
194        );
195    }
196
197    #[cfg(feature = "postgres")]
198    #[test]
199    fn single_column_postgres() {
200        assert_eq!(
201            DBImpl::Postgres
202                .create_index("user_login_idx", "user")
203                .add_column("login")
204                .build()
205                .unwrap(),
206            r#"CREATE INDEX "user_login_idx" ON "user" ("login");"#
207        );
208    }
209
210    #[cfg(feature = "sqlite")]
211    #[test]
212    fn multiple_columns_sqlite() {
213        assert_eq!(
214            DBImpl::SQLite
215                .create_index("user_full_name_idx", "user")
216                .add_column("last_name")
217                .add_column("first_name")
218                .build()
219                .unwrap(),
220            r#"CREATE INDEX "user_full_name_idx" ON "user" ("last_name", "first_name");"#
221        );
222    }
223
224    #[cfg(feature = "postgres")]
225    #[test]
226    fn multiple_columns_postgres() {
227        assert_eq!(
228            DBImpl::Postgres
229                .create_index("user_full_name_idx", "user")
230                .add_column("last_name")
231                .add_column("first_name")
232                .build()
233                .unwrap(),
234            r#"CREATE INDEX "user_full_name_idx" ON "user" ("last_name", "first_name");"#
235        );
236    }
237
238    #[cfg(feature = "sqlite")]
239    #[test]
240    fn unique_and_if_not_exists_sqlite() {
241        assert_eq!(
242            DBImpl::SQLite
243                .create_index("user_login_idx", "user")
244                .add_column("login")
245                .unique()
246                .if_not_exists()
247                .build()
248                .unwrap(),
249            r#"CREATE UNIQUE INDEX IF NOT EXISTS "user_login_idx" ON "user" ("login");"#
250        );
251    }
252
253    #[cfg(feature = "sqlite")]
254    #[test]
255    fn condition_sqlite() {
256        assert_eq!(
257            DBImpl::SQLite
258                .create_index("user_login_idx", "user")
259                .add_column("login")
260                .set_condition("\"login\" IS NOT NULL".to_string())
261                .build()
262                .unwrap(),
263            r#"CREATE INDEX "user_login_idx" ON "user" ("login") WHERE "login" IS NOT NULL;"#
264        );
265    }
266
267    #[cfg(feature = "sqlite")]
268    #[test]
269    fn missing_columns_is_an_error() {
270        assert!(DBImpl::SQLite
271            .create_index("user_login_idx", "user")
272            .build()
273            .is_err());
274    }
275}