diff --git a/Cargo.lock b/Cargo.lock index 5705acccc6..1a16714bba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1291,6 +1291,7 @@ dependencies = [ "sha2", "shell-escape", "spki", + "sqlformat", "strsim", "tar", "tempfile", @@ -6794,6 +6795,17 @@ dependencies = [ "der", ] +[[package]] +name = "sqlformat" +version = "0.3.1" +source = "git+https://github.com/shssoichiro/sqlformat-rs.git?rev=827d639#827d639bef94d8e5a5a0e29b41185c8d572f24e6" +dependencies = [ + "nom 7.1.3", + "once_cell", + "regex", + "unicode_categories", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index dd1d44a8ef..16f39e9d48 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -151,6 +151,8 @@ serde_repr.workspace = true sha2.workspace = true shell-escape = "=0.1.5" spki = { version = "0.7", features = ["pem"] } +# NOTE(bartlomieju): for now using github URL, because 0.3.2 with important fixes hasn't been released yet. +sqlformat = { git = "https://github.com/shssoichiro/sqlformat-rs.git", rev = "827d639" } strsim = "0.11.1" tar.workspace = true tempfile.workspace = true diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 39db12b5f1..262bc04682 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -210,6 +210,7 @@ pub struct FmtFlags { pub no_semicolons: Option, pub watch: Option, pub unstable_component: bool, + pub unstable_sql: bool, } impl FmtFlags { @@ -2291,7 +2292,7 @@ Ignore formatting a file by adding an ignore comment at the top of the file: .value_parser([ "ts", "tsx", "js", "jsx", "md", "json", "jsonc", "css", "scss", "sass", "less", "html", "svelte", "vue", "astro", "yml", "yaml", - "ipynb", + "ipynb", "sql" ]) .help_heading(FMT_HEADING).requires("files"), ) @@ -2410,6 +2411,14 @@ Ignore formatting a file by adding an ignore comment at the top of the file: .help_heading(FMT_HEADING) .hide(true), ) + .arg( + Arg::new("unstable-sql") + .long("unstable-sql") + .help("Enable formatting SQL files.") + .value_parser(FalseyValueParser::new()) + .action(ArgAction::SetTrue) + .help_heading(FMT_HEADING), + ) }) } @@ -4634,6 +4643,7 @@ fn fmt_parse( let prose_wrap = matches.remove_one::("prose-wrap"); let no_semicolons = matches.remove_one::("no-semicolons"); let unstable_component = matches.get_flag("unstable-component"); + let unstable_sql = matches.get_flag("unstable-sql"); flags.subcommand = DenoSubcommand::Fmt(FmtFlags { check: matches.get_flag("check"), @@ -4646,6 +4656,7 @@ fn fmt_parse( no_semicolons, watch: watch_arg_parse(matches)?, unstable_component, + unstable_sql, }); Ok(()) } @@ -6565,6 +6576,7 @@ mod tests { prose_wrap: None, no_semicolons: None, unstable_component: false, + unstable_sql: false, watch: Default::default(), }), ..Flags::default() @@ -6588,6 +6600,7 @@ mod tests { prose_wrap: None, no_semicolons: None, unstable_component: false, + unstable_sql: false, watch: Default::default(), }), ..Flags::default() @@ -6611,6 +6624,7 @@ mod tests { prose_wrap: None, no_semicolons: None, unstable_component: false, + unstable_sql: false, watch: Default::default(), }), ..Flags::default() @@ -6634,6 +6648,7 @@ mod tests { prose_wrap: None, no_semicolons: None, unstable_component: false, + unstable_sql: false, watch: Some(Default::default()), }), ..Flags::default() @@ -6648,7 +6663,8 @@ mod tests { "--unstable-css", "--unstable-html", "--unstable-component", - "--unstable-yaml" + "--unstable-yaml", + "--unstable-sql" ]); assert_eq!( r.unwrap(), @@ -6666,6 +6682,7 @@ mod tests { prose_wrap: None, no_semicolons: None, unstable_component: true, + unstable_sql: true, watch: Some(WatchFlags { hmr: false, no_clear_screen: true, @@ -6700,6 +6717,7 @@ mod tests { prose_wrap: None, no_semicolons: None, unstable_component: false, + unstable_sql: false, watch: Some(Default::default()), }), ..Flags::default() @@ -6723,6 +6741,7 @@ mod tests { prose_wrap: None, no_semicolons: None, unstable_component: false, + unstable_sql: false, watch: Default::default(), }), config_flag: ConfigFlag::Path("deno.jsonc".to_string()), @@ -6754,6 +6773,7 @@ mod tests { prose_wrap: None, no_semicolons: None, unstable_component: false, + unstable_sql: false, watch: Some(Default::default()), }), config_flag: ConfigFlag::Path("deno.jsonc".to_string()), @@ -6790,6 +6810,7 @@ mod tests { prose_wrap: Some("never".to_string()), no_semicolons: Some(true), unstable_component: false, + unstable_sql: false, watch: Default::default(), }), ..Flags::default() @@ -6820,6 +6841,7 @@ mod tests { prose_wrap: None, no_semicolons: Some(false), unstable_component: false, + unstable_sql: false, watch: Default::default(), }), ..Flags::default() @@ -6845,6 +6867,7 @@ mod tests { prose_wrap: None, no_semicolons: None, unstable_component: false, + unstable_sql: false, watch: Default::default(), }), ext: Some("html".to_string()), diff --git a/cli/args/mod.rs b/cli/args/mod.rs index ec75d7a100..61e1443a75 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -289,6 +289,7 @@ impl BenchOptions { #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] pub struct UnstableFmtOptions { pub component: bool, + pub sql: bool, } #[derive(Clone, Debug)] @@ -322,6 +323,7 @@ impl FmtOptions { options: resolve_fmt_options(fmt_flags, fmt_config.options), unstable: UnstableFmtOptions { component: unstable.component || fmt_flags.unstable_component, + sql: unstable.sql || fmt_flags.unstable_sql, }, files: fmt_config.files, } @@ -1319,6 +1321,7 @@ impl CliOptions { let workspace = self.workspace(); UnstableFmtOptions { component: workspace.has_unstable("fmt-component"), + sql: workspace.has_unstable("fmt-sql"), } } @@ -1667,6 +1670,7 @@ impl CliOptions { "byonm", "bare-node-builtins", "fmt-component", + "fmt-sql", ]) .collect(); diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 2ce26c1f2e..cbe194e14e 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -1405,6 +1405,9 @@ impl Inner { component: config_data .map(|d| d.unstable.contains("fmt-component")) .unwrap_or(false), + sql: config_data + .map(|d| d.unstable.contains("fmt-sql")) + .unwrap_or(false), }; let document = document.clone(); move || { diff --git a/cli/schemas/config-file.v1.json b/cli/schemas/config-file.v1.json index 3ba803ef8c..ccd773efbf 100644 --- a/cli/schemas/config-file.v1.json +++ b/cli/schemas/config-file.v1.json @@ -557,6 +557,7 @@ "ffi", "fs", "fmt-component", + "fmt-sql", "http", "kv", "net", diff --git a/cli/tools/fmt.rs b/cli/tools/fmt.rs index d40abd5f50..9c2c709129 100644 --- a/cli/tools/fmt.rs +++ b/cli/tools/fmt.rs @@ -272,6 +272,7 @@ fn format_markdown( | "njk" | "yml" | "yaml" + | "sql" ) { // It's important to tell dprint proper file extension, otherwise // it might parse the file twice. @@ -301,6 +302,13 @@ fn format_markdown( } } "yml" | "yaml" => format_yaml(text, fmt_options), + "sql" => { + if unstable_options.sql { + format_sql(text, fmt_options) + } else { + Ok(None) + } + } _ => { let mut codeblock_config = get_resolved_typescript_config(fmt_options); @@ -503,7 +511,48 @@ pub fn format_html( }) } -/// Formats a single TS, TSX, JS, JSX, JSONC, JSON, MD, or IPYNB file. +pub fn format_sql( + file_text: &str, + fmt_options: &FmtOptionsConfig, +) -> Result, AnyError> { + let ignore_file = file_text + .lines() + .take_while(|line| line.starts_with("--")) + .any(|line| { + line + .strip_prefix("--") + .unwrap() + .trim() + .starts_with("deno-fmt-ignore-file") + }); + + if ignore_file { + return Ok(None); + } + + let mut formatted_str = sqlformat::format( + file_text, + &sqlformat::QueryParams::None, + &sqlformat::FormatOptions { + ignore_case_convert: None, + indent: if fmt_options.use_tabs.unwrap_or_default() { + sqlformat::Indent::Tabs + } else { + sqlformat::Indent::Spaces(fmt_options.indent_width.unwrap_or(2)) + }, + // leave one blank line between queries. + lines_between_queries: 2, + uppercase: Some(true), + }, + ); + + // Add single new line to the end of file. + formatted_str.push('\n'); + + Ok(Some(formatted_str)) +} + +/// Formats a single TS, TSX, JS, JSX, JSONC, JSON, MD, IPYNB or SQL file. pub fn format_file( file_path: &Path, file_text: &str, @@ -538,6 +587,13 @@ pub fn format_file( format_file(file_path, &file_text, fmt_options, unstable_options, None) }, ), + "sql" => { + if unstable_options.sql { + format_sql(file_text, fmt_options) + } else { + Ok(None) + } + } _ => { let config = get_resolved_typescript_config(fmt_options); dprint_plugin_typescript::format_text( @@ -1209,6 +1265,7 @@ fn is_supported_ext_fmt(path: &Path) -> bool { | "yml" | "yaml" | "ipynb" + | "sql" ) }) } @@ -1269,6 +1326,11 @@ mod test { assert!(is_supported_ext_fmt(Path::new("foo.yaml"))); assert!(is_supported_ext_fmt(Path::new("foo.YaML"))); assert!(is_supported_ext_fmt(Path::new("foo.ipynb"))); + assert!(is_supported_ext_fmt(Path::new("foo.sql"))); + assert!(is_supported_ext_fmt(Path::new("foo.Sql"))); + assert!(is_supported_ext_fmt(Path::new("foo.sQl"))); + assert!(is_supported_ext_fmt(Path::new("foo.sqL"))); + assert!(is_supported_ext_fmt(Path::new("foo.SQL"))); } #[test] diff --git a/tests/integration/fmt_tests.rs b/tests/integration/fmt_tests.rs index b890b3b72a..ccf54a4d0f 100644 --- a/tests/integration/fmt_tests.rs +++ b/tests/integration/fmt_tests.rs @@ -61,6 +61,12 @@ fn fmt_test() { let badly_formatted_yaml = t.path().join("badly_formatted.yaml"); badly_formatted_original_yaml.copy(&badly_formatted_yaml); + let fixed_sql = testdata_fmt_dir.join("badly_formatted_fixed.sql"); + let badly_formatted_original_sql = + testdata_fmt_dir.join("badly_formatted.sql"); + let badly_formatted_sql = t.path().join("badly_formatted.sql"); + badly_formatted_original_sql.copy(&badly_formatted_sql); + // First, check formatting by ignoring the badly formatted file. let output = context .new_command() @@ -71,11 +77,12 @@ fn fmt_test() { "--unstable-html".to_string(), "--unstable-component".to_string(), "--unstable-yaml".to_string(), + "--unstable-sql".to_string(), format!( - "--ignore={badly_formatted_js},{badly_formatted_md},{badly_formatted_json},{badly_formatted_css},{badly_formatted_html},{badly_formatted_component},{badly_formatted_yaml},{badly_formatted_ipynb}", + "--ignore={badly_formatted_js},{badly_formatted_md},{badly_formatted_json},{badly_formatted_css},{badly_formatted_html},{badly_formatted_component},{badly_formatted_yaml},{badly_formatted_ipynb},{badly_formatted_sql}", ), format!( - "--check {badly_formatted_js} {badly_formatted_md} {badly_formatted_json} {badly_formatted_css} {badly_formatted_html} {badly_formatted_component} {badly_formatted_yaml} {badly_formatted_ipynb}", + "--check {badly_formatted_js} {badly_formatted_md} {badly_formatted_json} {badly_formatted_css} {badly_formatted_html} {badly_formatted_component} {badly_formatted_yaml} {badly_formatted_ipynb} {badly_formatted_sql}", ), ]) .run(); @@ -95,6 +102,7 @@ fn fmt_test() { "--unstable-html".to_string(), "--unstable-component".to_string(), "--unstable-yaml".to_string(), + "--unstable-sql".to_string(), badly_formatted_js.to_string(), badly_formatted_md.to_string(), badly_formatted_json.to_string(), @@ -103,6 +111,7 @@ fn fmt_test() { badly_formatted_component.to_string(), badly_formatted_yaml.to_string(), badly_formatted_ipynb.to_string(), + badly_formatted_sql.to_string(), ]) .run(); @@ -119,6 +128,7 @@ fn fmt_test() { "--unstable-html".to_string(), "--unstable-component".to_string(), "--unstable-yaml".to_string(), + "--unstable-sql".to_string(), badly_formatted_js.to_string(), badly_formatted_md.to_string(), badly_formatted_json.to_string(), @@ -127,6 +137,7 @@ fn fmt_test() { badly_formatted_component.to_string(), badly_formatted_yaml.to_string(), badly_formatted_ipynb.to_string(), + badly_formatted_sql.to_string(), ]) .run(); @@ -141,6 +152,7 @@ fn fmt_test() { let expected_component = fixed_component.read_to_string(); let expected_yaml = fixed_yaml.read_to_string(); let expected_ipynb = fixed_ipynb.read_to_string(); + let expected_sql = fixed_sql.read_to_string(); let actual_js = badly_formatted_js.read_to_string(); let actual_md = badly_formatted_md.read_to_string(); let actual_json = badly_formatted_json.read_to_string(); @@ -149,6 +161,7 @@ fn fmt_test() { let actual_component = badly_formatted_component.read_to_string(); let actual_yaml = badly_formatted_yaml.read_to_string(); let actual_ipynb = badly_formatted_ipynb.read_to_string(); + let actual_sql = badly_formatted_sql.read_to_string(); assert_eq!(expected_js, actual_js); assert_eq!(expected_md, actual_md); assert_eq!(expected_json, actual_json); @@ -157,6 +170,7 @@ fn fmt_test() { assert_eq!(expected_component, actual_component); assert_eq!(expected_yaml, actual_yaml); assert_eq!(expected_ipynb, actual_ipynb); + assert_eq!(expected_sql, actual_sql); } #[test] diff --git a/tests/specs/fmt/sql/__test__.jsonc b/tests/specs/fmt/sql/__test__.jsonc new file mode 100644 index 0000000000..a335e79c24 --- /dev/null +++ b/tests/specs/fmt/sql/__test__.jsonc @@ -0,0 +1,25 @@ +{ + "tempDir": true, + "tests": { + "nothing": { + "args": "fmt", + "output": "Checked 7 files\n" + }, + "flag": { + "args": "fmt --unstable-sql", + "output": "[UNORDERED_START]\n[WILDLINE]badly_formatted.sql\n[WILDLINE]well_formatted.sql\n[WILDLINE]wrong_file_ignore.sql\n[UNORDERED_END]\nChecked 7 files\n" + }, + "config_file": { + "steps": [{ + "args": [ + "eval", + "Deno.writeTextFile('deno.json', '{\\n \"unstable\": [\"fmt-sql\"]\\n}\\n')" + ], + "output": "[WILDCARD]" + }, { + "args": "fmt", + "output": "[UNORDERED_START]\n[WILDLINE]badly_formatted.sql\n[WILDLINE]well_formatted.sql\n[WILDLINE]wrong_file_ignore.sql\n[UNORDERED_END]\nChecked 8 files\n" + }] + } + } +} diff --git a/tests/specs/fmt/sql/badly_formatted.sql b/tests/specs/fmt/sql/badly_formatted.sql new file mode 100644 index 0000000000..619fc7afdd --- /dev/null +++ b/tests/specs/fmt/sql/badly_formatted.sql @@ -0,0 +1 @@ +select *; \ No newline at end of file diff --git a/tests/specs/fmt/sql/ignore_file.sql b/tests/specs/fmt/sql/ignore_file.sql new file mode 100644 index 0000000000..62418c611b --- /dev/null +++ b/tests/specs/fmt/sql/ignore_file.sql @@ -0,0 +1,3 @@ +-- deno-fmt-ignore-file + +foo%! diff --git a/tests/specs/fmt/sql/ignore_file2.sql b/tests/specs/fmt/sql/ignore_file2.sql new file mode 100644 index 0000000000..fe9969f1d3 --- /dev/null +++ b/tests/specs/fmt/sql/ignore_file2.sql @@ -0,0 +1,3 @@ +--deno-fmt-ignore-file + +foo%! diff --git a/tests/specs/fmt/sql/ignore_file3.sql b/tests/specs/fmt/sql/ignore_file3.sql new file mode 100644 index 0000000000..c87da3e591 --- /dev/null +++ b/tests/specs/fmt/sql/ignore_file3.sql @@ -0,0 +1,6 @@ +-- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +-- incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, +-- quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +-- deno-fmt-ignore-file + + foo%! diff --git a/tests/specs/fmt/sql/ignore_file4.sql b/tests/specs/fmt/sql/ignore_file4.sql new file mode 100644 index 0000000000..2de65712a5 --- /dev/null +++ b/tests/specs/fmt/sql/ignore_file4.sql @@ -0,0 +1,3 @@ +-- deno-fmt-ignore-file Foo bar biz + + foo%! diff --git a/tests/specs/fmt/sql/well_formatted.sql b/tests/specs/fmt/sql/well_formatted.sql new file mode 100644 index 0000000000..92ce980185 --- /dev/null +++ b/tests/specs/fmt/sql/well_formatted.sql @@ -0,0 +1,4 @@ +SELECT + * +FROM + foo; diff --git a/tests/specs/fmt/sql/wrong_file_ignore.sql b/tests/specs/fmt/sql/wrong_file_ignore.sql new file mode 100644 index 0000000000..c124855dc2 --- /dev/null +++ b/tests/specs/fmt/sql/wrong_file_ignore.sql @@ -0,0 +1,6 @@ +-- File ignore directive only works if it's in the first cluster +-- of comment, ie. there are no empty lines after the first n-leading lines. + +-- deno-fmt-ignore-file + + foo \ No newline at end of file diff --git a/tests/testdata/fmt/badly_formatted.md b/tests/testdata/fmt/badly_formatted.md index 642918ceae..be90ff845f 100644 --- a/tests/testdata/fmt/badly_formatted.md +++ b/tests/testdata/fmt/badly_formatted.md @@ -63,3 +63,15 @@ function foo(): number { let a:number ``` + + +```sql + seLect * , biz, buz +from baz; +``` + +```sql +-- deno-fmt-ignore-file + seLect * , biz, buz +from baz; +``` diff --git a/tests/testdata/fmt/badly_formatted.sql b/tests/testdata/fmt/badly_formatted.sql new file mode 100644 index 0000000000..8b4cb978f0 --- /dev/null +++ b/tests/testdata/fmt/badly_formatted.sql @@ -0,0 +1,21 @@ +select * from foo; +update foo set a = 'b'Where id = 'biz'; + + + create table foo(id text not null +bar text, + biz int, + buz number NOT NULL +); + +INSERT + into + user_data + (first_name, +last_name, address, phone, email) +VALUES + ('foo', 'bar', + 'biz', 1, 'bix'); + + + diff --git a/tests/testdata/fmt/badly_formatted_fixed.md b/tests/testdata/fmt/badly_formatted_fixed.md index 21176742bb..7a482e058f 100644 --- a/tests/testdata/fmt/badly_formatted_fixed.md +++ b/tests/testdata/fmt/badly_formatted_fixed.md @@ -56,3 +56,18 @@ function foo(): number { let a: number; ``` + +```sql +SELECT + *, + biz, + buz +FROM + baz; +``` + +```sql +-- deno-fmt-ignore-file + seLect * , biz, buz +from baz; +``` diff --git a/tests/testdata/fmt/badly_formatted_fixed.sql b/tests/testdata/fmt/badly_formatted_fixed.sql new file mode 100644 index 0000000000..d50c619216 --- /dev/null +++ b/tests/testdata/fmt/badly_formatted_fixed.sql @@ -0,0 +1,22 @@ +SELECT + * +FROM + foo; + +UPDATE + foo +SET + a = 'b' +WHERE + id = 'biz'; + +CREATE TABLE foo( + id text NOT NULL bar text, + biz int, + buz number NOT NULL +); + +INSERT INTO + user_data (first_name, last_name, address, phone, email) +VALUES + ('foo', 'bar', 'biz', 1, 'bix');