Skip to content

Commit 20ab8e1

Browse files
committed
자동 포멧팅 기능 개선
1 parent d6ed60f commit 20ab8e1

14 files changed

Lines changed: 1834 additions & 31 deletions

src/db/query/query_tests.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16068,6 +16068,34 @@ fn test_parse_tool_command_classifies_unhandled_sqlplus_set_options_as_unsupport
1606816068
}
1606916069
}
1607016070

16071+
#[test]
16072+
fn test_parse_tool_command_accepts_sqlterminator_compatibility_directive() {
16073+
for set_command in ["SET SQLTERMINATOR OFF", "SET SQLTERMINATOR ON;"] {
16074+
let parsed = QueryExecutor::parse_tool_command(set_command);
16075+
assert!(
16076+
matches!(
16077+
&parsed,
16078+
Some(ToolCommand::SqlPlusReportLayout { raw })
16079+
if raw == set_command.trim_end_matches(';')
16080+
),
16081+
"{set_command} should be accepted as a validated compatibility directive: {parsed:?}"
16082+
);
16083+
}
16084+
16085+
let parsed = QueryExecutor::parse_tool_command("SET SQLTERMINATOR MAYBE");
16086+
assert!(
16087+
matches!(
16088+
&parsed,
16089+
Some(ToolCommand::Unsupported {
16090+
message,
16091+
is_error: true,
16092+
..
16093+
}) if message.contains("ON or OFF")
16094+
),
16095+
"invalid SQLTERMINATOR mode must remain an error: {parsed:?}"
16096+
);
16097+
}
16098+
1607116099
#[test]
1607216100
fn test_split_script_items_oracle_with_function_recovers_to_sqlplus_set_options() {
1607316101
for set_command in [

src/db/query/script.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10185,6 +10185,20 @@ impl QueryExecutor {
1018510185
return Some(Self::parse_sqlblanklines_command(trimmed));
1018610186
}
1018710187

10188+
if upper.starts_with("SET SQLTERMINATOR") {
10189+
// The script splitter already accepts both SQL terminators (`;` and a
10190+
// line containing `/`) without a mutable SQL*Plus mode. Treat this as
10191+
// a validated compatibility directive so SQL*Plus fixtures can run
10192+
// unchanged instead of failing before their first SQL statement.
10193+
return Some(Self::parse_set_on_off_command(
10194+
trimmed,
10195+
"SET SQLTERMINATOR",
10196+
|_| ToolCommand::SqlPlusReportLayout {
10197+
raw: trimmed.to_string(),
10198+
},
10199+
));
10200+
}
10201+
1018810202
if upper.starts_with("SET TAB") {
1018910203
return Some(Self::parse_tab_command(trimmed));
1019010204
}

src/ui/sql_editor/format_sweep_tests.rs

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -837,39 +837,55 @@ fn format_sweep_audit_ddl_query_body_depth(
837837
let line_starts = line_start_offsets(formatted);
838838
let document = FormatSweepDocument::new(formatted, db_type);
839839
let mut words_by_line = vec![Vec::<&str>::new(); lines.len()];
840-
let mut first_word_paren_depth_by_line = vec![None; lines.len()];
840+
let mut first_word_delimiter_depth_by_line = vec![None; lines.len()];
841841
let mut first_word_statement_index_by_line = vec![None; lines.len()];
842-
let mut trailing_comma_paren_depth_by_line = vec![None; lines.len()];
842+
let mut trailing_comma_delimiter_depth_by_line = vec![None; lines.len()];
843843
let mut trailing_semicolon_paren_depth_by_line = vec![None; lines.len()];
844844
let mut line_idx = 0usize;
845845
let mut paren_depth = 0usize;
846+
let mut brace_depth = 0usize;
847+
let mut bracket_depth = 0usize;
846848

847849
for token in &document.tokens {
848850
while line_idx + 1 < line_starts.len() && line_starts[line_idx + 1] <= token.start {
849851
line_idx = line_idx.saturating_add(1);
850852
}
851-
if matches!(&token.token, SqlToken::Symbol(symbol) if symbol == ")") {
852-
paren_depth = paren_depth.saturating_sub(1);
853+
if let SqlToken::Symbol(symbol) = &token.token {
854+
match symbol.as_str() {
855+
")" => paren_depth = paren_depth.saturating_sub(1),
856+
"}" => brace_depth = brace_depth.saturating_sub(1),
857+
"]" => bracket_depth = bracket_depth.saturating_sub(1),
858+
_ => {}
859+
}
853860
}
854861
if let SqlToken::Word(word) = &token.token {
855862
if let Some(words) = words_by_line.get_mut(line_idx) {
856863
if words.is_empty() {
857-
first_word_paren_depth_by_line[line_idx] = Some(paren_depth);
864+
first_word_delimiter_depth_by_line[line_idx] =
865+
Some((paren_depth, brace_depth, bracket_depth));
858866
first_word_statement_index_by_line[line_idx] = Some(token.statement_index);
859867
}
860868
words.push(word);
861869
}
862870
}
863871
if !matches!(token.token, SqlToken::Comment(_)) {
864-
trailing_comma_paren_depth_by_line[line_idx] =
865-
matches!(&token.token, SqlToken::Symbol(symbol) if symbol == ",")
866-
.then_some(paren_depth);
872+
trailing_comma_delimiter_depth_by_line[line_idx] =
873+
matches!(&token.token, SqlToken::Symbol(symbol) if symbol == ",").then_some((
874+
paren_depth,
875+
brace_depth,
876+
bracket_depth,
877+
));
867878
trailing_semicolon_paren_depth_by_line[line_idx] =
868879
matches!(&token.token, SqlToken::Symbol(symbol) if symbol == ";")
869880
.then_some(paren_depth);
870881
}
871-
if matches!(&token.token, SqlToken::Symbol(symbol) if symbol == "(") {
872-
paren_depth = paren_depth.saturating_add(1);
882+
if let SqlToken::Symbol(symbol) = &token.token {
883+
match symbol.as_str() {
884+
"(" => paren_depth = paren_depth.saturating_add(1),
885+
"{" => brace_depth = brace_depth.saturating_add(1),
886+
"[" => bracket_depth = bracket_depth.saturating_add(1),
887+
_ => {}
888+
}
873889
}
874890
}
875891

@@ -962,7 +978,9 @@ fn format_sweep_audit_ddl_query_body_depth(
962978
));
963979
}
964980

965-
let query_paren_depth = first_word_paren_depth_by_line[body_idx].unwrap_or(0);
981+
let query_delimiter_depth =
982+
first_word_delimiter_depth_by_line[body_idx].unwrap_or((0, 0, 0));
983+
let query_paren_depth = query_delimiter_depth.0;
966984
let statement_span_end_idx = first_word_statement_index_by_line[body_idx]
967985
.and_then(|statement_index| document.statements.get(statement_index))
968986
.map(|statement| {
@@ -977,7 +995,7 @@ fn format_sweep_audit_ddl_query_body_depth(
977995
.unwrap_or(statement_span_end_idx);
978996

979997
for clause_idx in body_idx.saturating_add(1)..=statement_end_idx {
980-
if first_word_paren_depth_by_line[clause_idx] != Some(query_paren_depth)
998+
if first_word_delimiter_depth_by_line[clause_idx] != Some(query_delimiter_depth)
981999
|| !is_direct_query_clause(&words_by_line[clause_idx])
9821000
{
9831001
continue;
@@ -1001,17 +1019,17 @@ fn format_sweep_audit_ddl_query_body_depth(
10011019
}
10021020
}
10031021

1004-
for (comma_idx, trailing_comma_paren_depth) in trailing_comma_paren_depth_by_line
1022+
for (comma_idx, trailing_comma_delimiter_depth) in trailing_comma_delimiter_depth_by_line
10051023
.iter()
10061024
.enumerate()
10071025
.take(statement_end_idx)
10081026
.skip(body_idx)
10091027
{
1010-
if *trailing_comma_paren_depth != Some(query_paren_depth) {
1028+
if *trailing_comma_delimiter_depth != Some(query_delimiter_depth) {
10111029
continue;
10121030
}
10131031
let Some(sibling_idx) = (comma_idx.saturating_add(1)..=statement_end_idx).find(|idx| {
1014-
first_word_paren_depth_by_line[*idx] == Some(query_paren_depth)
1032+
first_word_delimiter_depth_by_line[*idx] == Some(query_delimiter_depth)
10151033
&& !words_by_line[*idx].is_empty()
10161034
}) else {
10171035
continue;
@@ -2450,6 +2468,22 @@ fn formatting_sweep_ddl_query_body_depth_audit_flags_missing_owner_depth() {
24502468
}
24512469
}
24522470

2471+
#[test]
2472+
fn formatting_sweep_ddl_query_body_depth_ignores_json_duality_delimiters() {
2473+
let formatted = r#"CREATE JSON RELATIONAL DUALITY VIEW dv_demo AS
2474+
SELECT JSON { '_id' :parent_owner.id, 'children' : [
2475+
SELECT JSON { 'childId' :child_owner.id, 'amount' :child_owner.amount }
2476+
FROM child_table child_owner WITH INSERT UPDATE DELETE
2477+
WHERE child_owner.parent_id = parent_owner.id ] }
2478+
FROM parent_table parent_owner WITH UPDATE INSERT DELETE;"#;
2479+
2480+
let (_, issues) = format_sweep_audit_ddl_query_body_depth(formatted, DatabaseType::Oracle);
2481+
assert!(
2482+
issues.is_empty(),
2483+
"JSON object/array commas are not DDL query-list siblings: {issues:#?}"
2484+
);
2485+
}
2486+
24532487
#[test]
24542488
fn formatting_sweep_mysql_fixed_phrases_ignore_source_line_breaks() {
24552489
let source = r#"DELIMITER $$

src/ui/sql_editor/formatter.rs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20459,8 +20459,31 @@ impl SqlEditorWidget {
2045920459
needs_space = true;
2046020460
}
2046120461
"." => {
20462-
trim_trailing_space(&mut out);
20462+
let mysql_leading_default_schema_qualifier = mysql_compatible
20463+
&& matches!(
20464+
prev_word_upper,
20465+
Some(
20466+
"FROM"
20467+
| "INTO"
20468+
| "JOIN"
20469+
| "TABLE"
20470+
| "TABLES"
20471+
| "UPDATE"
20472+
| "USING"
20473+
)
20474+
);
20475+
if !mysql_leading_default_schema_qualifier {
20476+
trim_trailing_space(&mut out);
20477+
}
2046320478
ensure_indent(&mut out, &mut at_line_start, line_indent);
20479+
if mysql_leading_default_schema_qualifier
20480+
&& !out
20481+
.as_bytes()
20482+
.last()
20483+
.is_some_and(|byte| byte.is_ascii_whitespace())
20484+
{
20485+
out.push(' ');
20486+
}
2046420487
out.push('.');
2046520488
needs_space = false;
2046620489
}
@@ -32220,6 +32243,73 @@ FROM demo t WITH UPDATE INSERT DELETE;"#;
3222032243
);
3222132244
}
3222232245

32246+
#[test]
32247+
fn format_for_auto_formatting_keeps_nested_json_duality_view_query_owned() {
32248+
let source = r#"CREATE JSON RELATIONAL DUALITY VIEW dv_demo AS
32249+
SELECT JSON {
32250+
'_id' : parent_owner.id,
32251+
'children' : [
32252+
SELECT JSON {
32253+
'childId' : child_owner.id,
32254+
'amount' : child_owner.amount
32255+
}
32256+
FROM child_table child_owner WITH INSERT UPDATE DELETE
32257+
WHERE child_owner.parent_id = parent_owner.id
32258+
]
32259+
}
32260+
FROM parent_table parent_owner WITH UPDATE INSERT DELETE;"#;
32261+
32262+
let (formatted, audit) =
32263+
SqlEditorWidget::format_for_auto_formatting_with_frame_alignment_audit(
32264+
source,
32265+
Some(crate::db::connection::DatabaseType::Oracle),
32266+
);
32267+
32268+
assert!(audit.issues.is_empty(), "{:#?}\n{formatted}", audit.issues);
32269+
assert_eq!(
32270+
SqlEditorWidget::format_for_auto_formatting_with_frame_alignment_audit(
32271+
&formatted,
32272+
Some(crate::db::connection::DatabaseType::Oracle),
32273+
)
32274+
.0,
32275+
formatted,
32276+
"nested duality-view formatting must be idempotent"
32277+
);
32278+
}
32279+
32280+
#[test]
32281+
fn format_for_auto_formatting_preserves_mariadb_leading_dot_table_qualifier() {
32282+
let source = "INSERT INTO . demo_table (id) VALUES (1);\n\
32283+
SELECT * FROM . demo_table;\n\
32284+
UPDATE . demo_table SET id = 2;\n\
32285+
DELETE FROM . demo_table WHERE id = 2;";
32286+
let formatted = SqlEditorWidget::format_for_auto_formatting_with_db_type(
32287+
source,
32288+
false,
32289+
Some(crate::db::connection::DatabaseType::MariaDB),
32290+
);
32291+
32292+
for target in [
32293+
"INSERT INTO .demo_table",
32294+
"FROM .demo_table",
32295+
"UPDATE .demo_table",
32296+
] {
32297+
assert!(
32298+
formatted.contains(target),
32299+
"MariaDB leading-dot qualifier lost its keyword separator `{target}`:\n{formatted}"
32300+
);
32301+
}
32302+
assert_eq!(
32303+
SqlEditorWidget::format_for_auto_formatting_with_db_type(
32304+
&formatted,
32305+
false,
32306+
Some(crate::db::connection::DatabaseType::MariaDB),
32307+
),
32308+
formatted,
32309+
"MariaDB leading-dot formatting must be idempotent"
32310+
);
32311+
}
32312+
3222332313
#[test]
3222432314
fn format_for_auto_formatting_keeps_parenthesized_sql_macro_body_attached() {
3222532315
let source = r#"CREATE OR REPLACE FUNCTION topn RETURN VARCHAR2 SQL_MACRO(TABLE)

src/ui/sql_editor/intellisense/completion.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65154,6 +65154,35 @@ impl SqlEditorWidget {
6515465154
last_word_index_before("CURSOR").is_some_and(|cursor_idx| {
6515565155
last_word_index_before("SELECT").is_none_or(|select_idx| cursor_idx > select_idx)
6515665156
});
65157+
if mysql_compatible
65158+
&& previous_is_symbol("{")
65159+
&& next_idx.is_some_and(
65160+
|idx| matches!(tokens.get(idx), Some(SqlToken::String(_))),
65161+
)
65162+
&& ["D", "T", "TS"]
65163+
.iter()
65164+
.any(|keyword| matches_prefix(keyword))
65165+
{
65166+
return Some(&["D", "T", "TS"]);
65167+
}
65168+
if matches_prefix("ON")
65169+
&& previous_word.is_some_and(|word| {
65170+
matches!(
65171+
word.to_ascii_uppercase().as_str(),
65172+
"ERROR" | "FALSE" | "NULL" | "TRUE"
65173+
)
65174+
})
65175+
&& next_word.is_some_and(|word| {
65176+
matches!(word.to_ascii_uppercase().as_str(), "EMPTY" | "ERROR")
65177+
})
65178+
&& Self::cursor_is_inside_table_function_columns_clause_matching(
65179+
tokens,
65180+
structural_end,
65181+
|table_fn| table_fn.eq_ignore_ascii_case("JSON_TABLE"),
65182+
)
65183+
{
65184+
return Some(&["ON"]);
65185+
}
6515765186
if matches_prefix("JOIN")
6515865187
&& matches!(
6515965188
phase,
@@ -67394,11 +67423,35 @@ impl SqlEditorWidget {
6739467423
return Some(&["DUALITY"]);
6739567424
}
6739667425
if matches_prefix("WITH")
67397-
&& has("DUALITY")
67426+
&& (has("DUALITY")
67427+
|| (matches!(phase, intellisense_context::SqlPhase::FromClause)
67428+
&& has("SELECT")
67429+
&& has("JSON")))
6739867430
&& next_word.is_some_and(|word| word.eq_ignore_ascii_case("UPDATE"))
6739967431
{
6740067432
return Some(&["WITH"]);
6740167433
}
67434+
if matches_prefix("WITH")
67435+
&& matches!(phase, intellisense_context::SqlPhase::FromClause)
67436+
&& has("SELECT")
67437+
&& has("JSON")
67438+
&& next_word.is_some_and(|word| {
67439+
matches!(
67440+
word.to_ascii_uppercase().as_str(),
67441+
"INSERT" | "UPDATE" | "DELETE"
67442+
)
67443+
})
67444+
{
67445+
return Some(&["WITH"]);
67446+
}
67447+
if matches_prefix("FROM")
67448+
&& previous_is_symbol("}")
67449+
&& has("SELECT")
67450+
&& has("JSON")
67451+
&& next_is_identifier_or_string
67452+
{
67453+
return Some(&["FROM"]);
67454+
}
6740267455
if matches_prefix("UPDATE") && has("DUALITY") {
6740367456
return Some(&["UPDATE"]);
6740467457
}

0 commit comments

Comments
 (0)