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