evergreen/
idldb.rs

1//! Tools for managing IDL-classed objects/values via SQL.
2use crate as eg;
3use chrono::prelude::*;
4use eg::db;
5use eg::idl;
6use eg::util::Pager;
7use eg::EgResult;
8use eg::EgValue;
9use pg::types::ToSql;
10use postgres as pg;
11use rust_decimal::Decimal;
12use std::cell::RefCell;
13use std::collections::HashMap;
14use std::fmt;
15use std::rc::Rc;
16use std::sync::Arc;
17
18pub const MAX_FLESH_DEPTH: i16 = 100;
19
20#[derive(Debug, Clone, PartialEq)]
21pub enum OrderByDir {
22    Asc,
23    Desc,
24}
25
26impl fmt::Display for OrderByDir {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        write!(
29            f,
30            "{}",
31            match *self {
32                OrderByDir::Asc => "ASC",
33                _ => "DESC",
34            }
35        )
36    }
37}
38
39#[derive(Debug, Clone, PartialEq)]
40pub struct OrderBy {
41    field: String,
42    dir: OrderByDir,
43}
44
45impl OrderBy {
46    pub fn new(field: &str, dir: OrderByDir) -> Self {
47        OrderBy {
48            dir,
49            field: field.to_string(),
50        }
51    }
52}
53
54/// Models a request to create a set of IDL objects of a given class.
55pub struct IdlClassCreate {
56    pub classname: String,
57    // Outer Vec is our list of value collections.
58    // Inner list is a single set of values to create.
59    pub values: Vec<Vec<(String, EgValue)>>,
60}
61
62impl IdlClassCreate {
63    pub fn new(classname: &str) -> Self {
64        IdlClassCreate {
65            classname: classname.to_string(),
66            values: vec![vec![]],
67        }
68    }
69}
70
71/// Models a request to update a set of values on a set of IDL objects
72/// of a given class.
73pub struct IdlClassUpdate {
74    pub classname: String,
75    pub values: Vec<(String, EgValue)>,
76    pub filter: Option<EgValue>,
77}
78
79impl IdlClassUpdate {
80    pub fn new(classname: &str) -> Self {
81        IdlClassUpdate {
82            classname: classname.to_string(),
83            values: Vec::new(),
84            filter: None,
85        }
86    }
87    pub fn reset(&mut self) {
88        self.values = Vec::new();
89        self.filter = None;
90    }
91    pub fn values(&self) -> &Vec<(String, EgValue)> {
92        &self.values
93    }
94
95    pub fn add_value(&mut self, field: &str, value: &EgValue) {
96        self.values.push((field.to_string(), value.clone()));
97    }
98
99    pub fn filter(&self) -> &Option<EgValue> {
100        &self.filter
101    }
102
103    pub fn set_filter(&mut self, f: EgValue) {
104        self.filter = Some(f);
105    }
106}
107
108#[derive(Debug, Clone)]
109pub struct FleshDef {
110    pub fields: HashMap<String, Vec<String>>,
111    /// Depth of <0 means flesh to maximum.
112    pub depth: i16,
113}
114
115impl FleshDef {
116    /// Creates a FleshDef from a JSON options hash/object.
117    ///
118    /// ```
119    /// use evergreen as eg;
120    /// use eg::idldb::FleshDef;
121    /// use json;
122    ///
123    /// let obj = eg::hash! {
124    ///   "flesh": -1, "flesh_fields": {"au": ["home_ou", "profile"]}
125    /// };
126    ///
127    /// let flesh_def = FleshDef::from_eg_value(&obj).expect("Parsed Flesh");
128    /// assert_eq!(flesh_def.depth, evergreen::idldb::MAX_FLESH_DEPTH);
129    /// assert_eq!(flesh_def.fields.len(), 1);
130    /// assert_eq!(flesh_def.fields.get("au").expect("Has an au").len(), 2);
131    /// ```
132    pub fn from_eg_value(obj: &EgValue) -> EgResult<Self> {
133        let mut fields = HashMap::new();
134
135        for (classname, field_names) in obj["flesh_fields"].entries() {
136            let mut list = Vec::new();
137            for name in field_names.members() {
138                let n = name
139                    .as_str()
140                    .ok_or_else(|| format!("Invalid flesh definition: {}", obj.dump()))?;
141                list.push(n.to_string());
142            }
143
144            fields.insert(classname.to_string(), list);
145        }
146
147        let depth = if let Some(num) = obj["flesh"].as_i16() {
148            // Is the flesh depth reasonable?
149            if (0..=MAX_FLESH_DEPTH).contains(&num) {
150                num
151            } else {
152                // Negative depth means max-flesh
153                MAX_FLESH_DEPTH
154            }
155        } else {
156            0
157        };
158
159        Ok(FleshDef { depth, fields })
160    }
161}
162
163/// Models a request to search for a set of IDL objects of a given class.
164#[derive(Debug)]
165pub struct IdlClassSearch {
166    pub classname: String,
167    pub filter: Option<EgValue>,
168    pub order_by: Option<Vec<OrderBy>>,
169    pub pager: Option<Pager>,
170    pub flesh: Option<FleshDef>,
171}
172
173impl IdlClassSearch {
174    pub fn new(classname: &str) -> Self {
175        IdlClassSearch {
176            classname: classname.to_string(),
177            filter: None,
178            order_by: None,
179            pager: None,
180            flesh: None,
181        }
182    }
183
184    pub fn set_flesh(&mut self, flesh_def: FleshDef) {
185        self.flesh = Some(flesh_def);
186    }
187
188    pub fn classname(&self) -> &str {
189        &self.classname
190    }
191
192    pub fn filter(&self) -> &Option<EgValue> {
193        &self.filter
194    }
195
196    pub fn set_filter(&mut self, f: EgValue) {
197        self.filter = Some(f);
198    }
199
200    pub fn order_by(&self) -> &Option<Vec<OrderBy>> {
201        &self.order_by
202    }
203
204    pub fn set_order_by(&mut self, v: Vec<OrderBy>) {
205        self.order_by = Some(v);
206    }
207
208    pub fn pager(&self) -> &Option<Pager> {
209        &self.pager
210    }
211
212    pub fn set_pager(&mut self, pager: Pager) {
213        self.pager = Some(pager);
214    }
215}
216
217/// Manages the translation to / from IDL objects and database queries.
218pub struct Translator {
219    db: Rc<RefCell<db::DatabaseConnection>>,
220}
221
222impl Translator {
223    pub fn new(db: Rc<RefCell<db::DatabaseConnection>>) -> Self {
224        Translator { db }
225    }
226
227    /// Start a new database transaction
228    pub fn xact_begin(&mut self) -> EgResult<()> {
229        self.db.borrow_mut().xact_begin()
230    }
231
232    /// Commit an in-progress transaction.
233    pub fn xact_commit(&mut self) -> EgResult<()> {
234        self.db.borrow_mut().xact_commit()
235    }
236
237    /// Roll back an in-progress transaction.
238    pub fn xact_rollback(&mut self) -> EgResult<()> {
239        self.db.borrow_mut().xact_rollback()
240    }
241
242    /// Retrieve an IDL object via pkey lookup.
243    ///
244    /// Numeric pkey values should be passed as strings.  They will be
245    /// numerified withih before the query is issued.
246    pub fn get_idl_object_by_pkey(
247        &self,
248        classname: &str,
249        pkey: &EgValue,
250        flesh_def: Option<FleshDef>,
251    ) -> EgResult<Option<EgValue>> {
252        let idl_class = idl::get_class(classname)?;
253
254        let pkey_field = match idl_class.pkey_field() {
255            Some(f) => f,
256            None => return Err(format!("Class {classname} has no primary key field").into()),
257        };
258
259        let mut filter = EgValue::new_object();
260        filter
261            .insert(pkey_field.name(), pkey.clone())
262            .expect("Is Object");
263
264        let mut search = IdlClassSearch::new(classname);
265        search.set_filter(filter);
266        search.flesh = flesh_def;
267
268        let mut list = self.idl_class_search(&search)?;
269
270        match list.len() {
271            0 => Ok(None),
272            1 => Ok(Some(list.pop().unwrap())),
273            _ => Err(format!("Pkey query for {classname} returned {} results", list.len()).into()),
274        }
275    }
276
277    /// Fleshes an IDL object in place based on the flesh_fields definitions.
278    pub fn flesh_idl_object(&self, object: &mut EgValue, flesh_def: &FleshDef) -> EgResult<()> {
279        if flesh_def.depth == 0 {
280            log::warn!("Attempt to flesh beyond flesh depth");
281            return Ok(());
282        }
283
284        let idl_class = self.get_idl_class_from_object(object)?.clone();
285        let classname = idl_class.classname();
286
287        // Clone these out since flesh_def is mutable.
288        let fieldnames;
289
290        if let Some(list) = flesh_def.fields.get(classname) {
291            fieldnames = list.clone();
292        } else {
293            // Nothing to flesh on this object.  Probably shouldnt
294            // ever get here, but treating like a non-error for now.
295            return Ok(());
296        }
297
298        // What fields are we fleshing on this class?
299        for fieldname in fieldnames.iter() {
300            self.flesh_idl_object_field(object, flesh_def, fieldname, &idl_class)?;
301        }
302
303        Ok(())
304    }
305
306    /// Flesh a single field on an object.
307    fn flesh_idl_object_field(
308        &self,
309        object: &mut EgValue,
310        flesh_def: &FleshDef,
311        fieldname: &str,
312        idl_class: &idl::Class,
313    ) -> EgResult<()> {
314        let classname = idl_class.classname();
315
316        // Def has to be cloned so it can be locally modified and
317        // given to another search.
318        let mut flesh_def = flesh_def.clone();
319
320        let idl_link = idl_class
321            .links()
322            .get(fieldname)
323            .ok_or_else(|| format!("Field {fieldname} on class {classname} cannot be fleshed"))?;
324
325        let reltype = idl_link.reltype();
326
327        let search_value = if reltype == idl::RelType::HasMany || reltype == idl::RelType::MightHave
328        {
329            // When the foreign key relationship points from the
330            // fleshed object back to us, the search value will be
331            // this object's primary key.
332
333            object
334                .pkey_value()
335                .ok_or_else(|| format!("Object has no pkey value: {}", object.dump()))?
336        } else {
337            //search_value = object[fieldname].clone();
338            &object[fieldname]
339        };
340
341        if !search_value.is_string() && !search_value.is_number() {
342            return Err(format!(
343                "Class {classname} cannot flesh field {fieldname} on value: {}",
344                search_value.dump()
345            )
346            .into());
347        }
348
349        // TODO verify the linked class may be accessed by this
350        // controller, e.g. pcrud
351        // Set the value to an array if needed for reltype.
352
353        if let Some(map_field) = idl_link.map() {
354            // When an intermediate mapping object is defined,
355            // add it to our pile of fleshed fields.
356            let cname = idl_link.class();
357            let fname = map_field.to_string();
358
359            if let Some(list) = flesh_def.fields.get_mut(cname) {
360                list.push(fname);
361            } else {
362                flesh_def.fields.insert(cname.to_string(), vec![fname]);
363            }
364        } else {
365            // When adding an implicit mapped field, avoid decrementing
366            // the flesh depth so the caller is not penalized.
367            flesh_def.depth -= 1;
368        }
369
370        log::debug!(
371            "Fleshing {}.{}; Link field: {}, remote class: {} , fkey: {}, reltype: {}",
372            classname,
373            fieldname,
374            idl_link.field(),
375            idl_link.class(),
376            idl_link.key(),
377            idl_link.reltype(),
378        );
379
380        let mut class_search = IdlClassSearch::new(idl_link.class());
381        class_search.flesh = Some(flesh_def);
382
383        let mut filter = eg::hash! {};
384        filter[idl_link.key()] = search_value.clone();
385        class_search.set_filter(filter);
386
387        let mut children = self.idl_class_search(&class_search)?;
388
389        log::debug!("Fleshed search returned {} results", children.len());
390
391        if !children.is_empty() {
392            // Get the values of the mapped fields on the found children
393            if let Some(map_field) = idl_link.map() {
394                let mut mapped_values = Vec::new();
395                for mut child in children.drain(..) {
396                    mapped_values.push(child[map_field].take());
397                }
398                children = mapped_values;
399            }
400        }
401
402        // Attach the child data to the fleshed object.
403
404        // For HasA / MightHave, the (presumably singl) child is linked
405        // directly to the parent by the field name.
406        if !children.is_empty()
407            && (reltype == idl::RelType::HasA || reltype == idl::RelType::MightHave)
408        {
409            object[fieldname] = children.remove(0); // len() above
410        }
411
412        // For HasMany, the children are retained as an array
413        if reltype == idl::RelType::HasMany {
414            object[fieldname] = EgValue::from(children);
415        }
416
417        Ok(())
418    }
419
420    /// Get the IDL Class representing to the provided object.
421    pub fn get_idl_class_from_object<'a>(&self, obj: &'a EgValue) -> EgResult<&'a Arc<idl::Class>> {
422        obj.idl_class()
423            .ok_or_else(|| format!("Not an IDL object: {}", obj.dump()).into())
424    }
425
426    /// Create an IDL object in the database
427    ///
428    /// Returns the created value
429    pub fn create_idl_object(&self, obj: &EgValue) -> EgResult<EgValue> {
430        let idl_class = self.get_idl_class_from_object(obj)?;
431
432        let mut create = IdlClassCreate::new(idl_class.classname());
433        let values = &mut create.values[0]; // list of lists
434
435        for name in idl_class.real_field_names_sorted() {
436            values.push((name.to_string(), obj[name].clone()));
437        }
438
439        let mut values = self.idl_class_create(&create)?;
440
441        if let Some(v) = values.pop() {
442            Ok(v)
443        } else {
444            // Should encounter an error before we get here, but just
445            // to cover our bases.
446            Err(format!(
447                "Could not create new value for class: {}",
448                idl_class.classname()
449            )
450            .into())
451        }
452    }
453
454    /// Create one or more IDL objects in the database.
455    ///
456    /// Returns the created rows.
457    pub fn idl_class_create(&self, create: &IdlClassCreate) -> EgResult<Vec<EgValue>> {
458        if !create.values.is_empty() {
459            return Err("No values to create in idl_class_create()".into());
460        }
461
462        if !self.db.borrow().in_transaction() {
463            return Err("idl_class_create() requires a transaction".into());
464        }
465
466        let classname = &create.classname;
467
468        let idl_class = idl::get_class(classname)?;
469
470        let tablename = idl_class.tablename().ok_or_else(|| {
471            format!("Cannot query an IDL class that has no tablename: {classname}")
472        })?;
473
474        let pkey_field = idl_class
475            .pkey()
476            .ok_or_else(|| "Cannot create rows that have no primary key".to_string())?;
477
478        let mut query = format!("INSERT INTO {tablename} (");
479
480        // Add the column names
481        query += &idl_class.real_field_names_sorted().join(", ");
482
483        query += ") VALUES ";
484
485        // Now add the sets of values to insert
486        let mut param_index: usize = 1;
487        let mut param_list: Vec<String> = Vec::new();
488        let mut strings: Vec<String> = Vec::new();
489        for values in &create.values {
490            strings.push(self.compile_class_create(
491                idl_class,
492                values,
493                &mut param_index,
494                &mut param_list,
495            )?);
496        }
497
498        query += &strings.join(", ");
499
500        // And finally, tell PG to return the primary keys just created.
501        query += &format!(" RETURNING {pkey_field}");
502
503        let mut params: Vec<&(dyn ToSql + Sync)> = Vec::new();
504        for p in param_list.iter() {
505            params.push(p);
506        }
507
508        log::debug!("create() executing query: {query}; params=[{param_list:?}]");
509
510        let query_res = self.db.borrow_mut().client().query(&query, &params);
511
512        if let Err(ref e) = query_res {
513            log::error!("DB Error: {e} query={query} param={params:?}");
514            return Err("DB query failed. See error logs".into());
515        }
516
517        // Use the primary key values reported by PG to find the
518        // newly created rows.
519        let mut results: Vec<EgValue> = Vec::new();
520
521        for row in query_res.unwrap() {
522            let pkey_value = Translator::col_value_to_json_value(&row, 0)?;
523
524            match self.get_idl_object_by_pkey(idl_class.classname(), &pkey_value, None)? {
525                Some(pkv) => results.push(pkv),
526                None => return Err("Could not recover newly created value from the DB".into()),
527            };
528        }
529
530        Ok(results)
531    }
532
533    /// Update one IDL object in the database.
534    pub fn update_idl_object(&self, obj: &EgValue) -> EgResult<u64> {
535        let idl_class = self.get_idl_class_from_object(obj)?;
536
537        let mut update = IdlClassUpdate::new(idl_class.classname());
538        for name in idl_class.real_field_names_sorted() {
539            update.add_value(name, &obj[name]);
540        }
541
542        let (pkey_field, pkey_value) = obj
543            .pkey_info()
544            .ok_or_else(|| "Object has no primary key field".to_string())?;
545
546        let mut filter = EgValue::new_object();
547        filter
548            .insert(pkey_field.name(), pkey_value.clone())
549            .unwrap();
550
551        update.set_filter(filter);
552
553        self.idl_class_update(&update)
554    }
555
556    /// Update one or more IDL objects in the database.
557    ///
558    /// Returns Result of the number of rows modified.
559    pub fn idl_class_update(&self, update: &IdlClassUpdate) -> EgResult<u64> {
560        if update.values.is_empty() {
561            Err("No values to update in idl_class_update()".to_string())?;
562        }
563
564        if !self.db.borrow().in_transaction() {
565            return Err("idl_class_update() requires a transaction".into());
566        }
567
568        let classname = &update.classname;
569
570        let class = idl::get_class(classname)?;
571
572        let tablename = class.tablename().ok_or_else(|| {
573            format!("Cannot query an IDL class that has no tablename: {classname}")
574        })?;
575
576        let mut param_list: Vec<String> = Vec::new();
577        let mut param_index: usize = 1;
578        let updates =
579            self.compile_class_update(class, &update.values, &mut param_index, &mut param_list)?;
580
581        let mut query = format!("UPDATE {tablename} {updates}");
582
583        if let Some(filter) = update.filter() {
584            query +=
585                &self.compile_class_filter(class, filter, &mut param_index, &mut param_list)?;
586        }
587
588        let mut params: Vec<&(dyn ToSql + Sync)> = Vec::new();
589        for p in param_list.iter() {
590            params.push(p);
591        }
592
593        log::debug!("update() executing query: {query}; params=[{param_list:?}]");
594
595        self.execute_one(&query, &params)
596    }
597
598    /// Execute a single db command and return the number of rows affected.
599    fn execute_one(&self, query: &str, params: &[&(dyn ToSql + Sync)]) -> EgResult<u64> {
600        log::debug!("update() executing query: {query}; params=[{params:?}]");
601
602        let query_res = self.db.borrow_mut().client().execute(query, params);
603
604        match query_res {
605            Ok(v) => {
606                log::debug!("Update modified {v} rows");
607                Ok(v)
608            }
609            Err(e) => {
610                log::error!("DB Error: {e} query={query} param={params:?}");
611                Err("DB query failed. See error logs".into())
612            }
613        }
614    }
615
616    /// Delete one IDL object via its primary key.
617    ///
618    /// Returns a Result of the number of rows affected.
619    pub fn delete_idl_object_by_pkey(&self, classname: &str, pkey: &EgValue) -> EgResult<u64> {
620        if !self.db.borrow().in_transaction() {
621            return Err("delete_idl_object_by_pkey requires a transaction".into());
622        }
623
624        let class = idl::get_class(classname)?;
625
626        let tablename = class.tablename().ok_or_else(|| {
627            format!("Cannot query an IDL class that has no tablename: {classname}")
628        })?;
629
630        let pkey_field = class
631            .pkey_field()
632            .ok_or_else(|| format!("IDL class {classname} has no primary key field"))?;
633
634        let mut param_list: Vec<String> = Vec::new();
635        let mut param_index: usize = 1;
636
637        let mut query = format!("DELETE FROM {tablename} WHERE {} ", pkey_field.name());
638
639        query += &self.append_json_literal(
640            &mut param_index,
641            &mut param_list,
642            pkey_field,
643            pkey,
644            Some("="),
645            false,
646        )?;
647
648        let mut params: Vec<&(dyn ToSql + Sync)> = Vec::new();
649        for p in param_list.iter() {
650            params.push(p);
651        }
652
653        self.execute_one(&query, &params)
654    }
655
656    /// Search for IDL objects in the database.
657    ///
658    /// Returns a Vec of the found IDL objects.
659    pub fn idl_class_search(&self, search: &IdlClassSearch) -> EgResult<Vec<EgValue>> {
660        let mut results: Vec<EgValue> = Vec::new();
661        let classname = &search.classname;
662
663        log::debug!("idl_class_search() {search:?}");
664
665        let class = idl::get_class(classname)?;
666
667        let tablename = class.tablename().ok_or_else(|| {
668            format!("Cannot query an IDL class that has no tablename: {classname}")
669        })?;
670
671        let columns = class.real_field_names_sorted().join(", ");
672
673        let mut query = format!("SELECT {columns} FROM {tablename}");
674
675        // Some parameters require binding within the DB statement.
676        // Put them here.
677        let mut param_list: Vec<String> = Vec::new();
678        let mut param_index: usize = 1;
679
680        if let Some(filter) = &search.filter {
681            query +=
682                &self.compile_class_filter(class, filter, &mut param_index, &mut param_list)?;
683        }
684
685        if let Some(order) = &search.order_by {
686            query += &self.compile_class_order_by(order);
687        }
688
689        if let Some(pager) = &search.pager {
690            query += &self.compile_pager(pager);
691        }
692
693        log::debug!("search() executing query: {query}");
694
695        let mut params: Vec<&(dyn ToSql + Sync)> = Vec::new();
696        for p in param_list.iter() {
697            params.push(p);
698        }
699
700        let query_res = self.db.borrow_mut().client().query(&query, &params);
701
702        if let Err(ref e) = query_res {
703            log::error!("DB Error: {e} query={query} param={params:?}");
704            return Err("DB query failed. See error logs".into());
705        }
706
707        for row in query_res.unwrap() {
708            let mut obj = self.row_to_idl(class, &row)?;
709            if let Some(flesh_def) = search.flesh.as_ref() {
710                self.flesh_idl_object(&mut obj, flesh_def)?;
711            }
712            results.push(obj);
713        }
714
715        Ok(results)
716    }
717
718    /// Create a query ORDER BY string.
719    fn compile_class_order_by(&self, order: &Vec<OrderBy>) -> String {
720        let mut sql = String::new();
721        let mut count = order.len();
722
723        if count > 0 {
724            sql += " ORDER BY";
725            for order_by in order {
726                sql += &format!(" {} {}", &order_by.field, &order_by.dir);
727                count -= 1;
728                if count > 0 {
729                    sql += ",";
730                }
731            }
732        }
733
734        sql
735    }
736
737    /// Translate numeric IDL field values from Strings into Numbers.
738    ///
739    /// Sometimes numbers are passed as strings in the wild west of JSON,
740    /// but the database doesn't want strings for, say, numeric primary key
741    /// matches.  Numerify if we should and can.
742    ///
743    /// JSON Null values are ignored.
744    fn try_translate_numeric(
745        &self,
746        idl_field: &idl::Field,
747        value: &EgValue,
748    ) -> EgResult<Option<EgValue>> {
749        if !value.is_string() {
750            return Ok(None);
751        }
752
753        if !idl_field.datatype().is_numeric() {
754            return Ok(None);
755        }
756
757        if let Some(n) = value.as_int() {
758            Ok(Some(EgValue::from(n)))
759        } else if let Some(n) = value.as_float() {
760            Ok(Some(EgValue::from(n)))
761        } else {
762            Err(format!("Value cannot be coerced int a number: {value}").into())
763        }
764    }
765
766    /// Create the values lists of an SQL create command.
767    fn compile_class_create(
768        &self,
769        class: &idl::Class,
770        values: &Vec<(String, EgValue)>,
771        param_index: &mut usize,
772        param_list: &mut Vec<String>,
773    ) -> EgResult<String> {
774        let mut sql = String::from("(");
775        let mut strings = Vec::new();
776
777        for kvp in values {
778            let field = &kvp.0;
779            let value = &kvp.1;
780
781            let idl_field = class.get_real_field(field).ok_or_else(|| {
782                format!(
783                    "No such real field '{field}' on class '{}'",
784                    class.classname()
785                )
786            })?;
787
788            strings.push(self.append_json_literal(
789                param_index,
790                param_list,
791                idl_field,
792                value,
793                None,
794                true,
795            )?);
796        }
797
798        sql += &strings.join(", ");
799
800        sql += ")";
801
802        Ok(sql)
803    }
804
805    /// Create the SET portion of an SQL update command.
806    fn compile_class_update(
807        &self,
808        class: &idl::Class,
809        values: &Vec<(String, EgValue)>,
810        param_index: &mut usize,
811        param_list: &mut Vec<String>,
812    ) -> EgResult<String> {
813        let mut parts = Vec::new();
814
815        for kvp in values {
816            let field = &kvp.0;
817            let value = &kvp.1;
818
819            let idl_field = class.get_real_field(field).ok_or_else(|| {
820                format!(
821                    "No such real field '{field}' on class '{}'",
822                    class.classname()
823                )
824            })?;
825
826            parts.push(format!(
827                "{field} {}",
828                self.append_json_literal(
829                    param_index,
830                    param_list,
831                    idl_field,
832                    value,
833                    Some("="),
834                    false,
835                )?
836            ));
837        }
838
839        Ok(format!("SET {}", parts.join(", ")))
840    }
841
842    /// Create the limit/offset part of the query string.
843    fn compile_pager(&self, pager: &Pager) -> String {
844        format!(" LIMIT {} OFFSET {}", pager.limit(), pager.offset())
845    }
846
847    /// Generate a WHERE clause from a JSON query object.
848    fn compile_class_filter(
849        &self,
850        class: &idl::Class,
851        filter: &EgValue,
852        param_index: &mut usize,
853        param_list: &mut Vec<String>,
854    ) -> EgResult<String> {
855        if !filter.is_object() {
856            return Err(format!(
857                "Translator class filter must be an object: {}",
858                filter.dump()
859            )
860            .into());
861        }
862
863        let mut filters = Vec::new();
864        for (field, subq) in filter.entries() {
865            log::trace!("compile_class_filter adding filter on field: {field}");
866
867            let idl_field = class.get_real_field(field).ok_or_else(|| {
868                format!(
869                    "No such real field '{field}' on class '{}'",
870                    class.classname()
871                )
872            })?;
873
874            let filter = match subq {
875                EgValue::Array(_) => {
876                    self.compile_class_filter_array(param_index, param_list, idl_field, subq, "IN")?
877                }
878                EgValue::Hash(_) => {
879                    self.compile_class_filter_object(param_index, param_list, idl_field, subq)?
880                }
881                EgValue::Number(_) | EgValue::String(_) => self.append_json_literal(
882                    param_index,
883                    param_list,
884                    idl_field,
885                    subq,
886                    Some("="),
887                    false,
888                )?,
889                EgValue::Boolean(_) | EgValue::Null => self.append_json_literal(
890                    param_index,
891                    param_list,
892                    idl_field,
893                    subq,
894                    Some("IS"),
895                    false,
896                )?,
897                EgValue::Blessed(_) => {
898                    return Err("Cannot create JSON filter from a blessed value".into())
899                }
900            };
901
902            filters.push(format!(" {field} {filter}"));
903        }
904
905        Ok(format!(" WHERE {}", filters.join(" AND")))
906    }
907
908    /// Add a JSON literal (scalar) value to a query.
909    ///
910    /// If the value is a JSON String, add it to the param_list for
911    /// query binding.  Otherwise, add it directly to the compiled
912    /// SQL string.
913    fn append_json_literal(
914        &self,
915        param_index: &mut usize,
916        param_list: &mut Vec<String>,
917        idl_field: &idl::Field,
918        obj: &EgValue,
919        operand: Option<&str>,
920        use_default: bool,
921    ) -> EgResult<String> {
922        if obj.is_object() || obj.is_array() {
923            return Err(format!("Cannot format array/object as a literal: {obj:?}").into());
924        }
925
926        if use_default && obj.is_null() {
927            return Ok("DEFAULT".to_string());
928        }
929
930        let opstr = match operand {
931            Some(op) => format!("{op} "),
932            None => String::new(),
933        };
934
935        // We may need to coerce a JSON String into a JSON Number
936        let new_obj = self.try_translate_numeric(idl_field, obj)?;
937        let obj = new_obj.as_ref().unwrap_or(obj);
938
939        // Track String parameters so we can use query binding on the
940        // them in the final query.  All other types, being derived
941        // from EgValue, have a known shape and size (number/bool/null),
942        // so query binding is less critical from a sql-injection
943        // perspective.
944        if obj.is_string() {
945            let s = format!("{opstr}${param_index}");
946            param_list.push(obj.to_string().expect("Is String"));
947            *param_index += 1;
948            Ok(s)
949        } else {
950            // obj here is a bool, number, or null
951            Ok(format!("{opstr}{}", obj))
952        }
953    }
954
955    /// Turn an object-based subquery into part of the WHERE AND.
956    fn compile_class_filter_object(
957        &self,
958        param_index: &mut usize,
959        param_list: &mut Vec<String>,
960        idl_field: &idl::Field,
961        obj: &EgValue,
962    ) -> EgResult<String> {
963        // A filter object may only contain a single operand => value combo
964        let (key, val) = obj
965            .entries()
966            .next()
967            .ok_or_else(|| format!("Invalid query object; {obj:?}"))?;
968
969        let operand = key.to_uppercase();
970
971        if !db::is_supported_operator(&operand) {
972            Err(format!("Unsupported operand: {operand} : {obj}"))?;
973        }
974
975        if val.is_array() {
976            // E.g. NOT IN (a, b, c, ...)
977
978            self.compile_class_filter_array(
979                param_index,
980                param_list,
981                idl_field,
982                val,
983                operand.as_str(),
984            )
985        } else {
986            self.append_json_literal(
987                param_index,
988                param_list,
989                idl_field,
990                val,
991                Some(&operand),
992                false,
993            )
994        }
995    }
996
997    /// Turn an array-based subquery into part of the WHERE AND.
998    ///
999    /// This creates a list of values to compare to, e.g. IN list.
1000    fn compile_class_filter_array(
1001        &self,
1002        param_index: &mut usize,
1003        param_list: &mut Vec<String>,
1004        idl_field: &idl::Field,
1005        arr: &EgValue,
1006        operand: &str,
1007    ) -> EgResult<String> {
1008        let operand = operand.to_uppercase();
1009        if !db::is_supported_operator(&operand) {
1010            Err(format!("Unsupported operand: {operand} : {arr}"))?;
1011        }
1012
1013        let mut filters: Vec<String> = Vec::new();
1014        for val in arr.members() {
1015            filters.push(self.append_json_literal(
1016                param_index,
1017                param_list,
1018                idl_field,
1019                val,
1020                None,
1021                false,
1022            )?);
1023        }
1024
1025        Ok(format!("{operand} ({})", filters.join(", ")))
1026    }
1027
1028    /// Maps a PG row into an IDL-based EgValue;
1029    fn row_to_idl(&self, class: &idl::Class, row: &pg::Row) -> EgResult<EgValue> {
1030        let mut obj = EgValue::new_object();
1031        obj.bless(class.classname())?;
1032
1033        for (index, name) in class.real_field_names_sorted().into_iter().enumerate() {
1034            obj[name] = Translator::col_value_to_json_value(row, index)?;
1035        }
1036
1037        Ok(obj)
1038    }
1039
1040    /// Translate a PG-typed row value into a EgValue
1041    pub fn col_value_to_json_value(row: &pg::Row, index: usize) -> EgResult<EgValue> {
1042        let col_type = row.columns().get(index).map(|c| c.type_().name()).unwrap();
1043
1044        match col_type {
1045            // EgValue has From<Option<T>>
1046            "bool" => {
1047                let v: Option<bool> = row.get(index);
1048                Ok(EgValue::from(v))
1049            }
1050            "interval" => {
1051                let v: Option<pg_interval::Interval> = row.get(index);
1052                let s = match v {
1053                    Some(val) => val.to_postgres(),
1054                    None => return Ok(EgValue::Null),
1055                };
1056                Ok(EgValue::from(s))
1057            }
1058            "varchar" | "char(n)" | "text" | "name" => {
1059                let v: Option<String> = row.get(index);
1060                Ok(EgValue::from(v))
1061            }
1062            "date" => {
1063                let v: Option<chrono::NaiveDate> = row.get(index);
1064                let s = match v {
1065                    Some(val) => val.format("%F").to_string(),
1066                    None => return Ok(EgValue::Null),
1067                };
1068                Ok(EgValue::from(s))
1069            }
1070            "timestamp" | "timestamptz" => {
1071                let v: Option<chrono::DateTime<Utc>> = row.get(index);
1072                let s = match v {
1073                    Some(val) => val.format("%FT%T%z").to_string(),
1074                    None => return Ok(EgValue::Null),
1075                };
1076                Ok(EgValue::from(s))
1077            }
1078            "int2" | "smallserial" | "smallint" => {
1079                let v: Option<i16> = row.get(index);
1080                Ok(EgValue::from(v))
1081            }
1082            "int" | "int4" | "serial" => {
1083                let v: Option<i32> = row.get(index);
1084                Ok(EgValue::from(v))
1085            }
1086            "int8" | "bigserial" | "bigint" => {
1087                let v: Option<i64> = row.get(index);
1088                Ok(EgValue::from(v))
1089            }
1090            "float4" | "real" => {
1091                let v: Option<f32> = row.get(index);
1092                Ok(EgValue::from(v))
1093            }
1094            "float8" | "double precision" => {
1095                let v: Option<f64> = row.get(index);
1096                Ok(EgValue::from(v))
1097            }
1098            "numeric" => {
1099                let decimal: Option<Decimal> = row.get(index);
1100                match decimal {
1101                    Some(d) => Ok(EgValue::from(d.to_string())),
1102                    None => Ok(EgValue::Null),
1103                }
1104            }
1105            "tsvector" => Ok(EgValue::Null),
1106            _ => Err(format!("Unsupported column type: {col_type}").into()),
1107        }
1108    }
1109}