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}