Skip to content

Commit d6ed60f

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

68 files changed

Lines changed: 1907 additions & 372 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/db/query/query_tests.rs

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ fn test_split_script_items_test12_if_alias_regression() {
429429
.count();
430430

431431
assert_eq!(
432-
tool_command_count, 2,
432+
tool_command_count, 5,
433433
"unexpected tool command split: {items:?}"
434434
);
435435
assert_eq!(
@@ -465,7 +465,7 @@ fn test_split_script_items_test13_keyword_alias_regression() {
465465
.count();
466466

467467
assert_eq!(
468-
tool_command_count, 2,
468+
tool_command_count, 5,
469469
"unexpected tool command split: {items:?}"
470470
);
471471
assert_eq!(
@@ -503,7 +503,7 @@ fn test_split_script_items_test14_deep_monster_view_regression() {
503503
.count();
504504

505505
assert_eq!(
506-
tool_command_count, 2,
506+
tool_command_count, 3,
507507
"unexpected tool command split: {items:?}"
508508
);
509509
assert_eq!(
@@ -657,7 +657,7 @@ fn test_split_script_items_test17_execution_unit_final_boss_regression() {
657657
.count();
658658

659659
assert_eq!(
660-
tool_command_count, 0,
660+
tool_command_count, 1,
661661
"unexpected tool command split: {items:?}"
662662
);
663663
assert_eq!(
@@ -716,7 +716,7 @@ fn test_split_script_items_test19_execution_unit_splitter_final_boss_regression(
716716
.count();
717717

718718
assert_eq!(
719-
tool_command_count, 0,
719+
tool_command_count, 5,
720720
"unexpected tool command split: {items:?}"
721721
);
722722
assert_eq!(
@@ -775,7 +775,7 @@ fn test_split_script_items_test20_execution_unit_splitter_regression() {
775775
.count();
776776

777777
assert_eq!(
778-
tool_command_count, 0,
778+
tool_command_count, 3,
779779
"unexpected tool command split: {items:?}"
780780
);
781781
assert_eq!(
@@ -834,7 +834,7 @@ fn test_split_script_items_test21_execution_unit_splitter_regression() {
834834
.count();
835835

836836
assert_eq!(
837-
tool_command_count, 0,
837+
tool_command_count, 3,
838838
"unexpected tool command split: {items:?}"
839839
);
840840
assert_eq!(
@@ -1134,7 +1134,7 @@ fn test_split_format_items_test19_execution_unit_splitter_final_boss_regression(
11341134
.filter(|item| matches!(item, FormatItem::Slash))
11351135
.count();
11361136

1137-
assert_eq!(slash_count, 11, "unexpected slash split: {items:?}");
1137+
assert_eq!(slash_count, 14, "unexpected slash split: {items:?}");
11381138

11391139
let statements: Vec<&str> = items
11401140
.iter()
@@ -1191,7 +1191,7 @@ fn test_split_format_items_test20_execution_unit_splitter_regression() {
11911191
.filter(|item| matches!(item, FormatItem::Slash))
11921192
.count();
11931193

1194-
assert_eq!(slash_count, 36, "unexpected slash split: {items:?}");
1194+
assert_eq!(slash_count, 15, "unexpected slash split: {items:?}");
11951195

11961196
let statements: Vec<&str> = items
11971197
.iter()
@@ -1242,7 +1242,7 @@ fn test_split_format_items_test21_execution_unit_splitter_regression() {
12421242
.filter(|item| matches!(item, FormatItem::Slash))
12431243
.count();
12441244

1245-
assert_eq!(slash_count, 45, "unexpected slash split: {items:?}");
1245+
assert_eq!(slash_count, 25, "unexpected slash split: {items:?}");
12461246

12471247
let statements: Vec<&str> = items
12481248
.iter()
@@ -12357,8 +12357,7 @@ fn test_mariadb_test7_select_into_statements_are_not_displayable_results() {
1235712357
}
1235812358

1235912359
#[test]
12360-
fn test_split_script_items_mariadb_comment_final_boss_keeps_transaction_directive_as_tool_command()
12361-
{
12360+
fn test_split_script_items_mariadb_comment_final_boss_keeps_transaction_statement() {
1236212361
let sql = load_mariadb_query_test_file("test8.txt");
1236312362
assert!(
1236412363
!sql.is_empty(),
@@ -12368,11 +12367,10 @@ fn test_split_script_items_mariadb_comment_final_boss_keeps_transaction_directiv
1236812367
let items = QueryExecutor::split_script_items(&sql);
1236912368

1237012369
assert!(
12371-
items.iter().any(|item| matches!(
12372-
item,
12373-
ScriptItem::ToolCommand(ToolCommand::SetAutoCommit { enabled: false })
12374-
)),
12375-
"test8 should keep @TRANSACTION as a transaction directive: {items:?}"
12370+
items.iter().any(
12371+
|item| matches!(item, ScriptItem::Statement(statement) if statement == "START TRANSACTION")
12372+
),
12373+
"test8 should keep START TRANSACTION as a SQL statement: {items:?}"
1237612374
);
1237712375
assert!(
1237812376
!items.iter().any(|item| matches!(

src/ui/sql_editor/intellisense/completion.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42198,6 +42198,7 @@ impl SqlEditorWidget {
4219842198
"SCAN",
4219942199
"SERVEROUTPUT",
4220042200
"SQLBLANKLINES",
42201+
"SQLTERMINATOR",
4220142202
"TAB",
4220242203
"TERMOUT",
4220342204
"TIMING",
@@ -49376,6 +49377,7 @@ impl SqlEditorWidget {
4937649377
{
4937749378
Some(&["ON", "OFF", "TRUE", "FALSE"])
4937849379
}
49380+
[set, option] if set == "SET" && option == "SQLTERMINATOR" => Some(&["ON", "OFF"]),
4937949381
[set, option]
4938049382
if set == "SET"
4938149383
&& !crate::sql_text::SQLPLUS_SET_OPTION_KEYWORDS
@@ -49626,10 +49628,17 @@ impl SqlEditorWidget {
4962649628
// its leading word distinguishes a query (full clause set) from a
4962749629
// `DELETE`/`UPDATE` (which has no `GROUP BY`/`HAVING`/… - only MySQL adds
4962849630
// `ORDER BY`/`LIMIT`). Anything else (MERGE, INSERT, …) gets nothing.
49629-
let statement_head = tokens.iter().find_map(|token| match token {
49630-
SqlToken::Word(word) => Some(word.to_ascii_uppercase()),
49631-
_ => None,
49632-
});
49631+
// A branch narrowed after `UNION ALL`/`UNION DISTINCT` retains the set
49632+
// quantifier as its first token. It modifies the set operator, not the
49633+
// following query, so resolve the statement head from the first word
49634+
// after that retained modifier.
49635+
let statement_head = tokens
49636+
.iter()
49637+
.filter_map(|token| match token {
49638+
SqlToken::Word(word) => Some(word.to_ascii_uppercase()),
49639+
_ => None,
49640+
})
49641+
.find(|word| !matches!(word.as_str(), "ALL" | "DISTINCT"));
4963349642
// A query-defining DDL/DML wrapper (`CREATE VIEW … AS SELECT`, CTAS,
4963449643
// `INSERT … SELECT`) precedes the embedded query whose clauses the cursor is
4963549644
// in; resolve the head to that query's `SELECT`/`WITH` so its continuation
@@ -59541,6 +59550,7 @@ impl SqlEditorWidget {
5954159550
| "HEADING"
5954259551
| "SCAN"
5954359552
| "SQLBLANKLINES"
59553+
| "SQLTERMINATOR"
5954459554
| "TAB"
5954559555
| "TIMING"
5954659556
| "TRIMOUT"

src/ui/sql_editor/intellisense/tests.rs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4789,6 +4789,75 @@ fn empty_group_by_slot_does_not_leak_downstream_openers() {
47894789
assert!(oracle.iter().any(|k| k == "ROLLUP"), "{oracle:?}");
47904790
}
47914791

4792+
#[test]
4793+
fn mysql_family_boolean_literal_join_condition_completes_join() {
4794+
use crate::db::DatabaseType::{MariaDB, MySQL};
4795+
4796+
let contains = |values: &[String], needle: &str| values.iter().any(|value| value == needle);
4797+
let final_suggestions = |sql: &str, db| audit_final_suggestions_for(sql, db).2;
4798+
4799+
for db in [MySQL, MariaDB] {
4800+
let join = final_suggestions("SELECT * FROM t1 JOIN t ON TRUE JOI|", db);
4801+
assert!(
4802+
contains(&join, "JOIN"),
4803+
"JOIN missing after a complete TRUE join condition for {db:?}: {join:?}"
4804+
);
4805+
4806+
let where_clause = final_suggestions("SELECT * FROM t1 JOIN t ON FALSE WHER|", db);
4807+
assert!(
4808+
contains(&where_clause, "WHERE"),
4809+
"WHERE missing after a complete FALSE join condition for {db:?}: {where_clause:?}"
4810+
);
4811+
4812+
let set_branch_join =
4813+
final_suggestions("SELECT 1 UNION ALL SELECT * FROM t1 JOIN t ON TRUE JOI|", db);
4814+
assert!(
4815+
contains(&set_branch_join, "JOIN"),
4816+
"JOIN missing in the second UNION ALL branch for {db:?}: {set_branch_join:?}"
4817+
);
4818+
4819+
let recursive_sql = "WITH RECURSIVE graph_walk AS (
4820+
SELECT 1 AS node_id
4821+
UNION ALL
4822+
SELECT edge.child_id
4823+
FROM graph_walk AS w
4824+
JOIN t AS parent ON parent.id = w.node_id
4825+
JOIN JSON_TABLE(
4826+
parent.payload,
4827+
'$.edges[*]' COLUMNS (child_id INT PATH '$')
4828+
) AS edge ON TRUE
4829+
JOI|
4830+
)
4831+
SELECT * FROM graph_walk";
4832+
let recursive_context = analyze_inline_cursor_sql(recursive_sql);
4833+
let recursive_join = final_suggestions(recursive_sql, db);
4834+
assert!(
4835+
contains(&recursive_join, "JOIN"),
4836+
"JOIN missing after JSON_TABLE ... ON TRUE in a recursive CTE for {db:?}: \
4837+
phase={:?}, cursor_token_len={}, current_query_end={}, current_query={:?}, \
4838+
tokens={:?}, suggestions={recursive_join:?}",
4839+
recursive_context.phase,
4840+
recursive_context.cursor_token_len,
4841+
SqlEditorWidget::cursor_token_len_in_current_query(&recursive_context),
4842+
SqlEditorWidget::current_query_tokens(&recursive_context),
4843+
recursive_context.statement_tokens,
4844+
);
4845+
4846+
for sql in [
4847+
"SELECT * FROM t1 JOIN t ON |",
4848+
"SELECT * FROM t1 JOIN t ON t.id |",
4849+
] {
4850+
let incomplete = final_suggestions(sql, db);
4851+
for leaked in ["JOIN", "WHERE", "GROUP BY", "ORDER BY", "LIMIT"] {
4852+
assert!(
4853+
!contains(&incomplete, leaked),
4854+
"{leaked} leaked after incomplete join condition `{sql}` for {db:?}: {incomplete:?}"
4855+
);
4856+
}
4857+
}
4858+
}
4859+
}
4860+
47924861
/// After the *complete* content of a query clause, the downstream clause openers
47934862
/// (`GROUP BY`/`ORDER BY`/…) are offered - additively and in canonical order -
47944863
/// so the popup is no longer silent there. The completeness gating must keep them
@@ -16992,6 +17061,15 @@ fn tool_command_keyword_slots_offer_only_supported_tool_keywords() {
1699217061
"SET AUTOCOMMIT __CODEX_CURSOR__",
1699317062
&["ON", "OFF", "TRUE", "FALSE"],
1699417063
);
17064+
assert_only(
17065+
"SET SQLTERMINATOR __CODEX_CURSOR__",
17066+
&["ON", "OFF"],
17067+
);
17068+
assert_only(
17069+
"SET SQLTERMINATOR O__CODEX_CURSOR__",
17070+
&["ON", "OFF"],
17071+
);
17072+
assert_only("SET SQLTERMINATOR OF__CODEX_CURSOR__", &["OFF"]);
1699517073
assert_only("SET VERIFY __CODEX_CURSOR__", &["ON", "OFF"]);
1699617074
assert_only("SET PAGESIZE __CODEX_CURSOR__", &[]);
1699717075
assert_only(
@@ -17429,7 +17507,13 @@ fn tool_command_keyword_slots_are_dialect_scoped() {
1742917507
);
1743017508
}
1743117509
let mysql_set = suggestions("SET |", db);
17432-
for oracle_only in ["AUTOCOMMIT", "SERVEROUTPUT", "PAGESIZE", "VERIFY"] {
17510+
for oracle_only in [
17511+
"AUTOCOMMIT",
17512+
"SERVEROUTPUT",
17513+
"SQLTERMINATOR",
17514+
"PAGESIZE",
17515+
"VERIFY",
17516+
] {
1743317517
assert!(
1743417518
!has(&mysql_set, oracle_only),
1743517519
"{oracle_only} leaked into {db:?} SET slot: {mysql_set:?}"

test/final.sql.out

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1020,7 +1020,7 @@ PROMPT [ORACLE FINAL] PASS
10201020
-- db: Oracle
10211021
-- metadata: test Oracle inferred catalog (file-scoped overlay) + Oracle built-ins
10221022
-- checked: 2349
1023-
-- suggestions checked: 9138
1023+
-- suggestions checked: 9164
10241024
-- missing: 0
10251025
-- suggestions without type labels: 0
10261026
-- marker: [[word]] means not suggested for the tested prefix

0 commit comments

Comments
 (0)