insertfmt_core/
core.rs

1use std::io::{Error, ErrorKind};
2
3use regex::Regex;
4use sqlparser::ast::{Ident, ObjectName, SetExpr, Statement, Values};
5use sqlparser::{dialect::MySqlDialect, parser::Parser, parser::ParserOptions};
6use wasm_bindgen::prelude::wasm_bindgen;
7use wasm_bindgen::JsValue;
8
9pub fn format_insert_queries(sql: &str) -> Result<String, Box<dyn std::error::Error>> {
10    let dialect = MySqlDialect {};
11    let options = ParserOptions::default().with_unescape(false);
12    let ast = Parser::new(&dialect)
13        .with_options(options)
14        .try_with_sql(sql)?
15        .parse_statements()?;
16
17    if !is_insert_only(&ast) {
18        return Err(Box::new(Error::new(
19            ErrorKind::InvalidInput,
20            "this sql contains queries other than insert.",
21        )));
22    }
23
24    let comment_and_query_map = generate_comment_and_query_map(sql);
25
26    let mut formatted_queries: Vec<String> = ast
27        .iter()
28        .filter_map(|query| {
29            if let Statement::Insert {
30                table_name,
31                columns,
32                source,
33                ..
34            } = query
35            {
36                if let SetExpr::Values(values) = *source.clone().unwrap().body {
37                    let max_char_length_vec = get_max_char_length_vec(columns, &values);
38                    let formatted_query = generate_formatted_query(
39                        table_name,
40                        columns,
41                        &values,
42                        &max_char_length_vec,
43                    );
44                    return Some(formatted_query);
45                }
46            }
47            return None;
48        })
49        .collect();
50
51    let result = comment_and_query_map
52        .into_iter()
53        .map(|comment_or_query| {
54            if comment_or_query.starts_with("INSERT INTO") {
55                return formatted_queries.remove(0);
56            } else {
57                return comment_or_query;
58            }
59        })
60        .collect::<Vec<String>>()
61        .join("\n");
62
63    return Ok(result);
64}
65
66#[wasm_bindgen]
67pub fn format_insert_queries_wasm(sql: &str) -> Result<String, JsValue> {
68    return match format_insert_queries(sql) {
69        Ok(formatted_queries) => Ok(formatted_queries),
70        Err(err) => Err(JsValue::from_str(&err.to_string())),
71    };
72}
73
74fn is_insert_only(ast: &[Statement]) -> bool {
75    ast.iter()
76        .all(|query| matches!(query, Statement::Insert { .. }))
77}
78
79// this func returns a vec of comments and query prefixes.
80// the prefixes are used to identify where each comment is written.
81fn generate_comment_and_query_map(sql_with_comment: &str) -> Vec<String> {
82    return Regex::new(r"(--.*)|(INSERT INTO)")
83        .unwrap()
84        .captures_iter(sql_with_comment)
85        .map(|capture| return String::from(&capture[0]))
86        .collect::<Vec<String>>();
87}
88
89fn get_max_char_length_vec(columns: &Vec<Ident>, values: &Values) -> Vec<usize> {
90    return get_char_length_matrix(columns, values)
91        .iter()
92        .fold(
93            vec![vec![0 as usize; 0]; columns.len()],
94            |mut transposed_matrix, char_length_of_row| {
95                char_length_of_row
96                    .iter()
97                    .enumerate()
98                    .for_each(|(column_index, char_length)| {
99                        transposed_matrix[column_index].push(*char_length)
100                    });
101                return transposed_matrix;
102            },
103        )
104        .iter()
105        .map(|char_length_of_column| {
106            return char_length_of_column
107                .into_iter()
108                .max()
109                .unwrap_or(&(0 as usize))
110                .clone();
111        })
112        .collect::<Vec<usize>>();
113}
114
115fn get_char_length_matrix(columns: &Vec<Ident>, values: &Values) -> Vec<Vec<usize>> {
116    return columns
117        .iter()
118        .map(|column| {
119            return column.to_string().len();
120        })
121        .collect::<Vec<usize>>()
122        .chunks(columns.len())
123        .map(|chunk| chunk.to_vec())
124        .chain(values.rows.iter().map(|row| {
125            row.iter()
126                .map(|value| {
127                    return value.to_string().len();
128                })
129                .collect::<Vec<usize>>()
130        }))
131        .collect::<Vec<Vec<usize>>>();
132}
133
134// TODO: make it functional
135// construct formatted query from scratch by using ast data
136fn generate_formatted_query(
137    table_name: &ObjectName,
138    columns: &Vec<Ident>,
139    values: &Values,
140    max_char_length_vec: &Vec<usize>,
141) -> String {
142    let table_name_part: String = String::from("INSERT INTO ") + &table_name.to_string() + "\n";
143
144    let mut column_name_part: String = String::from("(");
145    for (index, column) in columns.iter().enumerate() {
146        let adjustment =
147            String::from(" ").repeat(max_char_length_vec[index] - column.to_string().len());
148        column_name_part = column_name_part + &column.to_string() + &adjustment;
149        if index != columns.len() - 1 {
150            column_name_part += ","
151        }
152    }
153    column_name_part += ")\n";
154
155    let values_part: &str = "VALUES\n";
156
157    let mut rows_part: String = String::from("");
158    for (row_index, row) in values.rows.iter().enumerate() {
159        rows_part += "(";
160        for (column_index, value) in row.iter().enumerate() {
161            let adjustment = String::from(" ")
162                .repeat(max_char_length_vec[column_index] - value.to_string().len());
163            rows_part = rows_part + &value.to_string() + &adjustment;
164            if column_index != row.len() - 1 {
165                rows_part += ","
166            }
167        }
168        rows_part += ")";
169        if row_index != values.rows.len() - 1 {
170            rows_part += ","
171        } else {
172            rows_part += ";"
173        }
174        rows_part += "\n";
175    }
176
177    return String::from("") + &table_name_part + &column_name_part + &values_part + &rows_part;
178}
179
180// // The escape behavior of the sqlparser of v0.33.0 depends on if the value is quoted by backtick or else.
181// // If the value is quoted by backtick, the sqlparser escapes the backslash.
182// // But if the value is quoted by single quote or double quote, the sqlparser does NOT escape the backslash.
183// // This module escape the value WHEN it is quoted by single quote or double quote.
184// fn escape_stringified_value(str: &str) -> String {
185//     let ch1 = str.chars().nth(0).unwrap();
186//     match ch1 {
187//         '\'' => {
188//             return str
189//                 .replace("\n", "\\n")
190//                 .replace("\"", "\\\"")
191//                 .replace("\t", "\\t")
192//                 .replace("\r", "\\r")
193//         }
194//         '"' => {
195//             return str
196//                 .replace("\n", "\\n")
197//                 .replace("'", "\\'")
198//                 .replace("\t", "\\t")
199//                 .replace("\r", "\\r")
200//         }
201//         _ => return str.to_string(),
202//     }
203// }
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn keep_backslashes_in_query_enclosed_with_backticks() {
211        let query_with_backslash_with_backtick =
212            r#"INSERT INTO `table` (`id`, `content`) VALUES (1, `\"e\nxample\"`);"#;
213        let formatted = r#"INSERT INTO `table`
214(`id`,`content`      )
215VALUES
216(1   ,`\"e\nxample\"`);
217"#;
218        assert_eq!(
219            format_insert_queries(query_with_backslash_with_backtick).unwrap(),
220            formatted
221        );
222    }
223
224    #[test]
225    fn keep_backslashes_in_query_enclosed_with_single_quotes() {
226        let query_with_backslash_with_single_quote =
227            r#"INSERT INTO `table` ('id', 'content') VALUES (1, '\"e\nxample\"');"#;
228        let formatted = r#"INSERT INTO `table`
229('id','content'      )
230VALUES
231(1   ,'\"e\nxample\"');
232"#;
233        assert_eq!(
234            format_insert_queries(query_with_backslash_with_single_quote).unwrap(),
235            formatted
236        );
237    }
238
239    #[test]
240    fn keep_backslashes_in_query_enclosed_with_double_quotes() {
241        let query_with_backslash_with_double_quote =
242            r#"INSERT INTO `table` ("id", "content") VALUES (1, "\'e\nxample\'");"#;
243        let formatted = r#"INSERT INTO `table`
244("id","content"      )
245VALUES
246(1   ,"\'e\nxample\'");
247"#;
248        assert_eq!(
249            format_insert_queries(query_with_backslash_with_double_quote).unwrap(),
250            formatted
251        );
252    }
253
254    #[test]
255    fn not_add_backslashes_to_query_enclosed_with_backticks() {
256        let query_without_backslash_with_backticks =
257            r#"INSERT INTO `table` (`id`, `content`) VALUES (1, `'"example"'`);"#;
258        let formatted = r#"INSERT INTO `table`
259(`id`,`content`    )
260VALUES
261(1   ,`'"example"'`);
262"#;
263        assert_eq!(
264            format_insert_queries(query_without_backslash_with_backticks).unwrap(),
265            formatted
266        );
267    }
268
269    #[test]
270    fn not_add_backslashes_to_query_enclosed_with_single_quotes() {
271        let query_without_backslash_with_single_quotes =
272            r#"INSERT INTO `table` ('id', 'content') VALUES (1, '"example"');"#;
273        let formatted = r#"INSERT INTO `table`
274('id','content'  )
275VALUES
276(1   ,'"example"');
277"#;
278        assert_eq!(
279            format_insert_queries(query_without_backslash_with_single_quotes).unwrap(),
280            formatted
281        );
282    }
283
284    #[test]
285    fn not_add_backslashes_to_query_enclosed_with_double_quotes() {
286        let query_without_backslash_with_double_quotes =
287            r#"INSERT INTO `table` ("id", "content") VALUES (1, "'example'");"#;
288        let formatted = r#"INSERT INTO `table`
289("id","content"  )
290VALUES
291(1   ,"'example'");
292"#;
293        assert_eq!(
294            format_insert_queries(query_without_backslash_with_double_quotes).unwrap(),
295            formatted
296        );
297    }
298
299    #[test]
300    fn work_with_function() {
301        let query_with_function = "INSERT INTO `table` (`id`, `created_at`) VALUES (1, now());";
302        let formatted = "INSERT INTO `table`\n(`id`,`created_at`)\nVALUES\n(1   ,now()       );\n";
303        assert_eq!(
304            format_insert_queries(query_with_function).unwrap(),
305            formatted
306        );
307    }
308}