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
79fn 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
134fn 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#[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}