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 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}