use nodedb_types::DatabaseId;
use sqlparser::ast;
use super::super::dml_helpers::{
coerce_and_check_rows, convert_value_rows, materialize_defaults_in_rows,
};
use crate::error::{Result, SqlError};
use crate::parser::normalize::{normalize_insert_column, normalize_object_name_checked};
use crate::resolver::ColumnScope;
use crate::resolver::columns::{ResolvedTable, TableScope};
use crate::resolver::expr::convert_expr;
use crate::types::*;
const EXCLUDED_RELATION: &str = "excluded";
pub(super) fn resolve_target(
ins: &ast::Insert,
verb: &str,
catalog: &dyn SqlCatalog,
) -> Result<(String, CollectionInfo)> {
let table_name = match &ins.table {
ast::TableObject::TableName(name) => normalize_object_name_checked(name)?,
ast::TableObject::TableFunction(_) => {
return Err(SqlError::Unsupported {
detail: format!("{verb} INTO a table function is not supported"),
});
}
ast::TableObject::TableQuery(_) => {
return Err(SqlError::Unsupported {
detail: format!("{verb} INTO a subquery target is not supported"),
});
}
};
let info = catalog
.get_collection(DatabaseId::DEFAULT, &table_name)?
.ok_or_else(|| SqlError::UnknownTable {
name: table_name.clone(),
})?;
Ok((table_name, info))
}
pub(super) fn values_rows<'a>(
ins: &'a ast::Insert,
verb: &str,
) -> Result<&'a [ast::Parens<Vec<ast::Expr>>]> {
let source = ins.source.as_ref().ok_or_else(|| SqlError::Parse {
detail: format!("{verb} requires VALUES"),
})?;
match &*source.body {
ast::SetExpr::Values(values) => Ok(&values.rows),
_ => Err(SqlError::Unsupported {
detail: format!("{verb} source must be VALUES"),
}),
}
}
pub(super) fn target_scope(table_name: &str, info: &CollectionInfo) -> Result<TableScope> {
let mut scope = TableScope::single(ResolvedTable {
name: table_name.to_string(),
alias: None,
info: info.clone(),
})?;
scope.add_qualified_only(ResolvedTable {
name: EXCLUDED_RELATION.to_string(),
alias: None,
info: info.clone(),
})?;
Ok(scope)
}
pub(super) fn insert_columns(
columns: &[ast::ObjectName],
scope: &TableScope,
) -> Result<Vec<String>> {
columns
.iter()
.map(|c| {
let col = normalize_insert_column(c)?;
scope.check_name(None, &col)?;
Ok(col)
})
.collect()
}
pub(super) enum OnConflict {
None,
DoNothing,
DoUpdate(Vec<(String, SqlExpr)>),
}
pub(super) fn classify_on_conflict(ins: &ast::Insert, scope: &TableScope) -> Result<OnConflict> {
let Some(on) = ins.on.as_ref() else {
return Ok(OnConflict::None);
};
let ast::OnInsert::OnConflict(oc) = on else {
return Ok(OnConflict::None);
};
match &oc.action {
ast::OnConflictAction::DoNothing => Ok(OnConflict::DoNothing),
ast::OnConflictAction::DoUpdate(do_update) => {
let mut pairs = Vec::with_capacity(do_update.assignments.len());
for a in &do_update.assignments {
let name = match &a.target {
ast::AssignmentTarget::ColumnName(obj) => normalize_object_name_checked(obj)?,
ast::AssignmentTarget::Tuple(_) => {
return Err(SqlError::Unsupported {
detail: "ON CONFLICT DO UPDATE SET target must be a column name".into(),
});
}
};
scope.check_name(None, &name)?;
let expr = convert_expr(&a.value, &ColumnScope::Relations(scope))?;
pairs.push((name, expr));
}
Ok(OnConflict::DoUpdate(pairs))
}
}
}
pub(super) struct TypedRows {
pub rows: Vec<Vec<(String, SqlValue)>>,
pub volatile_defaults: bool,
}
pub(super) fn typed_rows(
info: &CollectionInfo,
columns: &[String],
rows_ast: &[ast::Parens<Vec<ast::Expr>>],
catalog: &dyn SqlCatalog,
) -> Result<TypedRows> {
let mut rows = convert_value_rows(columns, rows_ast)?;
let volatile_defaults = materialize_defaults_in_rows(&info.columns, &mut rows, catalog)?;
coerce_and_check_rows(info, &mut rows)?;
Ok(TypedRows {
rows,
volatile_defaults,
})
}
pub(super) fn column_schema(info: &CollectionInfo) -> Vec<(String, String)> {
info.columns
.iter()
.filter_map(|c| c.raw_type.as_ref().map(|t| (c.name.clone(), t.clone())))
.collect()
}