1#![deny(unsafe_code)]
49#![warn(missing_debug_implementations)]
50
51pub mod connect;
52pub mod connector;
53pub mod error;
54pub mod params;
55pub mod topology;
56pub mod types;
57
58#[cfg(feature = "embedded")]
59pub mod embedded;
60
61#[cfg(feature = "grpc")]
62pub mod grpc;
63
64#[cfg(feature = "grpc")]
65pub mod router;
66
67#[cfg(feature = "redwire")]
68pub mod redwire;
69
70#[cfg(feature = "http")]
71pub mod http;
72
73pub use error::{ClientError, ErrorCode, Result};
74pub use params::{IntoParams, IntoValue, Value};
75pub use types::{
76 BulkInsertResult, DeleteResult, DocumentItem, ExistsResult, InsertResult, JsonValue, KvItem,
77 KvWatchEvent, ListOptions, ListResult, QueryResult, Row, ValueOut,
78};
79
80pub use connector::{
85 repl, BulkCreateStatus, CreatedEntity, HealthStatus, OperationStatus, QueryResponse,
86 RedDBClient,
87};
88
89use connect::Target;
90
91#[derive(Debug)]
93pub enum Reddb {
94 #[cfg(feature = "embedded")]
95 Embedded(embedded::EmbeddedClient),
96 #[cfg(feature = "grpc")]
97 Grpc(grpc::GrpcClient),
98 #[cfg(feature = "http")]
99 Http(http::HttpClient),
100 Unavailable(&'static str),
106}
107
108impl Reddb {
109 pub async fn connect(uri: &str) -> Result<Self> {
111 let target = connect::parse(uri)?;
112 match target {
113 Target::Memory => {
114 #[cfg(feature = "embedded")]
115 {
116 embedded::EmbeddedClient::in_memory().map(Reddb::Embedded)
117 }
118 #[cfg(not(feature = "embedded"))]
119 {
120 Err(ClientError::feature_disabled("embedded"))
121 }
122 }
123 Target::File { path } => {
124 #[cfg(feature = "embedded")]
125 {
126 embedded::EmbeddedClient::open(path).map(Reddb::Embedded)
127 }
128 #[cfg(not(feature = "embedded"))]
129 {
130 let _ = path;
131 Err(ClientError::feature_disabled("embedded"))
132 }
133 }
134 Target::Grpc { endpoint } => {
135 #[cfg(feature = "grpc")]
136 {
137 grpc::GrpcClient::connect(endpoint).await.map(Reddb::Grpc)
138 }
139 #[cfg(not(feature = "grpc"))]
140 {
141 let _ = endpoint;
142 Err(ClientError::feature_disabled("grpc"))
143 }
144 }
145 Target::GrpcCluster {
146 primary,
147 replicas,
148 force_primary,
149 } => {
150 #[cfg(feature = "grpc")]
151 {
152 grpc::GrpcClient::connect_cluster(primary, replicas, force_primary)
153 .await
154 .map(Reddb::Grpc)
155 }
156 #[cfg(not(feature = "grpc"))]
157 {
158 let _ = (primary, replicas, force_primary);
159 Err(ClientError::feature_disabled("grpc"))
160 }
161 }
162 Target::Http { base_url } => {
163 #[cfg(feature = "http")]
164 {
165 http::HttpClient::connect(http::HttpOptions::new(base_url))
166 .await
167 .map(Reddb::Http)
168 }
169 #[cfg(not(feature = "http"))]
170 {
171 let _ = base_url;
172 Err(ClientError::feature_disabled("http"))
173 }
174 }
175 }
176 }
177
178 pub async fn query(&self, sql: &str) -> Result<QueryResult> {
179 if sql.trim().is_empty() {
183 return Err(ClientError::new(
184 ErrorCode::InvalidArgument,
185 "query SQL must not be empty",
186 ));
187 }
188 match self {
189 #[cfg(feature = "embedded")]
190 Reddb::Embedded(c) => c.query(sql),
191 #[cfg(feature = "grpc")]
192 Reddb::Grpc(c) => c.query(sql).await,
193 #[cfg(feature = "http")]
194 Reddb::Http(c) => c.query(sql).await,
195 Reddb::Unavailable(name) => Err(ClientError::feature_disabled(name)),
196 }
197 }
198
199 pub async fn query_with<P: IntoParams>(&self, sql: &str, params: P) -> Result<QueryResult> {
220 let values = params.into_params();
221 match self {
222 #[cfg(feature = "embedded")]
223 Reddb::Embedded(c) => c.query_with(sql, values),
224 #[cfg(feature = "grpc")]
225 Reddb::Grpc(c) => c.query_with(sql, &values).await,
226 #[cfg(feature = "http")]
227 Reddb::Http(c) => c.query_with(sql, &values).await,
228 Reddb::Unavailable(name) => Err(ClientError::feature_disabled(name)),
229 }
230 }
231
232 pub async fn execute_with<P: IntoParams>(&self, sql: &str, params: P) -> Result<QueryResult> {
236 self.query_with(sql, params).await
237 }
238
239 pub async fn insert(&self, collection: &str, payload: &JsonValue) -> Result<InsertResult> {
240 match self {
241 #[cfg(feature = "embedded")]
242 Reddb::Embedded(c) => c.insert(collection, payload),
243 #[cfg(feature = "grpc")]
244 Reddb::Grpc(c) => c.insert(collection, payload).await,
245 #[cfg(feature = "http")]
246 Reddb::Http(c) => c.insert(collection, payload).await,
247 Reddb::Unavailable(name) => Err(ClientError::feature_disabled(name)),
248 }
249 }
250
251 pub async fn bulk_insert(
252 &self,
253 collection: &str,
254 payloads: &[JsonValue],
255 ) -> Result<BulkInsertResult> {
256 match self {
257 #[cfg(feature = "embedded")]
258 Reddb::Embedded(c) => c.bulk_insert(collection, payloads),
259 #[cfg(feature = "grpc")]
260 Reddb::Grpc(c) => c.bulk_insert(collection, payloads).await,
261 #[cfg(feature = "http")]
262 Reddb::Http(c) => c.bulk_insert(collection, payloads).await,
263 Reddb::Unavailable(name) => Err(ClientError::feature_disabled(name)),
264 }
265 }
266
267 pub async fn delete(&self, collection: &str, rid: &str) -> Result<u64> {
268 match self {
269 #[cfg(feature = "embedded")]
270 Reddb::Embedded(c) => c.delete(collection, rid),
271 #[cfg(feature = "grpc")]
272 Reddb::Grpc(c) => c.delete(collection, rid).await,
273 #[cfg(feature = "http")]
274 Reddb::Http(c) => c.delete(collection, rid).await,
275 Reddb::Unavailable(name) => Err(ClientError::feature_disabled(name)),
276 }
277 }
278
279 pub fn documents(&self) -> DocumentClient<'_> {
280 DocumentClient { db: self }
281 }
282
283 pub fn queue(&self) -> QueueClient<'_> {
284 QueueClient { db: self }
285 }
286
287 pub fn kv_collection<'a>(&'a self, collection: &'a str) -> KvClient<'a> {
288 KvClient {
289 db: self,
290 collection,
291 }
292 }
293
294 pub async fn begin(&self) -> Result<QueryResult> {
295 self.query("BEGIN").await
296 }
297
298 pub async fn commit(&self) -> Result<QueryResult> {
299 self.query("COMMIT").await
300 }
301
302 pub async fn rollback(&self) -> Result<QueryResult> {
303 self.query("ROLLBACK").await
304 }
305
306 pub async fn close(&self) -> Result<()> {
307 match self {
308 #[cfg(feature = "embedded")]
309 Reddb::Embedded(c) => c.close(),
310 #[cfg(feature = "grpc")]
311 Reddb::Grpc(c) => c.close().await,
312 #[cfg(feature = "http")]
313 Reddb::Http(c) => c.close().await,
314 Reddb::Unavailable(_) => Ok(()),
315 }
316 }
317
318 pub fn kv(&self) -> KvClient<'_> {
319 KvClient {
320 db: self,
321 collection: "kv_default",
322 }
323 }
324
325 pub fn config(&self) -> ConfigClient<'_> {
326 self.config_collection("red.config")
327 }
328
329 pub fn vault(&self) -> VaultClient<'_> {
330 self.vault_collection("red.vault")
331 }
332
333 pub fn config_collection<'a>(&'a self, collection: &'a str) -> ConfigClient<'a> {
334 ConfigClient {
335 db: self,
336 collection,
337 }
338 }
339
340 pub fn vault_collection<'a>(&'a self, collection: &'a str) -> VaultClient<'a> {
341 VaultClient {
342 db: self,
343 collection,
344 }
345 }
346}
347
348#[derive(Debug)]
349pub struct DocumentClient<'a> {
350 db: &'a Reddb,
351}
352
353impl<'a> DocumentClient<'a> {
354 pub async fn insert(&self, collection: &str, body: &JsonValue) -> Result<DocumentItem> {
355 ensure_json_object("document body", body)?;
356 let collection = sql_identifier(collection)?;
357 self.db
358 .query(&format!("CREATE DOCUMENT IF NOT EXISTS {collection}"))
359 .await?;
360 let result = self
361 .db
362 .query(&format!(
363 "INSERT INTO {collection} DOCUMENT (body) VALUES ({}) RETURNING *",
364 json_text_literal(body)
365 ))
366 .await?;
367 document_item_from_first_row(result)
368 }
369
370 pub async fn get(&self, collection: &str, rid: &str) -> Result<DocumentItem> {
371 let collection = sql_identifier(collection)?;
372 let result = self
373 .db
374 .query(&format!(
375 "SELECT * FROM {collection} WHERE rid = {} LIMIT 1",
376 sql_string_literal(rid)
377 ))
378 .await?;
379 document_item_from_first_row(result)
380 }
381
382 pub async fn list(&self, collection: &str, options: ListOptions<'_>) -> Result<ListResult> {
383 let collection = sql_identifier(collection)?;
384 let result = self
385 .db
386 .query(&select_sql(&collection, "*", &options))
387 .await?;
388 Ok(ListResult {
389 affected: result.affected,
390 items: result.rows,
391 })
392 }
393
394 pub async fn filter(&self, collection: &str, filter: &str) -> Result<ListResult> {
395 self.list(collection, ListOptions::new().filter(filter))
396 .await
397 }
398
399 pub async fn patch(
400 &self,
401 collection: &str,
402 rid: &str,
403 patch: &JsonValue,
404 ) -> Result<DocumentItem> {
405 let entries = patch.as_object().ok_or_else(|| {
406 ClientError::new(
407 ErrorCode::InvalidArgument,
408 "document patch must be a JSON object",
409 )
410 })?;
411 if entries.is_empty() {
412 return Err(ClientError::new(
413 ErrorCode::InvalidArgument,
414 "document patch must contain at least one field",
415 ));
416 }
417 let collection = sql_identifier(collection)?;
418 let assignments = entries
419 .iter()
420 .map(|(field, value)| {
421 Ok(format!(
422 "{} = {}",
423 sql_identifier(field)?,
424 json_value_literal(value)
425 ))
426 })
427 .collect::<Result<Vec<_>>>()?;
428 let result = self
429 .db
430 .query(&format!(
431 "UPDATE {collection} DOCUMENTS SET {} WHERE rid = {} RETURNING *",
432 assignments.join(", "),
433 sql_string_literal(rid)
434 ))
435 .await?;
436 document_item_from_first_row(result)
437 }
438
439 pub async fn delete(&self, collection: &str, rid: &str) -> Result<DeleteResult> {
440 let collection = sql_identifier(collection)?;
441 let result = self
442 .db
443 .query(&format!(
444 "DELETE FROM {collection} WHERE rid = {}",
445 sql_string_literal(rid)
446 ))
447 .await?;
448 Ok(DeleteResult {
449 affected: result.affected,
450 deleted: result.affected > 0,
451 })
452 }
453}
454
455#[derive(Debug)]
456pub struct QueueClient<'a> {
457 db: &'a Reddb,
458}
459
460impl<'a> QueueClient<'a> {
461 pub async fn create(&self, queue: &str) -> Result<QueryResult> {
462 self.db
463 .query(&format!(
464 "CREATE QUEUE IF NOT EXISTS {}",
465 sql_identifier(queue)?
466 ))
467 .await
468 }
469
470 pub async fn push(&self, queue: &str, value: &JsonValue) -> Result<QueryResult> {
471 self.db
472 .query(&format!(
473 "QUEUE PUSH {} {}",
474 sql_identifier(queue)?,
475 json_value_literal(value)
476 ))
477 .await
478 }
479
480 pub async fn peek(&self, queue: &str, limit: Option<u64>) -> Result<ListResult> {
481 let mut sql = format!("QUEUE PEEK {}", sql_identifier(queue)?);
482 if let Some(limit) = limit {
483 sql.push_str(&format!(" {limit}"));
484 }
485 let result = self.db.query(&sql).await?;
486 Ok(ListResult {
487 affected: result.affected,
488 items: result.rows,
489 })
490 }
491
492 pub async fn pop(&self, queue: &str) -> Result<ListResult> {
493 let result = self
494 .db
495 .query(&format!("QUEUE POP {}", sql_identifier(queue)?))
496 .await?;
497 Ok(ListResult {
498 affected: result.affected,
499 items: result.rows,
500 })
501 }
502
503 pub async fn len(&self, queue: &str) -> Result<u64> {
504 let result = self
505 .db
506 .query(&format!("QUEUE LEN {}", sql_identifier(queue)?))
507 .await?;
508 row_value(&result.rows, "len")
509 .and_then(value_as_u64)
510 .ok_or_else(|| ClientError::new(ErrorCode::InvalidResponse, "QUEUE LEN missing len"))
511 }
512
513 pub async fn purge(&self, queue: &str) -> Result<DeleteResult> {
514 let result = self
515 .db
516 .query(&format!("QUEUE PURGE {}", sql_identifier(queue)?))
517 .await?;
518 Ok(DeleteResult {
519 affected: result.affected,
520 deleted: result.affected > 0,
521 })
522 }
523}
524
525#[derive(Debug)]
526pub struct KvClient<'a> {
527 db: &'a Reddb,
528 collection: &'a str,
529}
530
531impl<'a> KvClient<'a> {
532 pub async fn set(&self, key: &str, value: JsonValue) -> Result<QueryResult> {
533 self.put(key, value, &[]).await
534 }
535
536 pub async fn put(&self, key: &str, value: JsonValue, tags: &[&str]) -> Result<QueryResult> {
537 let tag_clause = if tags.is_empty() {
538 String::new()
539 } else {
540 format!(
541 " TAGS [{}]",
542 tags.iter()
543 .map(|tag| kv_tag_literal(tag))
544 .collect::<Vec<_>>()
545 .join(", ")
546 )
547 };
548 self.db
549 .query(&format!(
550 "KV PUT {}.{} = {}{}",
551 kv_collection_identifier(self.collection)?,
552 kv_path_segment(key),
553 json_value_literal(&value),
554 tag_clause
555 ))
556 .await
557 }
558
559 pub async fn get(&self, key: &str) -> Result<Option<KvItem>> {
560 let result = self
561 .db
562 .query(&format!(
563 "KV GET {}.{}",
564 kv_collection_identifier(self.collection)?,
565 kv_path_segment(key)
566 ))
567 .await?;
568 Ok(kv_item_from_rows(&result.rows))
569 }
570
571 pub async fn exists(&self, key: &str) -> Result<ExistsResult> {
572 Ok(ExistsResult {
573 exists: self.get(key).await?.is_some(),
574 })
575 }
576
577 pub async fn delete(&self, key: &str) -> Result<DeleteResult> {
578 let result = self
579 .db
580 .query(&format!(
581 "KV DELETE {}.{}",
582 kv_collection_identifier(self.collection)?,
583 kv_path_segment(key)
584 ))
585 .await?;
586 Ok(DeleteResult {
587 affected: result.affected,
588 deleted: result.affected > 0,
589 })
590 }
591
592 pub async fn list(&self, options: ListOptions<'_>) -> Result<ListResult> {
593 let collection = sql_identifier(self.collection)?;
594 let result = self
595 .db
596 .query(&select_sql(&collection, "key, value", &options))
597 .await?;
598 Ok(ListResult {
599 affected: result.affected,
600 items: result.rows,
601 })
602 }
603
604 pub async fn invalidate_tags(&self, tags: &[&str]) -> Result<u64> {
605 let result = self
606 .db
607 .query(&format!(
608 "INVALIDATE TAGS [{}] FROM {}",
609 tags.iter()
610 .map(|tag| kv_tag_literal(tag))
611 .collect::<Vec<_>>()
612 .join(", "),
613 kv_collection_identifier(self.collection)?
614 ))
615 .await?;
616 Ok(result.affected)
617 }
618
619 pub async fn watch(&self, key: &str) -> Result<Vec<KvWatchEvent>> {
620 self.watch_from_lsn(key, None).await
621 }
622
623 pub async fn watch_from_lsn(
624 &self,
625 key: &str,
626 from_lsn: Option<u64>,
627 ) -> Result<Vec<KvWatchEvent>> {
628 #[cfg(not(feature = "http"))]
629 {
630 let _ = key;
631 let _ = from_lsn;
632 let _ = self.collection;
633 }
634 match self.db {
635 #[cfg(feature = "http")]
636 Reddb::Http(c) => c.watch_kv(self.collection, key, from_lsn, None).await,
637 #[cfg(feature = "embedded")]
638 Reddb::Embedded(_) => Err(ClientError::feature_disabled("kv.watch embedded")),
639 #[cfg(feature = "grpc")]
640 Reddb::Grpc(_) => Err(ClientError::feature_disabled("kv.watch grpc")),
641 Reddb::Unavailable(name) => Err(ClientError::feature_disabled(name)),
642 }
643 }
644
645 pub async fn watch_prefix(&self, prefix: &str) -> Result<Vec<KvWatchEvent>> {
646 self.watch_prefix_from_lsn(prefix, None).await
647 }
648
649 pub async fn watch_prefix_from_lsn(
650 &self,
651 prefix: &str,
652 from_lsn: Option<u64>,
653 ) -> Result<Vec<KvWatchEvent>> {
654 let key = format!("{prefix}.*");
655 self.watch_from_lsn(&key, from_lsn).await
656 }
657}
658
659#[derive(Debug)]
660pub struct ConfigClient<'a> {
661 db: &'a Reddb,
662 collection: &'a str,
663}
664
665impl<'a> ConfigClient<'a> {
666 pub async fn put(&self, key: &str, value: JsonValue, tags: &[&str]) -> Result<QueryResult> {
667 let mut sql = format!(
668 "PUT CONFIG {} {} = {}",
669 kv_collection_identifier(self.collection)?,
670 kv_path_segment(key),
671 json_value_literal(&value)
672 );
673 append_tag_clause(&mut sql, tags);
674 self.db.query(&sql).await
675 }
676
677 pub async fn put_secret_ref(
678 &self,
679 key: &str,
680 vault_collection: &str,
681 vault_key: &str,
682 tags: &[&str],
683 ) -> Result<QueryResult> {
684 let mut sql = format!(
685 "PUT CONFIG {} {} = SECRET_REF(vault, {}.{})",
686 kv_collection_identifier(self.collection)?,
687 kv_path_segment(key),
688 kv_collection_identifier(vault_collection)?,
689 kv_path_segment(vault_key)
690 );
691 append_tag_clause(&mut sql, tags);
692 self.db.query(&sql).await
693 }
694
695 pub async fn get(&self, key: &str) -> Result<QueryResult> {
696 self.db
697 .query(&format!(
698 "GET CONFIG {} {}",
699 kv_collection_identifier(self.collection)?,
700 kv_path_segment(key)
701 ))
702 .await
703 }
704
705 pub async fn resolve(&self, key: &str) -> Result<QueryResult> {
706 self.db
707 .query(&format!(
708 "RESOLVE CONFIG {} {}",
709 kv_collection_identifier(self.collection)?,
710 kv_path_segment(key)
711 ))
712 .await
713 }
714}
715
716#[derive(Debug)]
717pub struct VaultClient<'a> {
718 db: &'a Reddb,
719 collection: &'a str,
720}
721
722impl<'a> VaultClient<'a> {
723 pub async fn put(&self, key: &str, value: JsonValue, tags: &[&str]) -> Result<QueryResult> {
724 let mut sql = format!(
725 "VAULT PUT {}.{} = {}",
726 kv_collection_identifier(self.collection)?,
727 kv_path_segment(key),
728 json_value_literal(&value)
729 );
730 append_tag_clause(&mut sql, tags);
731 self.db.query(&sql).await
732 }
733
734 pub async fn get(&self, key: &str) -> Result<QueryResult> {
735 self.db
736 .query(&format!(
737 "VAULT GET {}.{}",
738 kv_collection_identifier(self.collection)?,
739 kv_path_segment(key)
740 ))
741 .await
742 }
743
744 pub async fn unseal(&self, key: &str) -> Result<QueryResult> {
745 self.db
746 .query(&format!(
747 "UNSEAL VAULT {}.{}",
748 kv_collection_identifier(self.collection)?,
749 kv_path_segment(key)
750 ))
751 .await
752 }
753}
754
755fn append_tag_clause(sql: &mut String, tags: &[&str]) {
756 if tags.is_empty() {
757 return;
758 }
759 sql.push_str(" TAGS [");
760 sql.push_str(
761 &tags
762 .iter()
763 .map(|tag| kv_tag_literal(tag))
764 .collect::<Vec<_>>()
765 .join(", "),
766 );
767 sql.push(']');
768}
769
770fn sql_identifier(value: &str) -> Result<String> {
771 let mut chars = value.chars();
772 let Some(first) = chars.next() else {
773 return Err(ClientError::new(
774 ErrorCode::InvalidArgument,
775 "identifier must not be empty",
776 ));
777 };
778 if !(first.is_ascii_alphabetic() || first == '_') {
779 return Err(ClientError::new(
780 ErrorCode::InvalidArgument,
781 format!("invalid identifier `{value}`"),
782 ));
783 }
784 if chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_') {
785 Ok(value.to_string())
786 } else {
787 Err(ClientError::new(
788 ErrorCode::InvalidArgument,
789 format!("invalid identifier `{value}`"),
790 ))
791 }
792}
793
794fn kv_collection_identifier(value: &str) -> Result<String> {
795 let mut parts = Vec::new();
796 for part in value.split('.') {
797 parts.push(sql_identifier(part)?);
798 }
799 Ok(parts.join("."))
800}
801
802fn kv_path_segment(value: &str) -> String {
803 if is_plain_path_segment(value) {
804 value.to_string()
805 } else {
806 sql_string_literal(value)
807 }
808}
809
810fn is_plain_path_segment(value: &str) -> bool {
811 let mut chars = value.chars();
812 let Some(first) = chars.next() else {
813 return false;
814 };
815 (first.is_ascii_alphabetic() || first == '_')
816 && chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
817}
818
819fn json_value_literal(value: &JsonValue) -> String {
820 match value {
821 JsonValue::Null => "NULL".to_string(),
822 JsonValue::Bool(value) => value.to_string(),
823 JsonValue::Number(value) => value.to_string(),
824 JsonValue::String(value) => sql_string_literal(value),
825 JsonValue::Array(_) | JsonValue::Object(_) => value.to_json_string(),
826 }
827}
828
829fn json_text_literal(value: &JsonValue) -> String {
830 sql_string_literal(&value.to_json_string())
831}
832
833fn kv_tag_literal(value: &str) -> String {
834 sql_string_literal(value)
835}
836
837fn sql_string_literal(value: &str) -> String {
838 format!("'{}'", value.replace('\'', "''"))
839}
840
841fn ensure_json_object(name: &str, value: &JsonValue) -> Result<()> {
842 if value.as_object().is_some() {
843 Ok(())
844 } else {
845 Err(ClientError::new(
846 ErrorCode::InvalidArgument,
847 format!("{name} must be a JSON object"),
848 ))
849 }
850}
851
852fn select_sql(collection: &str, columns: &str, options: &ListOptions<'_>) -> String {
853 let mut sql = format!("SELECT {columns} FROM {collection}");
854 if let Some(filter) = options.filter {
855 sql.push_str(" WHERE ");
856 sql.push_str(filter);
857 }
858 if let Some(order_by) = options.order_by {
859 sql.push_str(" ORDER BY ");
860 sql.push_str(order_by);
861 }
862 if let Some(limit) = options.limit {
863 sql.push_str(&format!(" LIMIT {limit}"));
864 }
865 sql
866}
867
868fn document_item_from_first_row(result: QueryResult) -> Result<DocumentItem> {
869 let Some(row) = result.rows.into_iter().next() else {
870 return Err(ClientError::new(ErrorCode::NotFound, "document not found"));
871 };
872 let rid = row
873 .iter()
874 .find(|(column, _)| column == "rid")
875 .and_then(|(_, value)| value_as_string(value))
876 .ok_or_else(|| ClientError::new(ErrorCode::InvalidResponse, "document row missing rid"))?;
877 Ok(DocumentItem { rid, fields: row })
878}
879
880fn kv_item_from_rows(rows: &[Row]) -> Option<KvItem> {
881 let row = rows.first()?;
882 let value = row
883 .iter()
884 .find(|(column, _)| column == "value")
885 .map(|(_, value)| value.clone())?;
886 let rid = row
887 .iter()
888 .find(|(column, _)| column == "rid")
889 .map(|(_, value)| value);
890 if matches!(rid, Some(ValueOut::Null)) && value == ValueOut::Null {
891 return None;
892 }
893 let collection = row
894 .iter()
895 .find(|(column, _)| column == "collection")
896 .and_then(|(_, value)| value_as_string(value))
897 .unwrap_or_default();
898 let key = row
899 .iter()
900 .find(|(column, _)| column == "key")
901 .and_then(|(_, value)| value_as_string(value))
902 .unwrap_or_default();
903 Some(KvItem {
904 collection,
905 key,
906 value,
907 })
908}
909
910fn row_value<'a>(rows: &'a [Row], column: &str) -> Option<&'a ValueOut> {
911 rows.first()?
912 .iter()
913 .find(|(name, _)| name == column)
914 .map(|(_, value)| value)
915}
916
917fn value_as_string(value: &ValueOut) -> Option<String> {
918 match value {
919 ValueOut::String(value) => Some(value.clone()),
920 ValueOut::Integer(value) => Some(value.to_string()),
921 _ => None,
922 }
923}
924
925fn value_as_u64(value: &ValueOut) -> Option<u64> {
926 match value {
927 ValueOut::Integer(value) => (*value).try_into().ok(),
928 ValueOut::Float(value) if *value >= 0.0 => Some(*value as u64),
929 ValueOut::String(value) => value.parse().ok(),
930 _ => None,
931 }
932}
933
934pub fn version() -> &'static str {
936 env!("CARGO_PKG_VERSION")
937}
938
939pub const HELPER_SPEC_VERSION: &str = "1.0";
946
947#[cfg(test)]
948mod helper_spec_tests {
949 use super::HELPER_SPEC_VERSION;
950
951 #[test]
952 fn helper_spec_version_is_pinned() {
953 assert_eq!(HELPER_SPEC_VERSION, "1.0");
954 }
955}