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