gluesql_core/data/
schema.rs

1use {
2    crate::{
3        ast::{ColumnDef, Expr, ForeignKey, OrderByExpr, Statement, ToSql},
4        prelude::{parse, translate},
5        result::Result,
6    },
7    chrono::{NaiveDateTime, Utc},
8    serde::{Deserialize, Serialize},
9    std::{fmt::Debug, iter},
10    strum_macros::Display,
11    thiserror::Error as ThisError,
12};
13
14#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Display)]
15#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
16pub enum SchemaIndexOrd {
17    Asc,
18    Desc,
19    Both,
20}
21
22#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
23pub struct SchemaIndex {
24    pub name: String,
25    pub expr: Expr,
26    pub order: SchemaIndexOrd,
27    pub created: NaiveDateTime,
28}
29
30#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
31pub struct Schema {
32    pub table_name: String,
33    pub column_defs: Option<Vec<ColumnDef>>,
34    pub indexes: Vec<SchemaIndex>,
35    pub engine: Option<String>,
36    pub foreign_keys: Vec<ForeignKey>,
37    pub comment: Option<String>,
38}
39
40impl Schema {
41    pub fn to_ddl(&self) -> String {
42        let Schema {
43            table_name,
44            column_defs,
45            indexes,
46            engine,
47            foreign_keys,
48            comment,
49        } = self;
50
51        let columns = column_defs.as_ref().map(|column_defs| {
52            let foreign_keys = foreign_keys.iter().map(ToSql::to_sql);
53            let body = column_defs
54                .iter()
55                .map(ToSql::to_sql)
56                .chain(foreign_keys)
57                .collect::<Vec<_>>()
58                .join(", ");
59
60            format!("({body})")
61        });
62        let engine = engine.as_ref().map(|engine| format!(" ENGINE = {engine}"));
63        let comment = comment
64            .as_ref()
65            .map(|comment| format!(" COMMENT = '{comment}'"));
66
67        let create_table = vec![
68            Some(format!(r#"CREATE TABLE "{table_name}""#)),
69            columns,
70            engine,
71            comment,
72        ]
73        .into_iter()
74        .flatten()
75        .collect::<Vec<_>>()
76        .join(" ")
77            + ";";
78
79        let create_indexes = indexes.iter().map(|SchemaIndex { name, expr, .. }| {
80            let expr = expr.to_sql();
81
82            format!(r#"CREATE INDEX "{name}" ON "{table_name}" ({expr});"#)
83        });
84
85        iter::once(create_table)
86            .chain(create_indexes)
87            .collect::<Vec<_>>()
88            .join("\n")
89    }
90
91    pub fn from_ddl(ddl: &str) -> Result<Schema> {
92        let created = Utc::now().naive_utc();
93        let statements = parse(ddl)?;
94
95        let indexes = statements
96            .iter()
97            .skip(1)
98            .map(|create_index| {
99                let create_index = translate(create_index)?;
100                match create_index {
101                    Statement::CreateIndex {
102                        name,
103                        column: OrderByExpr { expr, asc },
104                        ..
105                    } => {
106                        let order = asc
107                            .and_then(|bool| bool.then_some(SchemaIndexOrd::Asc))
108                            .unwrap_or(SchemaIndexOrd::Both);
109
110                        let index = SchemaIndex {
111                            name,
112                            expr,
113                            order,
114                            created,
115                        };
116
117                        Ok(index)
118                    }
119                    _ => Err(SchemaParseError::CannotParseDDL.into()),
120                }
121            })
122            .collect::<Result<Vec<_>>>()?;
123
124        let create_table = statements.first().ok_or(SchemaParseError::CannotParseDDL)?;
125        let create_table = translate(create_table)?;
126
127        match create_table {
128            Statement::CreateTable {
129                name,
130                columns,
131                engine,
132                foreign_keys,
133                comment,
134                ..
135            } => Ok(Schema {
136                table_name: name,
137                column_defs: columns,
138                indexes,
139                engine,
140                foreign_keys,
141                comment,
142            }),
143            _ => Err(SchemaParseError::CannotParseDDL.into()),
144        }
145    }
146}
147
148#[derive(ThisError, Debug, PartialEq, Serialize)]
149pub enum SchemaParseError {
150    #[error("cannot parse ddl")]
151    CannotParseDDL,
152}
153
154#[cfg(test)]
155mod tests {
156    use {
157        super::SchemaParseError,
158        crate::{
159            ast::{AstLiteral, ColumnDef, ColumnUniqueOption, Expr},
160            chrono::Utc,
161            data::{Schema, SchemaIndex, SchemaIndexOrd},
162            prelude::DataType,
163        },
164    };
165
166    fn assert_schema(actual: Schema, expected: Schema) {
167        let Schema {
168            table_name,
169            column_defs,
170            indexes,
171            engine,
172            foreign_keys,
173            comment,
174        } = actual;
175
176        let Schema {
177            table_name: table_name_e,
178            column_defs: column_defs_e,
179            indexes: indexes_e,
180            engine: engine_e,
181            foreign_keys: foreign_keys_e,
182            comment: comment_e,
183        } = expected;
184
185        assert_eq!(table_name, table_name_e);
186        assert_eq!(column_defs, column_defs_e);
187        assert_eq!(engine, engine_e);
188        assert_eq!(foreign_keys, foreign_keys_e);
189        assert_eq!(comment, comment_e);
190        indexes
191            .into_iter()
192            .zip(indexes_e)
193            .for_each(|(actual, expected)| assert_index(actual, expected));
194    }
195
196    fn assert_index(actual: SchemaIndex, expected: SchemaIndex) {
197        let SchemaIndex {
198            name, expr, order, ..
199        } = actual;
200        let SchemaIndex {
201            name: name_e,
202            expr: expr_e,
203            order: order_e,
204            ..
205        } = expected;
206
207        assert_eq!(name, name_e);
208        assert_eq!(expr, expr_e);
209        assert_eq!(order, order_e);
210    }
211
212    #[test]
213    fn table_basic() {
214        let schema = Schema {
215            table_name: "User".to_owned(),
216            column_defs: Some(vec![
217                ColumnDef {
218                    name: "id".to_owned(),
219                    data_type: DataType::Int,
220                    nullable: false,
221                    default: None,
222                    unique: None,
223                    comment: None,
224                },
225                ColumnDef {
226                    name: "name".to_owned(),
227                    data_type: DataType::Text,
228                    nullable: true,
229                    default: Some(Expr::Literal(AstLiteral::QuotedString("glue".to_owned()))),
230                    unique: None,
231                    comment: None,
232                },
233            ]),
234            indexes: Vec::new(),
235            engine: None,
236            foreign_keys: Vec::new(),
237            comment: None,
238        };
239
240        let ddl = r#"CREATE TABLE "User" ("id" INT NOT NULL, "name" TEXT NULL DEFAULT 'glue');"#;
241        assert_eq!(schema.to_ddl(), ddl);
242
243        let actual = Schema::from_ddl(ddl).unwrap();
244        assert_schema(actual, schema);
245
246        let schema = Schema {
247            table_name: "Test".to_owned(),
248            column_defs: None,
249            indexes: Vec::new(),
250            engine: None,
251            foreign_keys: Vec::new(),
252            comment: None,
253        };
254        let ddl = r#"CREATE TABLE "Test";"#;
255        assert_eq!(schema.to_ddl(), ddl);
256
257        let actual = Schema::from_ddl(ddl).unwrap();
258        assert_schema(actual, schema);
259    }
260
261    #[test]
262    fn table_primary() {
263        let schema = Schema {
264            table_name: "User".to_owned(),
265            column_defs: Some(vec![ColumnDef {
266                name: "id".to_owned(),
267                data_type: DataType::Int,
268                nullable: false,
269                default: None,
270                unique: Some(ColumnUniqueOption { is_primary: true }),
271                comment: None,
272            }]),
273            indexes: Vec::new(),
274            engine: None,
275            foreign_keys: Vec::new(),
276            comment: None,
277        };
278
279        let ddl = r#"CREATE TABLE "User" ("id" INT NOT NULL PRIMARY KEY);"#;
280        assert_eq!(schema.to_ddl(), ddl);
281
282        let actual = Schema::from_ddl(ddl).unwrap();
283        assert_schema(actual, schema);
284    }
285
286    #[test]
287    fn invalid_ddl() {
288        // Only Statement::CreateTable is supported
289        let invalid_ddl = r#"DROP TABLE "Users";"#;
290        let actual = Schema::from_ddl(invalid_ddl);
291        assert_eq!(actual, Err(SchemaParseError::CannotParseDDL.into()));
292    }
293
294    #[test]
295    fn table_with_index() {
296        let schema = Schema {
297            table_name: "User".to_owned(),
298            column_defs: Some(vec![
299                ColumnDef {
300                    name: "id".to_owned(),
301                    data_type: DataType::Int,
302                    nullable: false,
303                    default: None,
304                    unique: None,
305                    comment: None,
306                },
307                ColumnDef {
308                    name: "name".to_owned(),
309                    data_type: DataType::Text,
310                    nullable: false,
311                    default: None,
312                    unique: None,
313                    comment: None,
314                },
315            ]),
316            indexes: vec![
317                SchemaIndex {
318                    name: "User_id".to_owned(),
319                    expr: Expr::Identifier("id".to_owned()),
320                    order: SchemaIndexOrd::Both,
321                    created: Utc::now().naive_utc(),
322                },
323                SchemaIndex {
324                    name: "User_name".to_owned(),
325                    expr: Expr::Identifier("name".to_owned()),
326                    order: SchemaIndexOrd::Both,
327                    created: Utc::now().naive_utc(),
328                },
329            ],
330            engine: None,
331            foreign_keys: Vec::new(),
332            comment: None,
333        };
334        let ddl = r#"CREATE TABLE "User" ("id" INT NOT NULL, "name" TEXT NOT NULL);
335CREATE INDEX "User_id" ON "User" ("id");
336CREATE INDEX "User_name" ON "User" ("name");"#;
337        assert_eq!(schema.to_ddl(), ddl);
338
339        let actual = Schema::from_ddl(ddl).unwrap();
340        assert_schema(actual, schema);
341
342        let index_should_not_be_first = r#"CREATE INDEX "User_id" ON "User" ("id");
343CREATE TABLE "User" ("id" INT NOT NULL, "name" TEXT NOT NULL);"#;
344        let actual = Schema::from_ddl(index_should_not_be_first);
345        assert_eq!(actual, Err(SchemaParseError::CannotParseDDL.into()));
346    }
347
348    #[test]
349    fn non_word_identifier() {
350        let schema = Schema {
351            table_name: 1.to_string(),
352            column_defs: Some(vec![
353                ColumnDef {
354                    name: 2.to_string(),
355                    data_type: DataType::Int,
356                    nullable: true,
357                    default: None,
358                    unique: None,
359                    comment: None,
360                },
361                ColumnDef {
362                    name: ";".to_owned(),
363                    data_type: DataType::Int,
364                    nullable: true,
365                    default: None,
366                    unique: None,
367                    comment: None,
368                },
369            ]),
370            indexes: vec![SchemaIndex {
371                name: ".".to_owned(),
372                expr: Expr::Identifier(";".to_owned()),
373                order: SchemaIndexOrd::Both,
374                created: Utc::now().naive_utc(),
375            }],
376            engine: None,
377            foreign_keys: Vec::new(),
378            comment: None,
379        };
380        let ddl = r#"CREATE TABLE "1" ("2" INT NULL, ";" INT NULL);
381CREATE INDEX "." ON "1" (";");"#;
382        assert_eq!(schema.to_ddl(), ddl);
383
384        let actual = Schema::from_ddl(ddl).unwrap();
385        assert_schema(actual, schema);
386    }
387}