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    /// If the associated handle authenticated as an Instance Principal, this value must be an OCID.
277    /// In all other cases, the value may be specified as either a name (or path for nested compartments) or as an OCID.
278    ///
279    /// If no compartment is given, the default compartment id for the handle is used. If that value was
280    /// not specified, the root compartment of the tenancy will be used.
281    pub fn compartment_id(mut self, compartment_id: &str) -> PutRequest {
282        self.compartment_id = compartment_id.to_string();
283        self
284    }
285
286    /// Return information about the existing row, if present.
287    /// Requesting this information incurs additional cost and may affect operation latency.
288    pub fn return_row(mut self, val: bool) -> PutRequest {
289        self.return_row = val;
290        self
291    }
292
293    /// Specifies the optional time to live (TTL) value, causing the time to live on
294    /// the row to be set to the specified value on put.
295    ///
296    /// Note: Internally, NoSQL uses a resolution of one hour for TTL values. This
297    /// value, if given, will be converted to a whole number of hours. The minimum
298    /// number of hours is 1.
299    pub fn ttl(mut self, val: &Duration) -> PutRequest {
300        self.ttl = val.clone();
301        self
302    }
303
304    /// Specifies whether to use the table's default TTL for the row.
305    /// If true, and there is an existing row, causes the operation to update
306    /// the time to live (TTL) value of the row based on the table's default
307    /// TTL if set. If the table has no default TTL this setting has no effect.
308    /// By default updating an existing row has no effect on its TTL.
309    pub fn use_table_ttl(mut self, val: bool) -> PutRequest {
310        self.use_table_ttl = val;
311        self
312    }
313
314    /// Succeed only if the given row exists and its version matches the given version.
315    pub fn if_version(mut self, version: &Version) -> PutRequest {
316        self.match_version = version.clone();
317        self.if_present = false;
318        self.if_absent = false;
319        self
320    }
321
322    /// Succeed only of the given row does not already exist.
323    pub fn if_absent(mut self) -> PutRequest {
324        self.if_absent = true;
325        self.if_present = false;
326        self.match_version.clear();
327        self
328    }
329
330    /// Succeed only of the given row already exists.
331    pub fn if_present(mut self) -> PutRequest {
332        self.if_present = true;
333        self.if_absent = false;
334        self.match_version.clear();
335        self
336    }
337
338    pub async fn execute(&self, h: &Handle) -> Result<PutResult, NoSQLError> {
339        let mut w: Writer = Writer::new();
340        w.write_i16(h.inner.serial_version);
341        let timeout = h.get_timeout(&self.timeout);
342        self.serialize_internal(&mut w, false, false, &timeout);
343        let mut opts = SendOptions {
344            timeout: timeout,
345            retryable: false,
346            compartment_id: self.compartment_id.clone(),
347            ..Default::default()
348        };
349        let mut r = h.send_and_receive(w, &mut opts).await?;
350        let resp = PutRequest::nson_deserialize(&mut r)?;
351        Ok(resp)
352    }
353
354    fn serialize_internal(
355        &self,
356        w: &mut Writer,
357        is_sub_request: bool,
358        add_table_name: bool,
359        timeout: &Duration,
360    ) {
361        let mut ns = NsonSerializer::start_request(w);
362        let mut opcode = OpCode::Put;
363        if self.match_version.len() > 0 {
364            opcode = OpCode::PutIfVersion;
365        } else if self.if_present {
366            opcode = OpCode::PutIfPresent;
367        } else if self.if_absent {
368            opcode = OpCode::PutIfAbsent;
369        }
370
371        if is_sub_request {
372            if add_table_name {
373                ns.write_string_field(TABLE_NAME, &self.table_name);
374            }
375            ns.write_i32_field(OP_CODE, opcode as i32);
376            if self.abort_on_fail {
377                ns.write_bool_field(ABORT_ON_FAIL, true);
378            }
379        } else {
380            ns.start_header();
381            ns.write_header(opcode, timeout, &self.table_name);
382            ns.end_header();
383            ns.start_payload();
384            //ns.write_i32_field(DURABILITY, 0); // TODO
385        }
386
387        ns.write_true_bool_field(RETURN_ROW, self.return_row);
388
389        if self.match_version.len() > 0 {
390            ns.write_binary_field(ROW_VERSION, &self.match_version);
391        }
392
393        if self.use_table_ttl {
394            ns.write_bool_field(UPDATE_TTL, true);
395        } else if self.ttl.as_secs() > 0 {
396            // currently, NoSQL only allows DAYS or HOURS settings.
397            // calculate a whole number of hours, and if it is evenly divisible by 24,
398            // convert that to days.
399            let mut hours = self.ttl.as_secs() / 3600;
400            // minumum TTL
401            if hours == 0 {
402                hours = 1;
403            }
404            let mut ttl = format!("{} HOURS", hours);
405            if (hours % 24) == 0 {
406                ttl = format!("{} DAYS", hours / 24);
407            }
408            ns.write_string_field(TTL, &ttl);
409            ns.write_bool_field(UPDATE_TTL, true);
410        }
411
412        ns.write_true_bool_field(EXACT_MATCH, self.exact_match);
413        // TODO identity cache size
414
415        ns.write_map_field(VALUE, &self.value);
416
417        // TODO others
418
419        if is_sub_request == false {
420            ns.end_payload();
421        }
422        ns.end_request();
423    }
424
425    pub(crate) fn nson_deserialize(r: &mut Reader) -> Result<PutResult, NoSQLError> {
426        let mut walker = MapWalker::new(r)?;
427        let mut res: PutResult = Default::default();
428        while walker.has_next() {
429            walker.next()?;
430            let name = walker.current_name();
431            match name.as_str() {
432                ERROR_CODE => {
433                    //println!("   w: ERROR_CODE");
434                    walker.handle_error_code()?;
435                }
436                CONSUMED => {
437                    //println!("   w: CONSUMED");
438                    res.consumed = Some(walker.read_nson_consumed_capacity()?);
439                    //println!(" consumed={:?}", res.consumed);
440                }
441                ROW_VERSION => {
442                    //println!("   w: ROW_VERSION");
443                    res.version = Some(walker.read_nson_binary()?);
444                }
445                GENERATED => {
446                    //println!("   w: GENERATED");
447                    res.generated_value = Some(walker.read_nson_field_value()?);
448                    //println!("generated_value={:?}", res.generated_value);
449                }
450                RETURN_INFO => {
451                    //println!("   w: RETURN_INFO");
452                    read_return_info(walker.r, &mut res)?;
453                }
454                _ => {
455                    //println!("   put_result: skipping field '{}'", name);
456                    walker.skip_nson_field()?;
457                }
458            }
459        }
460        Ok(res)
461    }
462}
463
464// TODO: make this common to all write results
465fn read_return_info(r: &mut Reader, res: &mut PutResult) -> Result<(), NoSQLError> {
466    let mut walker = MapWalker::new(r)?;
467    while walker.has_next() {
468        walker.next()?;
469        let name = walker.current_name();
470        match name.as_str() {
471            EXISTING_MOD_TIME => {
472                //println!("   read_ri: EXISTING_MOD_TIME");
473                res.existing_modification_time = walker.read_nson_i64()?;
474            }
475            //EXISTING_EXPIRATION => {
476            //println!("   read_ri: EXISTING_EXPIRATION");
477            //res.existing_expiration_time = walker.read_nson_i64()?;
478            //},
479            EXISTING_VERSION => {
480                //println!("   read_ri: EXISTING_VERSION");
481                res.existing_version = Some(walker.read_nson_binary()?);
482            }
483            EXISTING_VALUE => {
484                //println!("   read_ri: EXISTING_VALUE");
485                res.existing_value = Some(walker.read_nson_map()?);
486            }
487            _ => {
488                //println!("   put_result read_ri: skipping field '{}'", name);
489                walker.skip_nson_field()?;
490            }
491        }
492    }
493    Ok(())
494}
495
496impl NsonRequest for PutRequest {
497    fn serialize(&self, w: &mut Writer, timeout: &Duration) {
498        self.serialize_internal(w, false, true, timeout);
499    }
500}
501
502impl NsonSubRequest for PutRequest {
503    fn serialize(&self, w: &mut Writer, timeout: &Duration) {
504        self.serialize_internal(w, true, false, timeout);
505    }
506}