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