Skip to content

Commit 263ab18

Browse files
committed
자동 포멧팅 기능 개선
1 parent 612b617 commit 263ab18

16 files changed

Lines changed: 1012 additions & 110 deletions

src/db/transaction.rs

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ pub struct SessionResidueState {
6969
may_have_prepared_statement: bool,
7070
may_have_user_variable: bool,
7171
may_have_session_setting: bool,
72+
may_have_statement_diagnostics: bool,
7273
may_have_next_transaction_mode_override: bool,
7374
may_have_transaction_mode_override: bool,
7475
may_have_untracked_session_state: bool,
@@ -512,6 +513,7 @@ impl SessionResidueState {
512513
may_have_prepared_statement: effects.creates_prepared_statement,
513514
may_have_user_variable: effects.sets_user_variable,
514515
may_have_session_setting: effects.sets_session_setting,
516+
may_have_statement_diagnostics: effects.sets_statement_diagnostics,
515517
may_have_next_transaction_mode_override: effects.sets_next_transaction_mode_override,
516518
may_have_transaction_mode_override: effects.sets_transaction_mode_override,
517519
may_have_untracked_session_state: effects.may_leave_unknown_state,
@@ -527,6 +529,8 @@ impl SessionResidueState {
527529
may_have_user_variable: self.may_have_user_variable || other.may_have_user_variable,
528530
may_have_session_setting: self.may_have_session_setting
529531
|| other.may_have_session_setting,
532+
may_have_statement_diagnostics: self.may_have_statement_diagnostics
533+
|| other.may_have_statement_diagnostics,
530534
may_have_next_transaction_mode_override: self.may_have_next_transaction_mode_override
531535
|| other.may_have_next_transaction_mode_override,
532536
may_have_transaction_mode_override: self.may_have_transaction_mode_override
@@ -540,14 +544,22 @@ impl SessionResidueState {
540544
if effects.clears_all_session_residue {
541545
return Self::default();
542546
}
543-
let state = if effects.consumes_next_transaction_mode_override {
547+
let state = if effects.clears_statement_diagnostics {
544548
Self {
545-
may_have_next_transaction_mode_override: false,
549+
may_have_statement_diagnostics: false,
546550
..self
547551
}
548552
} else {
549553
self
550554
};
555+
let state = if effects.consumes_next_transaction_mode_override {
556+
Self {
557+
may_have_next_transaction_mode_override: false,
558+
..state
559+
}
560+
} else {
561+
state
562+
};
551563
state.merged_with(Self::from_effects(effects))
552564
}
553565

@@ -556,6 +568,7 @@ impl SessionResidueState {
556568
|| self.may_have_prepared_statement
557569
|| self.may_have_user_variable
558570
|| self.may_have_session_setting
571+
|| self.may_have_statement_diagnostics
559572
|| self.may_have_untracked_session_state
560573
}
561574

@@ -616,6 +629,13 @@ impl SessionResidueState {
616629
..self
617630
}
618631
}
632+
633+
fn with_statement_diagnostics_cleared(self) -> Self {
634+
Self {
635+
may_have_statement_diagnostics: false,
636+
..self
637+
}
638+
}
619639
}
620640

621641
pub(crate) fn mysql_statement_can_cleanup_retained_session_for_preflight(
@@ -900,6 +920,8 @@ struct StatementSessionResidueEffects {
900920
creates_prepared_statement: bool,
901921
sets_user_variable: bool,
902922
sets_session_setting: bool,
923+
sets_statement_diagnostics: bool,
924+
clears_statement_diagnostics: bool,
903925
sets_next_transaction_mode_override: bool,
904926
sets_transaction_mode_override: bool,
905927
consumes_next_transaction_mode_override: bool,
@@ -913,10 +935,19 @@ impl StatementSessionResidueEffects {
913935
|| self.creates_prepared_statement
914936
|| self.sets_user_variable
915937
|| self.sets_session_setting
938+
|| self.sets_statement_diagnostics
916939
|| self.sets_next_transaction_mode_override
917940
|| self.sets_transaction_mode_override
918941
|| self.may_leave_unknown_state
919942
}
943+
944+
fn without_statement_diagnostics(self) -> Self {
945+
Self {
946+
sets_statement_diagnostics: false,
947+
clears_statement_diagnostics: false,
948+
..self
949+
}
950+
}
920951
}
921952

922953
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
@@ -983,6 +1014,10 @@ impl StatementSessionEffects {
9831014
self.session_residue.may_leave_session_residue()
9841015
}
9851016

1017+
pub(crate) fn preserves_statement_diagnostics(self) -> bool {
1018+
self.session_residue.sets_statement_diagnostics
1019+
}
1020+
9861021
pub(crate) fn has_implicit_commit(self) -> bool {
9871022
self.transaction.has_implicit_commit
9881023
}
@@ -1425,7 +1460,10 @@ impl MySqlBatchSessionEffects {
14251460
} else {
14261461
self.session_residue_state = self
14271462
.session_residue_state
1428-
.merged_with(SessionResidueState::from_effects(effects.session_residue));
1463+
.with_statement_diagnostics_cleared()
1464+
.merged_with(SessionResidueState::from_effects(
1465+
effects.session_residue.without_statement_diagnostics(),
1466+
));
14291467
}
14301468

14311469
if cleanup_effects_confirmed && effects.releases_table_lock() {
@@ -1973,7 +2011,11 @@ pub(crate) fn retained_session_state_after_statement(
19732011
} else {
19742012
prior_state.session_residue_state()
19752013
};
1976-
prior_residue.merged_with(SessionResidueState::from_effects(effects.session_residue))
2014+
prior_residue
2015+
.with_statement_diagnostics_cleared()
2016+
.merged_with(SessionResidueState::from_effects(
2017+
effects.session_residue.without_statement_diagnostics(),
2018+
))
19772019
} else if effects.releases_physical_session() {
19782020
SessionResidueState::default()
19792021
} else {
@@ -4256,6 +4298,14 @@ fn mysql_session_residue_effects_for_analysis(
42564298

42574299
let leading_keyword = analysis.leading_keyword();
42584300
let mut effects = StatementSessionResidueEffects::default();
4301+
let sets_found_rows = matches!(leading_keyword, Some("SELECT" | "WITH"))
4302+
&& analysis
4303+
.words()
4304+
.iter()
4305+
.any(|word| word == "SQL_CALC_FOUND_ROWS");
4306+
effects.sets_statement_diagnostics =
4307+
sets_found_rows || analysis.classify_for_db_type(db_type) == SqlKind::Dml;
4308+
effects.clears_statement_diagnostics = !effects.sets_statement_diagnostics;
42594309
if mysql_statement_assigns_user_variable(sql, analysis) {
42604310
effects.sets_user_variable = true;
42614311
}
@@ -5661,6 +5711,56 @@ mod tests {
56615711
assert!(!plain_hint.may_leave_untracked_session_state);
56625712
}
56635713

5714+
#[test]
5715+
fn mysql_statement_diagnostics_are_retained_until_the_next_select() {
5716+
for db_type in [DatabaseType::MySQL, DatabaseType::MariaDB] {
5717+
let post_processor = statement_session_post_processor_for(db_type);
5718+
for sql in [
5719+
"UPDATE t SET value = value + 1",
5720+
"SELECT SQL_CALC_FOUND_ROWS id FROM t LIMIT 1",
5721+
] {
5722+
let effects = post_processor.effects_for_sql(sql);
5723+
assert!(
5724+
effects.preserves_statement_diagnostics(),
5725+
"{db_type} should preserve diagnostics after `{sql}`"
5726+
);
5727+
let retained = retained_session_state_after_statement(
5728+
post_processor,
5729+
RetainedSessionState::default(),
5730+
effects,
5731+
false,
5732+
false,
5733+
false,
5734+
false,
5735+
);
5736+
assert!(
5737+
retained.may_have_untracked_session_state(),
5738+
"{db_type} should retain the physical session after `{sql}`"
5739+
);
5740+
5741+
let next_select = post_processor.effects_for_sql("SELECT ROW_COUNT()");
5742+
assert!(!next_select.preserves_statement_diagnostics());
5743+
let cleared = retained_session_state_after_statement(
5744+
post_processor,
5745+
retained,
5746+
next_select,
5747+
false,
5748+
false,
5749+
false,
5750+
false,
5751+
);
5752+
assert!(
5753+
!cleared.may_have_untracked_session_state(),
5754+
"{db_type} should release consumed diagnostics after the next SELECT"
5755+
);
5756+
}
5757+
5758+
assert!(!post_processor
5759+
.effects_for_sql("SELECT 'SQL_CALC_FOUND_ROWS' AS note")
5760+
.preserves_statement_diagnostics());
5761+
}
5762+
}
5763+
56645764
#[test]
56655765
fn mysql_with_select_session_side_effects_are_tracked() {
56665766
let assignment_hint = mysql_session_state_hint_for_sql(

src/ui/intellisense.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,10 @@ pub struct IntellisenseData {
339339
member_kinds_by_qualifier: HashMap<String, HashMap<String, HashSet<QualifiedMemberKind>>>,
340340
relation_member_entries_by_qualifier: HashMap<String, Vec<NameEntry>>,
341341
collection_element_type_by_type: HashMap<String, String>,
342+
/// Declared object type of a column, keyed `TABLE.COLUMN`. Lets a
343+
/// `alias.object_column.` chain resolve to the type's attributes/methods
344+
/// even when nothing in the statement text spells the type out.
345+
object_type_by_column: HashMap<String, String>,
342346
synonym_target_by_synonym: HashMap<String, String>,
343347
relations_upper: HashSet<String>,
344348
/// Names of virtual tables (CTEs, subquery aliases) whose columns were
@@ -427,6 +431,7 @@ impl IntellisenseData {
427431
member_kinds_by_qualifier: HashMap::new(),
428432
relation_member_entries_by_qualifier: HashMap::new(),
429433
collection_element_type_by_type: HashMap::new(),
434+
object_type_by_column: HashMap::new(),
430435
synonym_target_by_synonym: HashMap::new(),
431436
relations_upper: HashSet::new(),
432437
virtual_table_keys: HashSet::new(),
@@ -1199,6 +1204,33 @@ impl IntellisenseData {
11991204
self.collection_element_type_by_type.insert(key, element);
12001205
}
12011206

1207+
pub fn set_object_type_for_column(&mut self, table: &str, column: &str, type_name: &str) {
1208+
let table_key = Self::normalize_qualifier_lookup_key(table);
1209+
let column_key = Self::normalize_qualifier_lookup_key(column);
1210+
let type_key = Self::normalize_qualifier_lookup_key(type_name);
1211+
if table_key.is_empty() || column_key.is_empty() || type_key.is_empty() {
1212+
return;
1213+
}
1214+
self.object_type_by_column
1215+
.insert(format!("{table_key}.{column_key}"), type_key);
1216+
}
1217+
1218+
pub fn object_type_for_column(&self, table: &str, column: &str) -> Option<&str> {
1219+
let column_key = Self::normalize_qualifier_lookup_key(column);
1220+
if column_key.is_empty() {
1221+
return None;
1222+
}
1223+
for table_key in Self::qualifier_lookup_keys(table) {
1224+
if let Some(type_name) = self
1225+
.object_type_by_column
1226+
.get(&format!("{table_key}.{column_key}"))
1227+
{
1228+
return Some(type_name.as_str());
1229+
}
1230+
}
1231+
None
1232+
}
1233+
12021234
pub fn collection_element_type_for_type(&self, collection_type: &str) -> Option<&str> {
12031235
for key in Self::qualifier_lookup_keys(collection_type) {
12041236
if let Some(element_type) = self.collection_element_type_by_type.get(&key) {

src/ui/intellisense_context.rs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,14 @@ fn ddl_alter_table_add_action_is_new_name(
785785
action_words: &[String],
786786
action_ends_with_dot: bool,
787787
) -> bool {
788+
let action_words = match action_words {
789+
[if_kw, not_kw, exists_kw, rest @ ..]
790+
if if_kw == "IF" && not_kw == "NOT" && exists_kw == "EXISTS" =>
791+
{
792+
rest
793+
}
794+
_ => action_words,
795+
};
788796
match action_words {
789797
[] => true,
790798
[word] if word == "CONSTRAINT" => true,
@@ -8087,13 +8095,18 @@ fn parse_top_level_pivot_clause(tokens: &[SqlToken]) -> Option<PivotClauseColumn
80878095
let aggregate_columns = parse_pivot_aggregate_columns(&clause_tokens[..for_idx]);
80888096
let aggregate_aliases = parse_pivot_aggregate_aliases(&clause_tokens[..for_idx]);
80898097
let for_columns = parse_identifier_segment(&clause_tokens[for_idx + 1..in_idx]);
8090-
let generated_columns = if pivot_mode.should_skip_generated_columns() {
8091-
Vec::new()
8092-
} else {
8093-
parse_pivot_generated_columns_from_in_segment(
8098+
let generated_columns = match pivot_mode {
8099+
PivotMode::Regular => parse_pivot_generated_columns_from_in_segment(
80948100
&clause_tokens[in_idx + 1..],
80958101
&aggregate_aliases,
8096-
)
8102+
),
8103+
PivotMode::Xml => {
8104+
let name = for_columns.join("_");
8105+
(!name.is_empty())
8106+
.then(|| format!("{name}_XML"))
8107+
.into_iter()
8108+
.collect()
8109+
}
80978110
};
80988111

80998112
let mut result = PivotClauseColumns {
@@ -12702,12 +12715,6 @@ enum PivotMode {
1270212715
Xml,
1270312716
}
1270412717

12705-
impl PivotMode {
12706-
fn should_skip_generated_columns(self) -> bool {
12707-
matches!(self, Self::Xml)
12708-
}
12709-
}
12710-
1271112718
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1271212719
enum IdentifierPathState {
1271312720
ExpectIdentifier,

src/ui/intellisense_context/tests.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7536,13 +7536,13 @@ fn extract_oracle_pivot_projection_columns_unescapes_string_literals_without_ali
75367536
}
75377537

75387538
#[test]
7539-
fn extract_oracle_pivot_xml_projection_keeps_source_columns_without_generated_aliases() {
7539+
fn extract_oracle_pivot_xml_projection_includes_the_generated_xml_column() {
75407540
let tokens = tokenize(
75417541
"SELECT * FROM (SELECT DEPTNO, job, SAL FROM oqt_t_emp) \
75427542
PIVOT XML (SUM(SAL) FOR DEPTNO IN (ANY))",
75437543
);
75447544
let cols = extract_oracle_pivot_unpivot_projection_columns(&tokens);
7545-
assert_eq!(cols, vec!["job"]);
7545+
assert_eq!(cols, vec!["job", "DEPTNO_XML"]);
75467546
}
75477547

75487548
#[test]
@@ -10076,11 +10076,11 @@ fn full_join_table_position_is_table_context() {
1007610076
// ─── State machine regression tests ─────────────────────────────────────
1007710077

1007810078
#[test]
10079-
fn pivot_xml_skips_generated_columns() {
10079+
fn pivot_xml_generates_one_xml_column() {
1008010080
let tokens =
1008110081
tokenize("SELECT * FROM sales PIVOT XML (SUM(amount) FOR quarter IN ('Q1' AS Q1))");
1008210082
let parsed = parse_top_level_pivot_clause(&tokens).expect("PIVOT XML clause should be parsed");
10083-
assert!(parsed.generated_columns.is_empty());
10083+
assert_eq!(parsed.generated_columns, vec!["quarter_XML".to_string()]);
1008410084
assert_eq!(parsed.for_columns, vec!["quarter".to_string()]);
1008510085
assert_eq!(parsed.aggregate_columns, vec!["amount".to_string()]);
1008610086
}

0 commit comments

Comments
 (0)