Skip to content

Commit d35e2d2

Browse files
committed
SQL 포매팅 및 IntelliSense 전체 스윕 보강
1 parent b855fa5 commit d35e2d2

52 files changed

Lines changed: 7028 additions & 168 deletions

Some content is hidden

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

src/sql_parser_engine/state.rs

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,24 +1439,30 @@ impl SplitState {
14391439

14401440

14411441
pub(crate) fn track_create_plsql_symbol(&mut self, ch: char) {
1442-
if self.block_depth() != 0
1443-
|| self.create_plsql_kind != CreatePlsqlKind::PackageBody
1444-
|| !self.awaiting_package_body_name
1445-
{
1446-
return;
1442+
if self.pending_sql_macro_call_spec && ch == '(' {
1443+
// `SQL_MACRO(TABLE)` / `SQL_MACRO(SCALAR)` is a function
1444+
// declaration property. It is followed by the real `AS`/`IS`
1445+
// routine body and must not be treated like the body-less
1446+
// `AS SQL_MACRO;` call-spec form.
1447+
self.pending_sql_macro_call_spec = false;
14471448
}
14481449

1449-
let keeps_awaiting_segment_after_dot = self.awaiting_package_body_name_dot
1450-
&& matches!(ch, '/' | '*' | '-');
1450+
if self.block_depth() == 0
1451+
&& self.create_plsql_kind == CreatePlsqlKind::PackageBody
1452+
&& self.awaiting_package_body_name
1453+
{
1454+
let keeps_awaiting_segment_after_dot = self.awaiting_package_body_name_dot
1455+
&& matches!(ch, '/' | '*' | '-');
14511456

1452-
match ch {
1453-
'.' if !self.package_body_name_segments.is_empty() => {
1454-
self.awaiting_package_body_name_dot = true;
1455-
}
1456-
_ if !ch.is_whitespace() && !keeps_awaiting_segment_after_dot => {
1457-
self.awaiting_package_body_name_dot = false;
1457+
match ch {
1458+
'.' if !self.package_body_name_segments.is_empty() => {
1459+
self.awaiting_package_body_name_dot = true;
1460+
}
1461+
_ if !ch.is_whitespace() && !keeps_awaiting_segment_after_dot => {
1462+
self.awaiting_package_body_name_dot = false;
1463+
}
1464+
_ => {}
14581465
}
1459-
_ => {}
14601466
}
14611467
}
14621468

src/sql_parser_engine/tests.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5179,6 +5179,25 @@ fn sql_macro_call_spec_without_external_keyword_splits_before_following_statemen
51795179
assert!(statements[1].starts_with("SELECT 12 FROM dual"));
51805180
}
51815181

5182+
#[test]
5183+
fn parenthesized_sql_macro_property_keeps_function_body_attached() {
5184+
let mut engine = SqlParserEngine::new();
5185+
5186+
engine.process_line("CREATE OR REPLACE FUNCTION topn RETURN VARCHAR2 SQL_MACRO(TABLE)");
5187+
engine.process_line("IS");
5188+
engine.process_line("BEGIN");
5189+
engine.process_line(" RETURN 'SELECT 1 FROM dual';");
5190+
engine.process_line("END topn;");
5191+
engine.process_line("/");
5192+
engine.process_line("SELECT * FROM topn();");
5193+
5194+
let statements = engine.finalize_and_take_statements();
5195+
assert_eq!(statements.len(), 2, "unexpected statements: {statements:?}");
5196+
assert!(statements[0].contains("SQL_MACRO(TABLE)\nIS\nBEGIN"));
5197+
assert!(statements[0].contains("END topn"));
5198+
assert!(statements[1].starts_with("SELECT * FROM topn()"));
5199+
}
5200+
51825201
#[test]
51835202
fn package_nested_sql_macro_call_spec_closes_nested_function_block_on_semicolon() {
51845203
let mut engine = SqlParserEngine::new();

src/sql_text.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5276,6 +5276,22 @@ fn line_starts_ddl_query_body_header_prefix(line: &str) -> bool {
52765276
return true;
52775277
}
52785278

5279+
if words
5280+
.get(idx)
5281+
.is_some_and(|word| word.eq_ignore_ascii_case("JSON"))
5282+
&& words
5283+
.get(idx + 1)
5284+
.is_some_and(|word| word.eq_ignore_ascii_case("RELATIONAL"))
5285+
&& words
5286+
.get(idx + 2)
5287+
.is_some_and(|word| word.eq_ignore_ascii_case("DUALITY"))
5288+
&& words
5289+
.get(idx + 3)
5290+
.is_some_and(|word| word.eq_ignore_ascii_case("VIEW"))
5291+
{
5292+
return true;
5293+
}
5294+
52795295
if words
52805296
.get(idx)
52815297
.is_some_and(|word| word.eq_ignore_ascii_case("TABLE"))
@@ -11445,6 +11461,9 @@ mod tests {
1144511461
assert!(line_is_ddl_query_body_header(
1144611462
"CREATE MATERIALIZED VIEW mv_demo AS"
1144711463
));
11464+
assert!(line_is_ddl_query_body_header(
11465+
"CREATE OR REPLACE JSON RELATIONAL DUALITY VIEW dv_demo AS"
11466+
));
1144811467
assert!(line_is_ddl_query_body_header("CREATE TABLE t_demo AS"));
1144911468
assert!(line_is_ddl_query_body_header(
1145011469
"CREATE TEMPORARY TABLE t_demo AS"

src/ui/intellisense.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3723,7 +3723,13 @@ fn incomplete_quoted_identifier_start_before_cursor(
37233723
span.kind == crate::sql_parser_engine::LexicalKind::QuotedIdentifier
37243724
&& span.start == prev_idx
37253725
});
3726-
let bracket_starts_in_code = ch == '[' && lexical_span.is_none();
3726+
let bracket_starts_graph_edge = ch == '['
3727+
&& text
3728+
.get(..prev_idx)
3729+
.and_then(|head| head.chars().next_back())
3730+
== Some('-');
3731+
let bracket_starts_in_code =
3732+
ch == '[' && lexical_span.is_none() && !bracket_starts_graph_edge;
37273733
if (is_parser_quoted_identifier_start || bracket_starts_in_code)
37283734
&& !has_unescaped_identifier_delimiter(
37293735
text,
@@ -6374,6 +6380,17 @@ mod intellisense_tests {
63746380
assert_eq!(end, cursor);
63756381
}
63766382

6383+
#[test]
6384+
fn get_word_at_cursor_does_not_treat_property_graph_edge_as_bracket_identifier() {
6385+
let sql = "SELECT * FROM GRAPH_TABLE (g MATCH (a IS node) -[e I links]-> (b IS node))";
6386+
let cursor = sql.find(" links").expect("expected cursor anchor");
6387+
let (word, start, end) = get_word_at_cursor(sql, cursor);
6388+
6389+
assert_eq!(word, "I");
6390+
assert_eq!(sql.get(start..cursor), Some("I"));
6391+
assert_eq!(end, cursor);
6392+
}
6393+
63776394
#[test]
63786395
fn get_word_at_cursor_ignores_completed_quoted_identifier_before_cursor() {
63796396
let sql = r#"SELECT rec."Street Name".ci FROM dual"#;

src/ui/intellisense_context.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,6 +1198,22 @@ fn is_from_consuming_function(name: &str) -> bool {
11981198
sql_text::is_from_consuming_function(name)
11991199
}
12001200

1201+
fn is_nth_value_from_modifier(tokens: &[SqlToken], from_idx: usize) -> bool {
1202+
if !next_word_upper(tokens, from_idx.saturating_add(1))
1203+
.is_some_and(|(word, _)| matches!(word.as_str(), "FIRST" | "LAST"))
1204+
{
1205+
return false;
1206+
}
1207+
let Some((SqlToken::Symbol(close), close_idx)) = prev_non_comment_token(tokens, from_idx)
1208+
else {
1209+
return false;
1210+
};
1211+
close == ")"
1212+
&& matching_open_paren_before_close(tokens, close_idx)
1213+
.and_then(|open_idx| prev_word_upper(tokens, open_idx))
1214+
.is_some_and(|(word, _)| word == "NTH_VALUE")
1215+
}
1216+
12011217
/// FROM-clause table functions that may reference left-side row source aliases
12021218
/// without an explicit APPLY/LATERAL modifier.
12031219
fn is_implicitly_lateral_table_function(name: &str) -> bool {
@@ -3860,6 +3876,8 @@ fn scan_cursor_context(tokens: &[SqlToken], cursor_token_len: usize) -> CursorSc
38603876
let from_belongs_to_distinct_predicate =
38613877
is_distinct_from_operator(tokens, idx)
38623878
&& current_phase.is_column_context();
3879+
let from_belongs_to_nth_value_modifier =
3880+
is_nth_value_from_modifier(tokens, idx);
38633881
let should_treat_as_function_from = depth_frames
38643882
.get(depth)
38653883
.map(|frame| {
@@ -3880,6 +3898,7 @@ fn scan_cursor_context(tokens: &[SqlToken], cursor_token_len: usize) -> CursorSc
38803898
current_phase,
38813899
)
38823900
|| from_belongs_to_distinct_predicate
3901+
|| from_belongs_to_nth_value_modifier
38833902
{
38843903
relation_state.clear();
38853904
} else {

src/ui/intellisense_context/tests.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3149,6 +3149,17 @@ fn multiset_intersect_inside_expression_keeps_where_phase_and_table_scope() {
31493149
);
31503150
}
31513151

3152+
#[test]
3153+
fn nth_value_from_last_does_not_start_the_query_from_clause() {
3154+
let ctx = analyze(
3155+
"SELECT NTH_VALUE(sal, 1) FROM LAST IGNORE NULLS \
3156+
OVER (PARTITION BY deptno ORDER BY hiredate), | FROM emp",
3157+
);
3158+
3159+
assert_eq!(ctx.phase, SqlPhase::SelectList);
3160+
assert!(has_name(&table_names(&ctx), "EMP"));
3161+
}
3162+
31523163
// ─── Qualifier resolution tests ──────────────────────────────────────────
31533164

31543165
#[test]

src/ui/sql_editor/execution.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30830,7 +30830,7 @@ DROP TEMPORARY TABLE IF EXISTS qt_result_route_monitor;
3083030830
assert_formatted_mysql_fixture_scripts_run_without_errors(
3083130831
DatabaseType::MySQL,
3083230832
"test_mysql",
30833-
11,
30833+
12,
3083430834
);
3083530835
}
3083630836

@@ -30844,7 +30844,7 @@ DROP TEMPORARY TABLE IF EXISTS qt_result_route_monitor;
3084430844
assert_formatted_mysql_fixture_scripts_run_without_errors(
3084530845
DatabaseType::MariaDB,
3084630846
"test_mariadb",
30847-
13,
30847+
14,
3084830848
);
3084930849
}
3085030850

@@ -33702,7 +33702,7 @@ mod mysql_transaction_feedback_tests {
3370233702
})
3370333703
.collect::<Vec<_>>();
3370433704
fixture_paths.sort();
33705-
assert_eq!(fixture_paths.len(), 42, "Oracle fixture inventory changed");
33705+
assert_eq!(fixture_paths.len(), 43, "Oracle fixture inventory changed");
3370633706

3370733707
let mut audit_failures = Vec::new();
3370833708
for path in fixture_paths {

src/ui/sql_editor/format_sweep_tests.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3764,19 +3764,22 @@ fn formatting_sweep_generate_out_report_from_env() {
37643764
#[test]
37653765
#[ignore = "audits every SQL fixture and writes reports under target/format-sweep"]
37663766
fn formatting_sweep_all_files_generate_out_report() {
3767-
for layout in [SqlCommaListLayout::Wrapped, SqlCommaListLayout::Stacked] {
3768-
formatting_sweep_all_files_generate_out_report_for_layout(layout);
3769-
}
3767+
let checked_files = [SqlCommaListLayout::Wrapped, SqlCommaListLayout::Stacked]
3768+
.into_iter()
3769+
.map(formatting_sweep_all_files_generate_out_report_for_layout)
3770+
.sum::<usize>();
37703771

37713772
let output_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/format-sweep");
37723773
fs::write(
37733774
output_root.join("format-sweep.out"),
3774-
"Auto-format sweep aggregate\ncomma_list_layouts=[Wrapped, Stacked]\nchecked_files=132\nfailures=0\nwrapped_report=wrapped/format-sweep.out\nstacked_report=stacked/format-sweep.out\n",
3775+
format!(
3776+
"Auto-format sweep aggregate\ncomma_list_layouts=[Wrapped, Stacked]\nchecked_files={checked_files}\nfailures=0\nwrapped_report=wrapped/format-sweep.out\nstacked_report=stacked/format-sweep.out\n"
3777+
),
37753778
)
37763779
.expect("write combined format sweep aggregate");
37773780
}
37783781

3779-
fn formatting_sweep_all_files_generate_out_report_for_layout(layout: SqlCommaListLayout) {
3782+
fn formatting_sweep_all_files_generate_out_report_for_layout(layout: SqlCommaListLayout) -> usize {
37803783
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
37813784
let layout_dir = match layout {
37823785
SqlCommaListLayout::Wrapped => "wrapped",
@@ -3961,6 +3964,7 @@ fn formatting_sweep_all_files_generate_out_report_for_layout(layout: SqlCommaLis
39613964
managed_frame_kinds, expected_frame_kinds,
39623965
"the complete fixture sweep must exercise every production frame kind"
39633966
);
3967+
checked_files
39643968
}
39653969

39663970
#[test]

0 commit comments

Comments
 (0)