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
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 pub async fn read_wait(
534 &self,
535 queue: &str,
536 consumer: &str,
537 wait: std::time::Duration,
538 opts: QueueReadWaitOptions<'_>,
539 ) -> Result<ListResult> {
540 let queue = sql_identifier(queue)?;
541 let consumer = sql_identifier(consumer)?;
542 let group = match opts.group {
543 Some(g) => format!(" GROUP {}", sql_identifier(g)?),
544 None => String::new(),
545 };
546 let count = match opts.count {
547 Some(c) => format!(" COUNT {c}"),
548 None => String::new(),
549 };
550 let wait_ms = wait.as_millis() as u64;
551 let sql = format!("QUEUE READ {queue}{group} CONSUMER {consumer}{count} WAIT {wait_ms}ms");
552 let result = self.db.query(&sql).await?;
553 Ok(ListResult {
554 affected: result.affected,
555 items: result.rows,
556 })
557 }
558}
559
560#[derive(Debug, Default, Clone, Copy)]
562pub struct QueueReadWaitOptions<'a> {
563 pub group: Option<&'a str>,
566 pub count: Option<u32>,
568}
569
570#[derive(Debug)]
571pub struct KvClient<'a> {
572 db: &'a Reddb,
573 collection: &'a str,
574}
575
576impl<'a> KvClient<'a> {
577 pub async fn set(&self, key: &str, value: JsonValue) -> Result<QueryResult> {
578 self.put(key, value, &[]).await
579 }
580
581 pub async fn put(&self, key: &str, value: JsonValue, tags: &[&str]) -> Result<QueryResult> {
582 let tag_clause = if tags.is_empty() {
583 String::new()
584 } else {
585 format!(
586 " TAGS [{}]",
587 tags.iter()
588 .map(|tag| kv_tag_literal(tag))
589 .collect::<Vec<_>>()
590 .join(", ")
591 )
592 };
593 self.db
594 .query(&format!(
595 "KV PUT {}.{} = {}{}",
596 kv_collection_identifier(self.collection)?,
597 kv_path_segment(key),
598 json_value_literal(&value),
599 tag_clause
600 ))
601 .await
602 }
603
604 pub async fn get(&self, key: &str) -> Result<Option<KvItem>> {
605 let result = self
606 .db
607 .query(&format!(
608 "KV GET {}.{}",
609 kv_collection_identifier(self.collection)?,
610 kv_path_segment(key)
611 ))
612 .await?;
613 Ok(kv_item_from_rows(&result.rows))
614 }
615
616 pub async fn exists(&self, key: &str) -> Result<ExistsResult> {
617 Ok(ExistsResult {
618 exists: self.get(key).await?.is_some(),
619 })
620 }
621
622 pub async fn delete(&self, key: &str) -> Result<DeleteResult> {
623 let result = self
624 .db
625 .query(&format!(
626 "KV DELETE {}.{}",
627 kv_collection_identifier(self.collection)?,
628 kv_path_segment(key)
629 ))
630 .await?;
631 Ok(DeleteResult {
632 affected: result.affected,
633 deleted: result.affected > 0,
634 })
635 }
636
637 pub async fn list(&self, options: ListOptions<'_>) -> Result<ListResult> {
638 let collection = sql_identifier(self.collection)?;
639 let result = self
640 .db
641 .query(&select_sql(&collection, "key, value", &options))
642 .await?;
643 Ok(ListResult {
644 affected: result.affected,
645 items: result.rows,
646 })
647 }
648
649 pub async fn invalidate_tags(&self, tags: &[&str]) -> Result<u64> {
650 let result = self
651 .db
652 .query(&format!(
653 "INVALIDATE TAGS [{}] FROM {}",
654 tags.iter()
655 .map(|tag| kv_tag_literal(tag))
656 .collect::<Vec<_>>()
657 .join(", "),
658 kv_collection_identifier(self.collection)?
659 ))
660 .await?;
661 Ok(result.affected)
662 }
663
664 pub async fn watch(&self, key: &str) -> Result<Vec<KvWatchEvent>> {
665 self.watch_from_lsn(key, None).await
666 }
667
668 pub async fn watch_from_lsn(
669 &self,
670 key: &str,
671 from_lsn: Option<u64>,
672 ) -> Result<Vec<KvWatchEvent>> {
673 #[cfg(not(feature = "http"))]
674 {
675 let _ = key;
676 let _ = from_lsn;
677 let _ = self.collection;
678 }
679 match self.db {
680 #[cfg(feature = "http")]
681 Reddb::Http(c) => c.watch_kv(self.collection, key, from_lsn, None).await,
682 #[cfg(feature = "embedded")]
683 Reddb::Embedded(_) => Err(ClientError::feature_disabled("kv.watch embedded")),
684 #[cfg(feature = "grpc")]
685 Reddb::Grpc(_) => Err(ClientError::feature_disabled("kv.watch grpc")),
686 Reddb::Unavailable(name) => Err(ClientError::feature_disabled(name)),
687 }
688 }
689
690 pub async fn watch_prefix(&self, prefix: &str) -> Result<Vec<KvWatchEvent>> {
691 self.watch_prefix_from_lsn(prefix, None).await
692 }
693
694 pub async fn watch_prefix_from_lsn(
695 &self,
696 prefix: &str,
697 from_lsn: Option<u64>,
698 ) -> Result<Vec<KvWatchEvent>> {
699 let key = format!("{prefix}.*");
700 self.watch_from_lsn(&key, from_lsn).await
701 }
702}
703
704#[derive(Debug)]
705pub struct ConfigClient<'a> {
706 db: &'a Reddb,
707 collection: &'a str,
708}
709
710impl<'a> ConfigClient<'a> {
711 pub async fn put(&self, key: &str, value: JsonValue, tags: &[&str]) -> Result<QueryResult> {
712 let mut sql = format!(
713 "PUT CONFIG {} {} = {}",
714 kv_collection_identifier(self.collection)?,
715 kv_path_segment(key),
716 json_value_literal(&value)
717 );
718 append_tag_clause(&mut sql, tags);
719 self.db.query(&sql).await
720 }
721
722 pub async fn put_secret_ref(
723 &self,
724 key: &str,
725 vault_collection: &str,
726 vault_key: &str,
727 tags: &[&str],
728 ) -> Result<QueryResult> {
729 let mut sql = format!(
730 "PUT CONFIG {} {} = SECRET_REF(vault, {}.{})",
731 kv_collection_identifier(self.collection)?,
732 kv_path_segment(key),
733 kv_collection_identifier(vault_collection)?,
734 kv_path_segment(vault_key)
735 );
736 append_tag_clause(&mut sql, tags);
737 self.db.query(&sql).await
738 }
739
740 pub async fn get(&self, key: &str) -> Result<QueryResult> {
741 self.db
742 .query(&format!(
743 "GET CONFIG {} {}",
744 kv_collection_identifier(self.collection)?,
745 kv_path_segment(key)
746 ))
747 .await
748 }
749
750 pub async fn resolve(&self, key: &str) -> Result<QueryResult> {
751 self.db
752 .query(&format!(
753 "RESOLVE CONFIG {} {}",
754 kv_collection_identifier(self.collection)?,
755 kv_path_segment(key)
756 ))
757 .await
758 }
759}
760
761#[derive(Debug)]
762pub struct VaultClient<'a> {
763 db: &'a Reddb,
764 collection: &'a str,
765}
766
767impl<'a> VaultClient<'a> {
768 pub async fn put(&self, key: &str, value: JsonValue, tags: &[&str]) -> Result<QueryResult> {
769 let mut sql = format!(
770 "VAULT PUT {}.{} = {}",
771 kv_collection_identifier(self.collection)?,
772 kv_path_segment(key),
773 json_value_literal(&value)
774 );
775 append_tag_clause(&mut sql, tags);
776 self.db.query(&sql).await
777 }
778
779 pub async fn get(&self, key: &str) -> Result<QueryResult> {
780 self.db
781 .query(&format!(
782 "VAULT GET {}.{}",
783 kv_collection_identifier(self.collection)?,
784 kv_path_segment(key)
785 ))
786 .await
787 }
788
789 pub async fn unseal(&self, key: &str) -> Result<QueryResult> {
790 self.db
791 .query(&format!(
792 "UNSEAL VAULT {}.{}",
793 kv_collection_identifier(self.collection)?,
794 kv_path_segment(key)
795 ))
796 .await
797 }
798}
799
800fn append_tag_clause(sql: &mut String, tags: &[&str]) {
801 if tags.is_empty() {
802 return;
803 }
804 sql.push_str(" TAGS [");
805 sql.push_str(
806 &tags
807 .iter()
808 .map(|tag| kv_tag_literal(tag))
809 .collect::<Vec<_>>()
810 .join(", "),
811 );
812 sql.push(']');
813}
814
815fn sql_identifier(value: &str) -> Result<String> {
816 let mut chars = value.chars();
817 let Some(first) = chars.next() else {
818 return Err(ClientError::new(
819 ErrorCode::InvalidArgument,
820 "identifier must not be empty",
821 ));
822 };
823 if !(first.is_ascii_alphabetic() || first == '_') {
824 return Err(ClientError::new(
825 ErrorCode::InvalidArgument,
826 format!("invalid identifier `{value}`"),
827 ));
828 }
829 if chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_') {
830 Ok(value.to_string())
831 } else {
832 Err(ClientError::new(
833 ErrorCode::InvalidArgument,
834 format!("invalid identifier `{value}`"),
835 ))
836 }
837}
838
839fn kv_collection_identifier(value: &str) -> Result<String> {
840 let mut parts = Vec::new();
841 for part in value.split('.') {
842 parts.push(sql_identifier(part)?);
843 }
844 Ok(parts.join("."))
845}
846
847fn kv_path_segment(value: &str) -> String {
848 if is_plain_path_segment(value) {
849 value.to_string()
850 } else {
851 sql_string_literal(value)
852 }
853}
854
855fn is_plain_path_segment(value: &str) -> bool {
856 let mut chars = value.chars();
857 let Some(first) = chars.next() else {
858 return false;
859 };
860 (first.is_ascii_alphabetic() || first == '_')
861 && chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
862}
863
864fn json_value_literal(value: &JsonValue) -> String {
865 match value {
866 JsonValue::Null => "NULL".to_string(),
867 JsonValue::Bool(value) => value.to_string(),
868 JsonValue::Number(value) => value.to_string(),
869 JsonValue::String(value) => sql_string_literal(value),
870 JsonValue::Array(_) | JsonValue::Object(_) => value.to_json_string(),
871 }
872}
873
874fn json_text_literal(value: &JsonValue) -> String {
875 sql_string_literal(&value.to_json_string())
876}
877
878fn kv_tag_literal(value: &str) -> String {
879 sql_string_literal(value)
880}
881
882fn sql_string_literal(value: &str) -> String {
883 format!("'{}'", value.replace('\'', "''"))
884}
885
886fn ensure_json_object(name: &str, value: &JsonValue) -> Result<()> {
887 if value.as_object().is_some() {
888 Ok(())
889 } else {
890 Err(ClientError::new(
891 ErrorCode::InvalidArgument,
892 format!("{name} must be a JSON object"),
893 ))
894 }
895}
896
897fn select_sql(collection: &str, columns: &str, options: &ListOptions<'_>) -> String {
898 let mut sql = format!("SELECT {columns} FROM {collection}");
899 if let Some(filter) = options.filter {
900 sql.push_str(" WHERE ");
901 sql.push_str(filter);
902 }
903 if let Some(order_by) = options.order_by {
904 sql.push_str(" ORDER BY ");
905 sql.push_str(order_by);
906 }
907 if let Some(limit) = options.limit {
908 sql.push_str(&format!(" LIMIT {limit}"));
909 }
910 sql
911}
912
913fn document_item_from_first_row(result: QueryResult) -> Result<DocumentItem> {
914 let Some(row) = result.rows.into_iter().next() else {
915 return Err(ClientError::new(ErrorCode::NotFound, "document not found"));
916 };
917 let rid = row
918 .iter()
919 .find(|(column, _)| column == "rid")
920 .and_then(|(_, value)| value_as_string(value))
921 .ok_or_else(|| ClientError::new(ErrorCode::InvalidResponse, "document row missing rid"))?;
922 Ok(DocumentItem { rid, fields: row })
923}
924
925fn kv_item_from_rows(rows: &[Row]) -> Option<KvItem> {
926 let row = rows.first()?;
927 let value = row
928 .iter()
929 .find(|(column, _)| column == "value")
930 .map(|(_, value)| value.clone())?;
931 let rid = row
932 .iter()
933 .find(|(column, _)| column == "rid")
934 .map(|(_, value)| value);
935 if matches!(rid, Some(ValueOut::Null)) && value == ValueOut::Null {
936 return None;
937 }
938 let collection = row
939 .iter()
940 .find(|(column, _)| column == "collection")
941 .and_then(|(_, value)| value_as_string(value))
942 .unwrap_or_default();
943 let key = row
944 .iter()
945 .find(|(column, _)| column == "key")
946 .and_then(|(_, value)| value_as_string(value))
947 .unwrap_or_default();
948 Some(KvItem {
949 collection,
950 key,
951 value,
952 })
953}
954
955fn row_value<'a>(rows: &'a [Row], column: &str) -> Option<&'a ValueOut> {
956 rows.first()?
957 .iter()
958 .find(|(name, _)| name == column)
959 .map(|(_, value)| value)
960}
961
962fn value_as_string(value: &ValueOut) -> Option<String> {
963 match value {
964 ValueOut::String(value) => Some(value.clone()),
965 ValueOut::Integer(value) => Some(value.to_string()),
966 _ => None,
967 }
968}
969
970fn value_as_u64(value: &ValueOut) -> Option<u64> {
971 match value {
972 ValueOut::Integer(value) => (*value).try_into().ok(),
973 ValueOut::Float(value) if *value >= 0.0 => Some(*value as u64),
974 ValueOut::String(value) => value.parse().ok(),
975 _ => None,
976 }
977}
978
979pub fn version() -> &'static str {
981 env!("CARGO_PKG_VERSION")
982}
983
984pub const HELPER_SPEC_VERSION: &str = "1.0";
991
992#[cfg(test)]
993mod helper_spec_tests {
994 use super::HELPER_SPEC_VERSION;
995
996 #[test]
997 fn helper_spec_version_is_pinned() {
998 assert_eq!(HELPER_SPEC_VERSION, "1.0");
999 }
1000}