Initial SFERA platform baseline

This commit is contained in:
2026-05-16 19:03:49 +03:00
commit 3b845c8fce
282 changed files with 55045 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
[package]
name = "query-parser"
version = "0.1.0"
edition = "2021"
+143
View File
@@ -0,0 +1,143 @@
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedQueryText {
pub tables: Vec<String>,
}
pub fn parse_query_text(query_text: &str) -> ParsedQueryText {
ParsedQueryText {
tables: extract_tables(query_text),
}
}
pub fn extract_tables(query_text: &str) -> Vec<String> {
let mut tables = Vec::new();
let lines: Vec<String> = 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<String> {
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<String> {
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<String> {
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<String>, 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!["Справочник.Контрагенты"]);
}
}