qail-core 1.3.1

AST-native query builder - type-safe expressions, zero SQL strings
Documentation
use std::collections::BTreeSet;

use crate::ast::Qail;

pub(super) fn normalize_column_name(name: impl Into<String>) -> String {
    let name = name.into();
    name.rsplit('.')
        .next()
        .unwrap_or(&name)
        .trim_matches('"')
        .to_ascii_lowercase()
}

pub(super) fn normalize_identifier_part(part: &str) -> String {
    part.trim().trim_matches('"').to_ascii_lowercase()
}

pub(super) fn normalize_table_ref(table_ref: &str) -> String {
    normalize_qualified_identifier(first_table_ref_token(table_ref))
}

pub(super) fn target_refs_for_command(cmd: &Qail, table: &str) -> BTreeSet<String> {
    let mut refs = BTreeSet::new();
    refs.insert(table.to_string());
    if let Some(short_name) = table.rsplit('.').next()
        && short_name != table
    {
        refs.insert(short_name.to_string());
    }
    if let Some(alias) = table_alias(&cmd.table) {
        refs.insert(alias);
    }
    if let Some(target_alias) = cmd
        .merge
        .as_ref()
        .and_then(|merge| merge.target_alias.as_deref())
    {
        refs.insert(normalize_identifier_part(target_alias));
    }
    refs
}

fn table_alias(table_ref: &str) -> Option<String> {
    let mut tokens = table_ref.split_whitespace();
    tokens.next()?;
    let token = tokens.next()?;
    let alias = if token.eq_ignore_ascii_case("as") {
        tokens.next()?
    } else {
        token
    };
    Some(normalize_identifier_part(alias)).filter(|alias| !alias.is_empty())
}

fn first_table_ref_token(table_ref: &str) -> &str {
    let trimmed = table_ref.trim_start();
    let mut in_double = false;
    let mut chars = trimmed.char_indices().peekable();

    while let Some((idx, ch)) = chars.next() {
        match ch {
            '"' => {
                if in_double && chars.peek().is_some_and(|(_, next)| *next == '"') {
                    chars.next();
                } else {
                    in_double = !in_double;
                }
            }
            _ if ch.is_whitespace() && !in_double => return &trimmed[..idx],
            _ => {}
        }
    }

    trimmed
}

fn normalize_qualified_identifier(value: &str) -> String {
    value
        .split('.')
        .map(normalize_identifier_part)
        .filter(|part| !part.is_empty())
        .collect::<Vec<_>>()
        .join(".")
}