#[derive(Debug, Clone, PartialEq, Eq)] pub struct ParsedQueryText { pub tables: Vec, } pub fn parse_query_text(query_text: &str) -> ParsedQueryText { ParsedQueryText { tables: extract_tables(query_text), } } pub fn extract_tables(query_text: &str) -> Vec { let mut tables = Vec::new(); let lines: Vec = query_text.lines().map(clean_query_line).collect(); for (index, line) in lines.iter().enumerate() { if let Some(table) = table_after_from(line) { push_unique(&mut tables, table); } else if is_standalone_from(line) { if let Some(next_line) = lines.get(index + 1) { if let Some(table) = first_table_token(next_line) { push_unique(&mut tables, table); } } } if let Some(table) = table_after_join(line) { push_unique(&mut tables, table); } } tables } fn clean_query_line(line: &str) -> String { let mut value = line .trim() .trim_end_matches(';') .trim() .trim_matches('"') .trim(); if let Some(stripped) = value.strip_prefix('|') { value = stripped.trim(); } value.trim_matches('"').trim().to_string() } fn is_standalone_from(line: &str) -> bool { line.eq_ignore_ascii_case("ИЗ") || line.eq_ignore_ascii_case("FROM") } fn table_after_from(line: &str) -> Option { let trimmed = line.trim(); let upper = trimmed.to_uppercase(); let rest = if upper.starts_with("ИЗ ") { &trimmed["ИЗ".len()..] } else if upper.starts_with("FROM ") { &trimmed["FROM".len()..] } else { return None; }; first_table_token(rest) } fn table_after_join(line: &str) -> Option { let normalized = line.replace('\t', " "); let parts: Vec<&str> = normalized.split_whitespace().collect(); let index = parts.iter().position(|part| { part.eq_ignore_ascii_case("JOIN") || part.eq_ignore_ascii_case("СОЕДИНЕНИЕ") })?; let table = parts.get(index + 1)?.trim_matches(','); if table.is_empty() { None } else { Some(table.to_string()) } } fn first_table_token(line: &str) -> Option { let table = line.trim().split_whitespace().next()?.trim_matches(','); if table.is_empty() { None } else { Some(table.to_string()) } } fn push_unique(values: &mut Vec, value: String) { if !values.iter().any(|existing| existing == &value) { values.push(value); } } #[cfg(test)] mod tests { use super::{extract_tables, parse_query_text}; #[test] fn extracts_table_after_standalone_from() { let tables = extract_tables( r#" ВЫБРАТЬ Остатки.Номенклатура ИЗ РегистрНакопления.ОстаткиТоваров КАК Остатки "#, ); assert_eq!(tables, vec!["РегистрНакопления.ОстаткиТоваров"]); } #[test] fn extracts_inline_from_and_join_tables() { let query = parse_query_text( r#" SELECT Orders.Ref, Customers.Description FROM Document.CustomerOrder AS Orders LEFT JOIN Catalog.Customers AS Customers ON Orders.Customer = Customers.Ref "#, ); assert_eq!( query.tables, vec!["Document.CustomerOrder", "Catalog.Customers"] ); } #[test] fn cleans_pipe_prefixed_1c_query_lines() { let tables = extract_tables( r#" |ВЫБРАТЬ |Контрагенты.Ссылка |ИЗ Справочник.Контрагенты КАК Контрагенты "#, ); assert_eq!(tables, vec!["Справочник.Контрагенты"]); } }