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