use crate::{plan_query, LogicalPlan};
use sqlparser::ast::{
OnConflictAction, OnInsert, SetExpr, Statement, TableFactor, TableWithJoins,
UpdateTableFromKind,
};
#[derive(Debug, Clone)]
pub enum DmlPlan {
InsertSelect {
table: String,
columns: Vec<String>,
source: Box<LogicalPlan>,
},
Upsert {
table: String,
columns: Vec<String>,
values: Vec<Vec<String>>,
conflict_column: String,
update_exprs: Vec<(String, String)>,
},
Update {
table: String,
set_exprs: Vec<(String, String)>,
predicate: Option<String>,
from_table: Option<String>,
},
}
pub fn plan_dml(stmt: &Statement) -> Option<DmlPlan> {
match stmt {
Statement::Insert(insert) => plan_insert(insert),
Statement::Update(update) => plan_update(update),
_ => None,
}
}
fn plan_insert(insert: &sqlparser::ast::Insert) -> Option<DmlPlan> {
let table = insert.table.to_string();
let columns: Vec<String> = insert
.columns
.iter()
.filter_map(|col| {
col.0
.last()
.and_then(|p| p.as_ident())
.map(|id| id.value.clone())
})
.collect();
if let Some(OnInsert::OnConflict(on_conflict)) = &insert.on {
if let OnConflictAction::DoUpdate(do_update) = &on_conflict.action {
let conflict_column = on_conflict
.conflict_target
.as_ref()
.map(|ct| match ct {
sqlparser::ast::ConflictTarget::Columns(cols) => cols
.iter()
.map(|id| id.value.clone())
.collect::<Vec<_>>()
.join(", "),
sqlparser::ast::ConflictTarget::OnConstraint(name) => name.to_string(),
})
.unwrap_or_default();
let update_exprs: Vec<(String, String)> = do_update
.assignments
.iter()
.map(|a| (a.target.to_string(), a.value.to_string()))
.collect();
let values = collect_values_rows(&insert.source);
return Some(DmlPlan::Upsert {
table,
columns,
values,
conflict_column,
update_exprs,
});
}
}
if let Some(src) = &insert.source {
if !matches!(*src.body, SetExpr::Values(_)) {
let source_plan = plan_query(src).ok()?;
return Some(DmlPlan::InsertSelect {
table,
columns,
source: Box::new(source_plan),
});
}
}
None
}
fn plan_update(update: &sqlparser::ast::Update) -> Option<DmlPlan> {
let table = match &update.table.relation {
sqlparser::ast::TableFactor::Table { name, .. } => name.to_string(),
other => other.to_string(),
};
let set_exprs: Vec<(String, String)> = update
.assignments
.iter()
.map(|a| (a.target.to_string(), a.value.to_string()))
.collect();
let predicate = update.selection.as_ref().map(|e| e.to_string());
let from_table = update.from.as_ref().and_then(|kind| {
let twjs: &[TableWithJoins] = match kind {
UpdateTableFromKind::BeforeSet(v) | UpdateTableFromKind::AfterSet(v) => v,
};
twjs.first().map(|twj| match &twj.relation {
TableFactor::Table { name, .. } => name.to_string(),
other => other.to_string(),
})
});
Some(DmlPlan::Update {
table,
set_exprs,
predicate,
from_table,
})
}
fn collect_values_rows(source: &Option<Box<sqlparser::ast::Query>>) -> Vec<Vec<String>> {
source
.as_ref()
.and_then(|q| {
if let SetExpr::Values(vals) = &*q.body {
Some(
vals.rows
.iter()
.map(|row| row.iter().map(|e| e.to_string()).collect())
.collect(),
)
} else {
None
}
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse_one;
#[test]
fn test_plan_insert_select() {
let stmt = parse_one("INSERT INTO archive SELECT * FROM orders WHERE status = 'done'")
.expect("parse");
let plan = plan_dml(&stmt).expect("should produce a DmlPlan");
match plan {
DmlPlan::InsertSelect { table, .. } => {
assert_eq!(table, "archive");
}
other => panic!("expected InsertSelect, got {other:?}"),
}
}
#[test]
fn test_plan_update() {
let stmt = parse_one("UPDATE users SET active = false WHERE last_login < '2020-01-01'")
.expect("parse");
let plan = plan_dml(&stmt).expect("should produce a DmlPlan");
match plan {
DmlPlan::Update {
table,
predicate,
from_table,
..
} => {
assert_eq!(table, "users");
assert!(predicate.is_some(), "expected a WHERE predicate");
assert!(
from_table.is_none(),
"plain UPDATE should have no from_table"
);
}
other => panic!("expected Update, got {other:?}"),
}
}
#[test]
fn test_plan_update_from() {
let sql = "UPDATE orders SET status = 'done' FROM users WHERE orders.user_id = users.id";
let result = crate::parse_postgres(sql);
match result {
Ok(stmts) => {
if let Some(stmt) = stmts.into_iter().next() {
let plan = plan_dml(&stmt);
match plan {
Some(DmlPlan::Update {
table, from_table, ..
}) => {
assert_eq!(table, "orders");
assert_eq!(
from_table.as_deref(),
Some("users"),
"from_table should be 'users', got {:?}",
from_table
);
}
other => {
let _ = other;
}
}
}
}
Err(_) => {
}
}
}
#[test]
fn test_plan_dml_returns_none_for_select() {
let stmt = parse_one("SELECT id FROM users").expect("parse");
let result = plan_dml(&stmt);
assert!(result.is_none(), "expected None for SELECT, got {result:?}");
}
#[test]
fn test_plan_upsert() {
let stmt = parse_one(
"INSERT INTO users (id, name) VALUES (1, 'alice') \
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name",
)
.expect("parse");
let plan = plan_dml(&stmt).expect("should produce a DmlPlan");
match plan {
DmlPlan::Upsert {
table,
conflict_column,
update_exprs,
..
} => {
assert_eq!(table, "users");
assert_eq!(conflict_column, "id");
assert!(
!update_exprs.is_empty(),
"expected at least one SET expression"
);
}
other => panic!("expected Upsert, got {other:?}"),
}
}
}