oracle_nosql_rust_sdk/
put_request.rs

1//
2// Copyright (c) 2024, 2025 Oracle and/or its affiliates. All rights reserved.
3//
4// Licensed under the Universal Permissive License v 1.0 as shown at
5//  https://oss.oracle.com/licenses/upl/
6//
7use crate::error::NoSQLError;
8use crate::handle::Handle;
9use crate::handle::SendOptions;
10use crate::nson::*;
11use crate::reader::Reader;
12use crate::types::{Capacity, FieldValue, MapValue, NoSQLRow, OpCode};
13use crate::writer::Writer;
14use crate::NoSQLErrorCode::IllegalArgument;
15use crate::Version;
16use std::result::Result;
17use std::time::Duration;
18
19/// Struct used for inserting a single row of data into a NoSQL table.
20///
21/// This request can be used to insert data represented as a NoSQL [`MapValue`], or as
22/// a native Rust struct using the [`macro@NoSQLRow`] derive macro.
23///
24/// This request can perform unconditional and conditional puts:
25/// - Overwrite existing row. This is the default.
26/// - Succeed only if the row does not exist. Use [`if_absent()`](PutRequest::if_absent()) for this case.
27/// - Succeed only if the row exists. Use [`if_present()`](PutRequest::if_present()) for this case.
28/// - Succeed only if the row exists and its [`Version`] matches a specific [`Version`]. Use [`if_version()`](PutRequest::if_version()) for this case.
29///
30/// Information about the existing row can be returned from a put operation using [`return_row(true)`](PutRequest::return_row()). Requesting this information incurs additional cost and may affect operation latency.
31///
32/// On successful operation, [`PutResult::version()`] is `Some`. This Version may
33/// be used in subsequent PutRequests.
34#[derive(Default, Debug)]
35pub struct PutRequest {
36    pub(crate) table_name: String,
37    pub(crate) compartment_id: String,
38    pub(crate) value: MapValue,
39    pub(crate) timeout: Option<Duration>,
40    pub(crate) abort_on_fail: bool,
41    pub(crate) return_row: bool,
42    if_present: bool,
43    if_absent: bool,
44    // TODO durability: Option<Version>
45    pub(crate) ttl: Duration,
46    pub(crate) use_table_ttl: bool,
47    pub(crate) exact_match: bool,
48    // TODO identity_cache_size,
49    match_version: Version,
50    // TODO: limiters, retry stats, etc
51}
52
53/// Struct representing the result of a [`PutRequest`] execution.
54///
55/// This struct is returned from a [`PutRequest::execute()`] call.
56#[derive(Default, Debug)]
57pub struct PutResult {
58    pub(crate) version: Option<Version>,
59    pub(crate) consumed: Option<Capacity>,
60    pub(crate) generated_value: Option<FieldValue>,
61    pub(crate) existing_modification_time: i64,
62    pub(crate) existing_value: Option<MapValue>,
63    pub(crate) existing_version: Option<Version>,
64    // TODO: stats, etc... (base)
65}
66
67impl PutResult {
68    /// Get the Version of the now-current record. This value is `Some` if the put operation succeeded. It
69    /// may be used in subsequent [`PutRequest::if_version()`] calls.
70    pub fn version(&self) -> Option<&Version> {
71        if let Some(v) = &self.version {
72            return Some(v);
73        }
74        None
75    }
76    /// Get the consumed capacity (read/write units) of the operation. This is only valid in the NoSQL Cloud Service.
77    pub fn consumed(&self) -> Option<&Capacity> {
78        if let Some(c) = &self.consumed {
79            return Some(c);
80        }
81        None
82    }
83    /// Get the value generated if the operation created a new value. This can happen if the table contains an
84    /// identity column or string column declared as a generated UUID. If the table has no such column, this value is `None`.
85    pub fn generated_value(&self) -> Option<&FieldValue> {
86        if let Some(r) = &self.generated_value {
87            return Some(r);
88        }
89        None
90    }
91
92    /// Get the modification time of the previous row if the put operation succeeded, or the modification time of the
93    /// current row if the operation failed due to a `if_version()` or `if_absent()` mismatch.
94    ///
95    /// In either case, this is only valid if [`return_row(true)`] was called on
96    /// the [`PutRequest`] and a previous row existed.
97    /// Its value is the number of milliseconds since the epoch (Jan 1 1970).
98    // TODO: make this a Time field
99    pub fn existing_modification_time(&self) -> i64 {
100        self.existing_modification_time
101    }
102    /// Get the value of the previous row if the put operation succeeded, or the value of the
103    /// current row if the operation failed due to a `if_version()` or `if_absent()` mismatch.
104    ///
105    /// In either case, this is only valid if [`return_row(true)`] was called on
106    /// the [`PutRequest`] and a previous row existed.
107    pub fn existing_value(&self) -> Option<&MapValue> {
108        if let Some(v) = &self.existing_value {
109            return Some(v);
110        }
111        None
112    }
113    /// Get the Version of the previous row if the put operation succeeded, or the Version of the
114    /// current row if the operation failed due to a `if_version()` or `if_absent()` mismatch.
115    ///
116    /// In either case, this is only valid if [`return_row(true)`] was called on
117    /// called on the [`PutRequest`] and a previous row existed.
118    pub fn existing_version(&self) -> Option<&Version> {
119        if let Some(v) = &self.existing_version {
120            return Some(v);
121        }
122        None
123    }
124    // TODO: stats, etc... (base)
125}
126
127impl PutRequest {
128    /// Create a new PutRequest.
129    ///
130    /// `table_name` should be the name of the table to insert the record into. It is required to be non-empty.
131    pub fn new(table_name: &str) -> PutRequest {
132        PutRequest {
133            table_name: table_name.to_string(),
134            ..Default::default()
135        }
136    }
137
138    /// Set the row value to use for the put operation, from a [`MapValue`].
139    ///
140    /// Either this method or [`put()`](PutRequest::put()) must be called for the `PutRequest` to be valid.
141    ///
142    /// The fields of the given value will be mapped to their matching table columns on insertion:
143    /// ```no_run
144    /// use oracle_nosql_rust_sdk::PutRequest;
145    /// use oracle_nosql_rust_sdk::types::*;
146    /// use chrono::DateTime;
147    ///
148    /// # use oracle_nosql_rust_sdk::Handle;
149    /// # use std::error::Error;
150    /// # use std::collections::HashMap;
151    /// # #[tokio::main]
152    /// # pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
153    /// # let handle = Handle::builder().build().await?;
154    /// // Assume a table was created with the following statement:
155    /// // "CREATE TABLE users (shard integer, id long, name string, street string, city string,
156    /// // zip integer, birth timestamp(3), numbers array(long), data binary, num_map map(integer),
157    /// // primary key(shard(shard), id))"
158    /// // A MapValue may be created a populated to represent the columns of the table as such:
159    /// let user = MapValue::new()
160    ///      .column("shard", 1)
161    ///      .column("id", 123456788)
162    ///      .column("name", "Jane".to_string())
163    ///      .column("street", Some("321 Main Street".to_string()))
164    ///      .column("city", "Anytown".to_string())
165    ///      .column("zip", 12345)
166    ///      .column("data", Option::<NoSQLBinary>::None)
167    ///      .column("numbers", vec![12345i64, 654556578i64, 43543543543543i64, 23232i64])
168    ///      .column("birth", Some(DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00")?))
169    ///      .column("num_map", HashMap::from([("cars".to_string(), 1), ("pets".to_string(), 4)]));
170    ///
171    /// let put_result = PutRequest::new("users")
172    ///                  .value(user)
173    ///                  .execute(&handle).await?;
174    /// # Ok(())
175    /// # }
176    /// ```
177    ///
178    pub fn value(mut self, val: MapValue) -> PutRequest {
179        self.value = val;
180        self
181    }
182
183    /// Set the row value to use for the put operation, from a given native Rust struct.
184    ///
185    /// Either this method or [`value()`](PutRequest::value()) must be called for the `PutRequest` to be valid.
186    ///
187    /// The fields of the given value will be mapped to their matching table columns on insertion, based
188    /// on the [`macro@NoSQLRow`] derive macro being specified on the given struct:
189    ///
190    /// ```no_run
191    /// use oracle_nosql_rust_sdk::PutRequest;
192    /// use oracle_nosql_rust_sdk::types::*;
193    /// use chrono::{DateTime, FixedOffset};
194    ///
195    /// # use oracle_nosql_rust_sdk::Handle;
196    /// # use std::error::Error;
197    /// # use std::collections::HashMap;
198    /// # #[tokio::main]
199    /// # pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
200    /// # let handle = Handle::builder().build().await?;
201    /// // Assume a table was created with the following statement:
202    /// // "CREATE TABLE users (shard integer, id long, name string, street string, city string,
203    /// // zip integer, birth timestamp(3), numbers array(long), data binary, num_map map(integer),
204    /// // primary key(shard(shard), id))"
205    /// // A corresponding Rust struct may look something like below. Adding the `NoSQLRow`
206    /// // derive allows instances of this struct to be written into the table without
207    /// // creating an equivalent `MapValue`.
208    /// #[derive(Default, Debug, NoSQLRow)]
209    /// struct Person {
210    ///     pub shard: i32,
211    ///     #[nosql(type=long, column=id)]
212    ///     pub uuid: i64,
213    ///     pub name: String,
214    ///     pub birth: Option<DateTime<FixedOffset>>,
215    ///     pub street: Option<String>,
216    ///     pub data: Option<NoSQLBinary>,
217    ///     pub city: String,
218    ///     pub zip: i32,
219    ///     pub numbers: Vec<i64>,
220    ///     pub num_map: HashMap<String, i32>,
221    /// }
222    ///
223    /// // Create an instance of the struct and insert it into the NoSQL database:
224    /// let user = Person {
225    ///      shard: 1,
226    ///      uuid: 123456788,
227    ///      name: "Jane".to_string(),
228    ///      street: Some("321 Main Street".to_string()),
229    ///      city: "Anytown".to_string(),
230    ///      zip: 12345,
231    ///      data: None,
232    ///      numbers: vec![12345, 654556578, 43543543543543, 23232],
233    ///      birth: Some(DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00")?),
234    ///      num_map: HashMap::from([("cars".to_string(), 1), ("pets".to_string(), 4)]),
235    ///      ..Default::default()
236    ///  };
237    ///
238    /// let put_result = PutRequest::new("users")
239    ///                  .put(user)?
240    ///                  .execute(&handle).await?;
241    /// # Ok(())
242    /// # }
243    /// ```
244    ///
245    /// See the [`GetRequest::execute_into()`](crate::GetRequest::execute_into()) documentation for
246    /// another example of using native Rust structs with `NoSQLRow`.
247    pub fn put(mut self, val: impl NoSQLRow) -> Result<PutRequest, NoSQLError> {
248        match val.to_map_value() {
249            Ok(value) => {
250                self.value = value;
251                return Ok(self);
252            }
253            Err(e) => {
254                // TODO: save error as source
255                return Err(NoSQLError::new(
256                    IllegalArgument,
257                    &format!("could not convert struct to MapValue: {}", e.to_string()),
258                ));
259            }
260        }
261    }
262
263    /// Specify the timeout value for the request.
264    ///
265    /// This is optional.
266    /// If set, it must be greater than or equal to 1 millisecond, otherwise an
267    /// IllegalArgument error will be returned.
268    /// If not set, the default timeout value configured for the [`Handle`](crate::HandleBuilder::timeout()) is used.
269    pub fn timeout(mut self, t: &Duration) -> PutRequest {
270        self.timeout = Some(t.clone());
271        self
272    }
273
274    /// Cloud Service only: set the name or id of a compartment to be used for this operation.
275    ///
276    /// The compartment may be specified as either a name (or path for nested compartments) or as an id (OCID).
277    /// A name (vs id) can only be used when authenticated using a specific user identity. It is not available if
278    /// the associated handle authenticated as an Instance Principal (which can be done when calling the service from
279    /// a compute instance in the Oracle Cloud Infrastructure: see [`HandleBuilder::cloud_auth_from_instance()`](crate::HandleBuilder::cloud_auth_from_instance()).)
280    ///
281    /// If no compartment is given, the root compartment of the tenancy will be used.
282    pub fn compartment_id(mut self, compartment_id: &str) -> PutRequest {
283        self.compartment_id = compartment_id.to_string();
284        self
285    }
286
287    /// Return information about the existing row, if present.
288    /// Requesting this information incurs additional cost and may affect operation latency.
289    pub fn return_row(mut self, val: bool) -> PutRequest {
290        self.return_row = val;
291        self
292    }
293
294    /// Specifies the optional time to live (TTL) value, causing the time to live on
295    /// the row to be set to the specified value on put.
296    ///
297    /// Note: Internally, NoSQL uses a resolution of one hour for TTL values. This
298    /// value, if given, will be converted to a whole number of hours. The minimum
299    /// number of hours is 1.
300    pub fn ttl(mut self, val: &Duration) -> PutRequest {
301        self.ttl = val.clone();
302        self
303    }
304
305    /// Specifies whether to use the table's default TTL for the row.
306    /// If true, and there is an existing row, causes the operation to update
307    /// the time to live (TTL) value of the row based on the table's default
308    /// TTL if set. If the table has no default TTL this setting has no effect.
309    /// By default updating an existing row has no effect on its TTL.
310    pub fn use_table_ttl(mut self, val: bool) -> PutRequest {
311        self.use_table_ttl = val;
312        self
313    }
314
315    /// Succeed only if the given row exists and its version matches the given version.
316    pub fn if_version(mut self, version: &Version) -> PutRequest {
317        self.match_version = version.clone();
318        self.if_present = false;
319        self.if_absent = false;
320        self
321    }
322
323    /// Succeed only of the given row does not already exist.
324    pub fn if_absent(mut self) -> PutRequest {
325        self.if_absent = true;
326        self.if_present = false;
327        self.match_version.clear();
328        self
329    }
330
331    /// Succeed only of the given row already exists.
332    pub fn if_present(mut self) -> PutRequest {
333        self.if_present = true;
334        self.if_absent = false;
335        self.match_version.clear();
336        self
337    }
338
339    pub async fn execute(&self, h: &Handle) -> Result<PutResult, NoSQLError> {
340        let mut w: Writer = Writer::new();
341        w.write_i16(h.inner.serial_version);
342        let timeout = h.get_timeout(&self.timeout);
343        self.serialize_internal(&mut w, false, false, &timeout);
344        let mut opts = SendOptions {
345            timeout: timeout,
346            retryable: false,
347            compartment_id: self.compartment_id.clone(),
348            ..Default::default()
349        };
350        let mut r = h.send_and_receive(w, &mut opts).await?;
351        let resp = PutRequest::nson_deserialize(&mut r)?;
352        Ok(resp)
353    }
354
355    fn serialize_internal(
356        &self,
357        w: &mut Writer,
358        is_sub_request: bool,
359        add_table_name: bool,
360        timeout: &Duration,
361    ) {
362        let mut ns = NsonSerializer::start_request(w);
363        let mut opcode = OpCode::Put;
364        if self.match_version.len() > 0 {
365            opcode = OpCode::PutIfVersion;
366        } else if self.if_present {
367            opcode = OpCode::PutIfPresent;
368        } else if self.if_absent {
369            opcode = OpCode::PutIfAbsent;
370        }
371
372        if is_sub_request {
373            if add_table_name {
374                ns.write_string_field(TABLE_NAME, &self.table_name);
375            }
376            ns.write_i32_field(OP_CODE, opcode as i32);
377            if self.abort_on_fail {
378                ns.write_bool_field(ABORT_ON_FAIL, true);
379            }
380        } else {
381            ns.start_header();
382            ns.write_header(opcode, timeout, &self.table_name);
383            ns.end_header();
384            ns.start_payload();
385            //ns.write_i32_field(DURABILITY, 0); // TODO
386        }
387
388        ns.write_true_bool_field(RETURN_ROW, self.return_row);
389
390        if self.match_version.len() > 0 {
391            ns.write_binary_field(ROW_VERSION, &self.match_version);
392        }
393
394        if self.use_table_ttl {
395            ns.write_bool_field(UPDATE_TTL, true);
396        } else if self.ttl.as_secs() > 0 {
397            // currently, NoSQL only allows DAYS or HOURS settings.
398            // calculate a whole number of hours, and if it is evenly divisible by 24,
399            // convert that to days.
400            let mut hours = self.ttl.as_secs() / 3600;
401            // minumum TTL
402            if hours == 0 {
403                hours = 1;
404            }
405            let mut ttl = format!("{} HOURS", hours);
406            if (hours % 24) == 0 {
407                ttl = format!("{} DAYS", hours / 24);
408            }
409            ns.write_string_field(TTL, &ttl);
410            ns.write_bool_field(UPDATE_TTL, true);
411        }
412
413        ns.write_true_bool_field(EXACT_MATCH, self.exact_match);
414        // TODO identity cache size
415
416        ns.write_map_field(VALUE, &self.value);
417
418        // TODO others
419
420        if is_sub_request == false {
421            ns.end_payload();
422        }
423        ns.end_request();
424    }
425
426    pub(crate) fn nson_deserialize(r: &mut Reader) -> Result<PutResult, NoSQLError> {
427        let mut walker = MapWalker::new(r)?;
428        let mut res: PutResult = Default::default();
429        while walker.has_next() {
430            walker.next()?;
431            let name = walker.current_name();
432            match name.as_str() {
433                ERROR_CODE => {
434                    //println!("   w: ERROR_CODE");
435                    walker.handle_error_code()?;
436                }
437                CONSUMED => {
438                    //println!("   w: CONSUMED");
439                    res.consumed = Some(walker.read_nson_consumed_capacity()?);
440                    //println!(" consumed={:?}", res.consumed);
441                }
442                ROW_VERSION => {
443                    //println!("   w: ROW_VERSION");
444                    res.version = Some(walker.read_nson_binary()?);
445                }
446                GENERATED => {
447                    //println!("   w: GENERATED");
448                    res.generated_value = Some(walker.read_nson_field_value()?);
449                    //println!("generated_value={:?}", res.generated_value);
450                }
451                RETURN_INFO => {
452                    //println!("   w: RETURN_INFO");
453                    read_return_info(walker.r, &mut res)?;
454                }
455                _ => {
456                    //println!("   put_result: skipping field '{}'", name);
457                    walker.skip_nson_field()?;
458                }
459            }
460        }
461        Ok(res)
462    }
463}
464
465// TODO: make this common to all write results
466fn read_return_info(r: &mut Reader, res: &mut PutResult) -> Result<(), NoSQLError> {
467    let mut walker = MapWalker::new(r)?;
468    while walker.has_next() {
469        walker.next()?;
470        let name = walker.current_name();
471        match name.as_str() {
472            EXISTING_MOD_TIME => {
473                //println!("   read_ri: EXISTING_MOD_TIME");
474                res.existing_modification_time = walker.read_nson_i64()?;
475            }
476            //EXISTING_EXPIRATION => {
477            //println!("   read_ri: EXISTING_EXPIRATION");
478            //res.existing_expiration_time = walker.read_nson_i64()?;
479            //},
480            EXISTING_VERSION => {
481                //println!("   read_ri: EXISTING_VERSION");
482                res.existing_version = Some(walker.read_nson_binary()?);
483            }
484            EXISTING_VALUE => {
485                //println!("   read_ri: EXISTING_VALUE");
486                res.existing_value = Some(walker.read_nson_map()?);
487            }
488            _ => {
489                //println!("   put_result read_ri: skipping field '{}'", name);
490                walker.skip_nson_field()?;
491            }
492        }
493    }
494    Ok(())
495}
496
497impl NsonRequest for PutRequest {
498    fn serialize(&self, w: &mut Writer, timeout: &Duration) {
499        self.serialize_internal(w, false, true, timeout);
500    }
501}
502
503impl NsonSubRequest for PutRequest {
504    fn serialize(&self, w: &mut Writer, timeout: &Duration) {
505        self.serialize_internal(w, true, false, timeout);
506    }
507}