use sqlparser::ast;
use super::helpers::{extract_column_name, extract_string_literal};
use super::text_options::{TextOptions, parse_text_options};
use crate::error::{Result, SqlError};
use crate::parser::normalize::{normalize_ident, normalize_object_name_checked};
use crate::resolver::columns::ResolvedTable;
pub(super) struct TextCall {
pub field: Option<String>,
pub query: String,
pub options: TextOptions,
}
pub(super) fn resolve_table_column(
expr: &ast::Expr,
table: &ResolvedTable,
) -> Result<Option<String>> {
match expr {
ast::Expr::Identifier(ident) => Ok(Some(normalize_ident(ident))),
ast::Expr::CompoundIdentifier(parts) if parts.len() == 2 => {
let qualifier = normalize_ident(&parts[0]);
check_qualifier(&qualifier, table)?;
Ok(Some(normalize_ident(&parts[1])))
}
ast::Expr::CompoundIdentifier(_) => extract_column_name(expr).map(Some),
_ => Ok(None),
}
}
fn check_qualifier(qualifier: &str, table: &ResolvedTable) -> Result<()> {
if qualifier == table.name || table.alias.as_deref() == Some(qualifier) {
Ok(())
} else {
Err(SqlError::UnknownTable {
name: qualifier.to_owned(),
})
}
}
pub(super) fn resolve_text_call(
function: &str,
func: &ast::Function,
table: &ResolvedTable,
) -> Result<TextCall> {
let arity = || SqlError::Arity {
detail: format!("{function}() takes (column, 'query', [mode => ..., fuzzy => ...])"),
};
let ast::FunctionArguments::List(list) = &func.args else {
return Err(arity());
};
let options = parse_text_options(function, &list.args)?;
let mut unnamed = list.args.iter().filter_map(|a| match a {
ast::FunctionArg::Unnamed(e) => Some(e),
_ => None,
});
let first = unnamed.next().ok_or_else(arity)?;
let ast::FunctionArgExpr::Expr(query_expr) = unnamed.next().ok_or_else(arity)? else {
return Err(arity());
};
let field = match first {
ast::FunctionArgExpr::Wildcard => None,
ast::FunctionArgExpr::QualifiedWildcard(name) => {
check_qualifier(&normalize_object_name_checked(name)?, table)?;
None
}
ast::FunctionArgExpr::Expr(expr) => match resolve_table_column(expr, table)? {
Some(column) => {
table.check_text_column(function, &column)?;
Some(column)
}
None => return Err(not_a_column(function, table, first)),
},
ast::FunctionArgExpr::WildcardWithOptions(_) => {
return Err(not_a_column(function, table, first));
}
};
Ok(TextCall {
field,
query: extract_string_literal(query_expr)?,
options,
})
}
fn not_a_column(function: &str, table: &ResolvedTable, arg: &ast::FunctionArgExpr) -> SqlError {
SqlError::TextColumn {
function: function.to_owned(),
collection: table.name.clone(),
column: arg.to_string(),
fault: nodedb_types::text_search::TextColumnFault::NotAColumn,
}
}