use crate::ast::*;
pub trait ToMongo {
fn to_mongo(&self) -> String;
}
impl ToMongo for Qail {
fn to_mongo(&self) -> String {
match self.action {
Action::Get => {
if !self.joins.is_empty() {
build_aggregate(self)
} else {
build_find(self)
}
}
Action::Set => build_update(self),
Action::Add => build_insert(self),
Action::Put => build_upsert(self),
Action::Del => build_delete(self),
Action::Make => format!("db.createCollection(\"{}\")", self.table),
Action::Drop => format!("db.{}.drop()", self.table),
Action::TxnStart => "session.startTransaction()".to_string(),
Action::TxnCommit => "session.commitTransaction()".to_string(),
Action::TxnRollback => "session.abortTransaction()".to_string(),
_ => format!("// Action {:?} not supported for MongoDB yet", self.action),
}
}
}
fn build_aggregate(cmd: &Qail) -> String {
let mut stages = Vec::new();
let filter = build_query_filter(cmd);
if filter != "{}" {
stages.push(format!("{{ \"$match\": {} }}", filter));
}
for join in &cmd.joins {
let target = &join.table;
let source_singular = cmd.table.trim_end_matches('s');
let pk = format!("{}_id", source_singular);
let lookup = format!(
"{{ \"$lookup\": {{ \"from\": \"{}\", \"localField\": \"_id\", \"foreignField\": \"{}\", \"as\": \"{}\" }} }}",
target, pk, target
);
stages.push(lookup);
}
let proj = build_projection(cmd);
if proj != "{}" {
stages.push(format!("{{ \"$project\": {} }}", proj));
}
for cage in &cmd.cages {
match &cage.kind {
CageKind::Sort(order) => {
let val = match order {
SortOrder::Asc | SortOrder::AscNullsFirst | SortOrder::AscNullsLast => 1,
SortOrder::Desc | SortOrder::DescNullsFirst | SortOrder::DescNullsLast => -1,
};
if let Some(cond) = cage.conditions.first() {
let col_str = match &cond.left {
Expr::Named(name) => name.clone(),
expr => expr.to_string(),
};
stages.push(format!("{{ \"$sort\": {{ \"{}\": {} }} }}", col_str, val));
}
}
CageKind::Offset(n) => stages.push(format!("{{ \"$skip\": {} }}", n)),
CageKind::Limit(n) => stages.push(format!("{{ \"$limit\": {} }}", n)),
_ => {}
}
}
format!("db.{}.aggregate([{}])", cmd.table, stages.join(", "))
}
fn build_find(cmd: &Qail) -> String {
let query = build_query_filter(cmd);
let projection = build_projection(cmd);
let mut mongo = format!("db.{}.find({}, {})", cmd.table, query, projection);
for cage in &cmd.cages {
match &cage.kind {
CageKind::Limit(n) => mongo.push_str(&format!(".limit({})", n)),
CageKind::Offset(n) => mongo.push_str(&format!(".skip({})", n)),
CageKind::Sort(order) => {
let val = match order {
SortOrder::Asc | SortOrder::AscNullsFirst | SortOrder::AscNullsLast => 1,
SortOrder::Desc | SortOrder::DescNullsFirst | SortOrder::DescNullsLast => -1,
};
if let Some(cond) = cage.conditions.first() {
let col_str = match &cond.left {
Expr::Named(name) => name.clone(),
expr => expr.to_string(),
};
mongo.push_str(&format!(".sort({{ \"{}\": {} }})", col_str, val));
}
}
_ => {}
}
}
mongo
}
fn build_update(cmd: &Qail) -> String {
let query = build_query_filter(cmd);
let mut update_doc = String::from("{ $set: { ");
let mut first = true;
for cage in &cmd.cages {
match cage.kind {
CageKind::Payload | CageKind::Filter => {
for cond in &cage.conditions {
if !first {
update_doc.push_str(", ");
}
let col_str = match &cond.left {
Expr::Named(name) => name.clone(),
expr => expr.to_string(),
};
update_doc.push_str(&format!(
"\"{}\": {}",
col_str,
value_to_json(&cond.value)
));
first = false;
}
}
_ => {}
}
}
update_doc.push_str(" } }");
format!("db.{}.updateMany({}, {})", cmd.table, query, update_doc)
}
fn build_insert(cmd: &Qail) -> String {
let mut doc = String::from("{ ");
let mut first = true;
for cage in &cmd.cages {
match cage.kind {
CageKind::Payload | CageKind::Filter => {
for cond in &cage.conditions {
if !first {
doc.push_str(", ");
}
let col_str = match &cond.left {
Expr::Named(name) => name.clone(),
expr => expr.to_string(),
};
doc.push_str(&format!("\"{}\": {}", col_str, value_to_json(&cond.value)));
first = false;
}
}
_ => {}
}
}
doc.push_str(" }");
format!("db.{}.insertOne({})", cmd.table, doc)
}
fn build_upsert(cmd: &Qail) -> String {
let query = build_query_filter(cmd);
let mut update_doc = String::from("{ $set: { ");
let mut first = true;
for cage in &cmd.cages {
match cage.kind {
CageKind::Payload | CageKind::Filter => {
for cond in &cage.conditions {
if !first {
update_doc.push_str(", ");
}
let col_str = match &cond.left {
Expr::Named(name) => name.clone(),
expr => expr.to_string(),
};
update_doc.push_str(&format!(
"\"{}\": {}",
col_str,
value_to_json(&cond.value)
));
first = false;
}
}
_ => {}
}
}
update_doc.push_str(" } }");
format!(
"db.{}.updateOne({}, {}, {{ \"upsert\": true }})",
cmd.table, query, update_doc
)
}
fn build_delete(cmd: &Qail) -> String {
let query = build_query_filter(cmd);
format!("db.{}.deleteMany({})", cmd.table, query)
}
fn build_query_filter(cmd: &Qail) -> String {
let mut query_parts = Vec::new();
for cage in &cmd.cages {
if let CageKind::Filter = cage.kind {
for cond in &cage.conditions {
let op = match cond.op {
Operator::Eq => "$eq",
Operator::Ne => "$ne",
Operator::Gt => "$gt",
Operator::Lt => "$lt",
Operator::Gte => "$gte",
Operator::Lte => "$lte",
_ => "$eq", };
let col_str = match &cond.left {
Expr::Named(name) => name.clone(),
expr => expr.to_string(),
};
if let Operator::Eq = cond.op {
query_parts.push(format!("\"{}\": {}", col_str, value_to_json(&cond.value)));
} else {
query_parts.push(format!(
"\"{}\": {{ \"{}\": {} }}",
col_str,
op,
value_to_json(&cond.value)
));
}
}
}
}
if query_parts.is_empty() {
return "{}".to_string();
}
format!("{{ {} }}", query_parts.join(", "))
}
fn build_projection(cmd: &Qail) -> String {
if cmd.columns.is_empty() {
return "{}".to_string();
}
let mut proj = String::from("{ ");
for (i, col) in cmd.columns.iter().enumerate() {
if i > 0 {
proj.push_str(", ");
}
if let Expr::Named(name) = col {
proj.push_str(&format!("\"{}\": 1", name));
}
}
proj.push_str(" }");
proj
}
fn value_to_json(v: &Value) -> String {
match v {
Value::String(s) => format!("\"{}\"", s),
Value::Int(n) => n.to_string(),
Value::Float(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::Null => "null".to_string(),
Value::Param(i) => format!("\"$param{}\"", i),
_ => "\"unknown\"".to_string(),
}
}