1use std::time::Duration;
9
10use bytes::Bytes;
11
12use crate::connection::Connection;
13use crate::error::{Error, Result};
14use crate::guid::Guid;
15use crate::proto;
16use crate::wire::{self, MaybeRow, Row};
17
18pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
20
21pub const DEFAULT_TRANSACTION_TIMEOUT: Duration = Duration::from_secs(15);
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27#[repr(i32)]
28pub enum TransactionType {
29 Master = 0,
30 Tablet = 1,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[repr(i32)]
36pub enum Atomicity {
37 Full = 0,
38 None = 1,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45#[repr(i32)]
46pub enum RowModificationType {
47 Write = 0,
48 Delete = 1,
49 WriteAndLock = 3,
51}
52
53pub type Timestamp = u64;
55
56pub const LATEST_TIMESTAMP: Timestamp = 0x3fff_ffff_ffff_ff01;
62
63#[derive(Debug)]
68pub struct Client {
69 connection: Connection,
70 timeout: Duration,
71}
72
73impl Client {
74 pub async fn connect(address: &str) -> Result<Self> {
78 Self::builder(address).connect().await
79 }
80
81 pub fn builder(address: &str) -> ClientBuilder {
83 ClientBuilder {
84 address: address.to_owned(),
85 token: None,
86 timeout: DEFAULT_TIMEOUT,
87 }
88 }
89
90 pub fn connection(&self) -> &Connection {
93 &self.connection
94 }
95
96 pub async fn discover_proxies(&self, role: Option<&str>) -> Result<Vec<String>> {
98 crate::connection::discover_proxies(&self.connection, role, Some(self.timeout)).await
99 }
100
101 pub async fn start_transaction(
108 &self,
109 transaction_type: TransactionType,
110 options: StartTransactionOptions,
111 ) -> Result<Transaction<'_>> {
112 let request = start_transaction_request(transaction_type, &options);
113
114 let (response, _) = self
115 .connection
116 .invoke::<proto::api::TRspStartTransaction>(
117 "StartTransaction",
118 &request,
119 Vec::new(),
120 Some(self.timeout),
121 "TRspStartTransaction",
122 )
123 .await?;
124
125 Ok(Transaction {
126 client: self,
127 id: Guid::from_proto(&response.id),
128 start_timestamp: response.start_timestamp,
129 finished: false,
130 })
131 }
132
133 pub async fn lookup_rows(
140 &self,
141 path: &str,
142 columns: &[&str],
143 keys: &[Row],
144 options: LookupOptions<'_>,
145 ) -> Result<Vec<MaybeRow>> {
146 let keys: Vec<MaybeRow> = keys.iter().cloned().map(Some).collect();
147 let request = lookup_request(path, columns, &options);
148
149 let (response, attachments) = self
150 .connection
151 .invoke::<proto::api::TRspLookupRows>(
152 "LookupRows",
153 &request,
154 vec![wire::encode_rowset(&keys)?],
155 Some(self.timeout),
156 "TRspLookupRows",
157 )
158 .await?;
159
160 decode_rowset_attachments(&attachments, Some(&response.rowset_descriptor))
161 }
162
163 pub async fn lookup_rows_with_columns(
169 &self,
170 path: &str,
171 columns: &[&str],
172 keys: &[Row],
173 options: LookupOptions<'_>,
174 ) -> Result<(Vec<MaybeRow>, Vec<String>)> {
175 let keys: Vec<MaybeRow> = keys.iter().cloned().map(Some).collect();
176 let request = lookup_request(path, columns, &options);
177
178 let (response, attachments) = self
179 .connection
180 .invoke::<proto::api::TRspLookupRows>(
181 "LookupRows",
182 &request,
183 vec![wire::encode_rowset(&keys)?],
184 Some(self.timeout),
185 "TRspLookupRows",
186 )
187 .await?;
188
189 let descriptor = &response.rowset_descriptor;
190 let rows = decode_rowset_attachments(&attachments, Some(descriptor))?;
191 Ok((rows, descriptor_column_names(descriptor)))
192 }
193
194 pub async fn select_rows(&self, query: &str, options: SelectOptions) -> Result<Vec<MaybeRow>> {
200 Ok(self.select_rows_with_columns(query, options).await?.0)
201 }
202
203 pub async fn select_rows_with_columns(
205 &self,
206 query: &str,
207 options: SelectOptions,
208 ) -> Result<(Vec<MaybeRow>, Vec<String>)> {
209 let request = select_request(query, &options);
210
211 let (response, attachments) = self
212 .connection
213 .invoke::<proto::api::TRspSelectRows>(
214 "SelectRows",
215 &request,
216 Vec::new(),
217 Some(self.timeout),
218 "TRspSelectRows",
219 )
220 .await?;
221
222 let descriptor = &response.rowset_descriptor;
223 let rows = decode_rowset_attachments(&attachments, Some(descriptor))?;
224 Ok((rows, descriptor_column_names(descriptor)))
225 }
226}
227
228#[derive(Debug, Clone)]
230pub struct ClientBuilder {
231 address: String,
232 token: Option<String>,
233 timeout: Duration,
234}
235
236impl ClientBuilder {
237 pub fn token(mut self, token: impl Into<String>) -> Self {
241 self.token = Some(token.into());
242 self
243 }
244
245 pub fn timeout(mut self, timeout: Duration) -> Self {
247 self.timeout = timeout;
248 self
249 }
250
251 pub async fn connect(self) -> Result<Client> {
252 let connection = Connection::connect(&self.address, self.token).await?;
253 Ok(Client {
254 connection,
255 timeout: self.timeout,
256 })
257 }
258}
259
260#[derive(Debug, Clone)]
262pub struct StartTransactionOptions {
263 pub timeout: Duration,
265 pub atomicity: Atomicity,
266 pub parent_id: Option<Guid>,
267}
268
269impl Default for StartTransactionOptions {
270 fn default() -> Self {
271 Self {
272 timeout: DEFAULT_TRANSACTION_TIMEOUT,
273 atomicity: Atomicity::Full,
274 parent_id: None,
275 }
276 }
277}
278
279#[derive(Debug, Clone, Default)]
281pub struct LookupOptions<'a> {
282 pub timestamp: Option<Timestamp>,
286 pub column_filter: Vec<&'a str>,
288}
289
290#[derive(Debug, Clone, Default)]
292pub struct SelectOptions {
293 pub timestamp: Option<Timestamp>,
295 pub output_row_limit: Option<u64>,
301}
302
303#[derive(Debug)]
310pub struct Transaction<'a> {
311 client: &'a Client,
312 id: Guid,
313 start_timestamp: Timestamp,
314 finished: bool,
315}
316
317impl Transaction<'_> {
318 pub fn id(&self) -> Guid {
319 self.id
320 }
321
322 pub fn start_timestamp(&self) -> Timestamp {
328 self.start_timestamp
329 }
330
331 pub async fn ping(&self) -> Result<()> {
336 let request = proto::api::TReqPingTransaction {
337 transaction_id: self.id.to_proto(),
338 ..Default::default()
339 };
340 self.client
341 .connection
342 .invoke::<proto::api::TRspPingTransaction>(
343 "PingTransaction",
344 &request,
345 Vec::new(),
346 Some(self.client.timeout),
347 "TRspPingTransaction",
348 )
349 .await?;
350 Ok(())
351 }
352
353 pub async fn commit(mut self) -> Result<()> {
355 let request = proto::api::TReqCommitTransaction {
356 transaction_id: self.id.to_proto(),
357 ..Default::default()
358 };
359 self.client
360 .connection
361 .invoke::<proto::api::TRspCommitTransaction>(
362 "CommitTransaction",
363 &request,
364 Vec::new(),
365 Some(self.client.timeout),
366 "TRspCommitTransaction",
367 )
368 .await?;
369 self.finished = true;
370 Ok(())
371 }
372
373 pub async fn abort(mut self) -> Result<()> {
375 let request = proto::api::TReqAbortTransaction {
376 transaction_id: self.id.to_proto(),
377 ..Default::default()
378 };
379 self.client
380 .connection
381 .invoke::<proto::api::TRspAbortTransaction>(
382 "AbortTransaction",
383 &request,
384 Vec::new(),
385 Some(self.client.timeout),
386 "TRspAbortTransaction",
387 )
388 .await?;
389 self.finished = true;
390 Ok(())
391 }
392
393 pub fn is_finished(&self) -> bool {
395 self.finished
396 }
397
398 pub async fn lookup_rows(
400 &self,
401 path: &str,
402 columns: &[&str],
403 keys: &[Row],
404 mut options: LookupOptions<'_>,
405 ) -> Result<Vec<MaybeRow>> {
406 options.timestamp = Some(self.start_timestamp);
407 self.client.lookup_rows(path, columns, keys, options).await
408 }
409
410 pub async fn lookup_rows_with_columns(
412 &self,
413 path: &str,
414 columns: &[&str],
415 keys: &[Row],
416 mut options: LookupOptions<'_>,
417 ) -> Result<(Vec<MaybeRow>, Vec<String>)> {
418 options.timestamp = Some(self.start_timestamp);
419 self.client
420 .lookup_rows_with_columns(path, columns, keys, options)
421 .await
422 }
423
424 pub async fn select_rows_with_columns(
426 &self,
427 query: &str,
428 mut options: SelectOptions,
429 ) -> Result<(Vec<MaybeRow>, Vec<String>)> {
430 options.timestamp = Some(self.start_timestamp);
431 self.client.select_rows_with_columns(query, options).await
432 }
433
434 pub async fn select_rows(
436 &self,
437 query: &str,
438 mut options: SelectOptions,
439 ) -> Result<Vec<MaybeRow>> {
440 options.timestamp = Some(self.start_timestamp);
441 self.client.select_rows(query, options).await
442 }
443
444 pub async fn insert_rows(&self, path: &str, columns: &[&str], rows: &[Row]) -> Result<()> {
446 self.modify_rows(path, columns, rows, RowModificationType::Write)
447 .await
448 }
449
450 pub async fn delete_rows(&self, path: &str, columns: &[&str], keys: &[Row]) -> Result<()> {
452 self.modify_rows(path, columns, keys, RowModificationType::Delete)
453 .await
454 }
455
456 pub async fn modify_rows(
462 &self,
463 path: &str,
464 columns: &[&str],
465 rows: &[Row],
466 modification: RowModificationType,
467 ) -> Result<()> {
468 let owned: Vec<MaybeRow> = rows.iter().cloned().map(Some).collect();
469 let request = modify_request(self.id, path, columns, rows.len(), modification);
470
471 self.client
472 .connection
473 .invoke::<proto::api::TRspModifyRows>(
474 "ModifyRows",
475 &request,
476 vec![wire::encode_rowset(&owned)?],
477 Some(self.client.timeout),
478 "TRspModifyRows",
479 )
480 .await?;
481 Ok(())
482 }
483}
484
485fn start_transaction_request(
491 transaction_type: TransactionType,
492 options: &StartTransactionOptions,
493) -> proto::api::TReqStartTransaction {
494 proto::api::TReqStartTransaction {
495 r#type: transaction_type as i32,
496 timeout: Some(options.timeout.as_micros() as i64),
497 sticky: Some(transaction_type == TransactionType::Tablet),
500 atomicity: Some(options.atomicity as i32),
501 parent_id: options.parent_id.map(Guid::to_proto),
502 ..Default::default()
503 }
504}
505
506fn lookup_request(
508 path: &str,
509 columns: &[&str],
510 options: &LookupOptions<'_>,
511) -> proto::api::TReqLookupRows {
512 proto::api::TReqLookupRows {
513 path: path.as_bytes().to_vec(),
515 rowset_descriptor: name_table_descriptor(columns),
516 timestamp: options.timestamp,
517 keep_missing_rows: Some(true),
521 columns: options
522 .column_filter
523 .iter()
524 .map(|column| (*column).to_owned())
525 .collect(),
526 ..Default::default()
527 }
528}
529
530fn select_request(query: &str, options: &SelectOptions) -> proto::api::TReqSelectRows {
532 proto::api::TReqSelectRows {
533 query: query.to_owned(),
534 timestamp: options.timestamp,
535 output_row_limit: options.output_row_limit,
536 ..Default::default()
537 }
538}
539
540fn modify_request(
542 transaction_id: Guid,
543 path: &str,
544 columns: &[&str],
545 row_count: usize,
546 modification: RowModificationType,
547) -> proto::api::TReqModifyRows {
548 proto::api::TReqModifyRows {
549 transaction_id: transaction_id.to_proto(),
550 path: path.as_bytes().to_vec(),
551 rowset_descriptor: name_table_descriptor(columns),
552 row_modification_types: vec![modification as i32; row_count],
555 ..Default::default()
556 }
557}
558
559fn descriptor_column_names(descriptor: &proto::api::TRowsetDescriptor) -> Vec<String> {
561 descriptor
562 .name_table_entries
563 .iter()
564 .map(|entry| entry.name.clone().unwrap_or_default())
565 .collect()
566}
567
568fn name_table_descriptor(columns: &[&str]) -> proto::api::TRowsetDescriptor {
572 proto::api::TRowsetDescriptor {
573 wire_format_version: Some(CURRENT_WIRE_FORMAT_VERSION),
574 rowset_kind: Some(proto::api::ERowsetKind::RkUnversioned as i32),
575 name_table_entries: columns
576 .iter()
577 .map(|name| proto::api::t_rowset_descriptor::TNameTableEntry {
578 name: Some((*name).to_owned()),
579 ..Default::default()
580 })
581 .collect(),
582 ..Default::default()
583 }
584}
585
586const CURRENT_WIRE_FORMAT_VERSION: i32 = 1;
588
589fn decode_rowset_attachments(
595 attachments: &[Bytes],
596 descriptor: Option<&proto::api::TRowsetDescriptor>,
597) -> Result<Vec<MaybeRow>> {
598 if let Some(descriptor) = descriptor
599 && let Some(version) = descriptor.wire_format_version
600 && version != CURRENT_WIRE_FORMAT_VERSION
601 {
602 return Err(Error::Protocol(format!(
603 "the proxy replied with wire format version {version}, and this client speaks {CURRENT_WIRE_FORMAT_VERSION}"
604 )));
605 }
606
607 let merged = match attachments {
608 [] => return Ok(Vec::new()),
609 [single] => single.clone(),
610 many => {
611 let mut merged =
612 bytes::BytesMut::with_capacity(many.iter().map(Bytes::len).sum::<usize>());
613 for attachment in many {
614 merged.extend_from_slice(attachment);
615 }
616 merged.freeze()
617 }
618 };
619
620 Ok(wire::decode_rowset(&merged)?)
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626 use crate::wire::{UnversionedValue, Value};
627 use prost::Message as _;
628
629 #[test]
633 fn enum_values_match_the_proto() {
634 assert_eq!(
635 RowModificationType::Write as i32,
636 proto::api::ERowModificationType::RmtWrite as i32
637 );
638 assert_eq!(
639 RowModificationType::Delete as i32,
640 proto::api::ERowModificationType::RmtDelete as i32
641 );
642 assert_eq!(
643 RowModificationType::WriteAndLock as i32,
644 proto::api::ERowModificationType::RmtModify as i32
645 );
646 assert_eq!(
647 TransactionType::Master as i32,
648 proto::api::ETransactionType::TtMaster as i32
649 );
650 assert_eq!(
651 TransactionType::Tablet as i32,
652 proto::api::ETransactionType::TtTablet as i32
653 );
654 assert_eq!(Atomicity::Full as i32, proto::api::EAtomicity::AFull as i32);
655 assert_eq!(Atomicity::None as i32, proto::api::EAtomicity::ANone as i32);
656 }
657
658 #[test]
659 fn enum_values_match_the_documented_numbers() {
660 assert_eq!(TransactionType::Master as i32, 0);
661 assert_eq!(TransactionType::Tablet as i32, 1);
662 assert_eq!(Atomicity::Full as i32, 0);
663 assert_eq!(Atomicity::None as i32, 1);
664 assert_eq!(RowModificationType::Write as i32, 0);
665 assert_eq!(RowModificationType::Delete as i32, 1);
666 assert_eq!(RowModificationType::WriteAndLock as i32, 3);
668 }
669
670 #[test]
676 fn the_latest_timestamp_sentinel_is_the_proto_default() {
677 assert_ne!(LATEST_TIMESTAMP, 0, "zero is NullTimestamp, not 'latest'");
678
679 let mut buffer = Vec::new();
684 proto::api::TReqLookupRows {
685 path: b"//tmp/t".to_vec(),
686 timestamp: None,
687 ..Default::default()
688 }
689 .encode(&mut buffer)
690 .unwrap();
691 let parsed = proto::api::TReqLookupRows::decode(&buffer[..]).unwrap();
692 assert_eq!(
693 parsed.timestamp.unwrap_or(LATEST_TIMESTAMP),
694 LATEST_TIMESTAMP
695 );
696 assert_eq!(LATEST_TIMESTAMP, 0x3fff_ffff_ffff_ff01);
697 }
698
699 #[test]
705 fn lookup_asks_for_what_it_promises() {
706 let request = lookup_request(
707 "//tmp/table",
708 &["key"],
709 &LookupOptions {
710 timestamp: None,
711 column_filter: vec!["key", "value"],
712 },
713 );
714
715 assert_eq!(request.path, b"//tmp/table".to_vec());
717 assert_eq!(request.columns, ["key", "value"]);
718 assert_eq!(
719 request.keep_missing_rows,
720 Some(true),
721 "without this a missing key shortens the answer and misaligns the rest"
722 );
723 assert_eq!(
724 request.timestamp, None,
725 "omitted means the proto default, which is the latest committed data; sending 0 would ask for NullTimestamp instead"
726 );
727 assert_eq!(
728 request
729 .rowset_descriptor
730 .name_table_entries
731 .iter()
732 .map(|entry| entry.name.clone().unwrap())
733 .collect::<Vec<_>>(),
734 ["key"],
735 "the descriptor names the key columns the attachment carries"
736 );
737 }
738
739 #[test]
740 fn a_lookup_in_a_transaction_reads_at_its_start_timestamp() {
741 let request = lookup_request(
744 "//tmp/table",
745 &["key"],
746 &LookupOptions {
747 timestamp: Some(1234),
748 column_filter: Vec::new(),
749 },
750 );
751 assert_eq!(request.timestamp, Some(1234));
752 assert!(
753 request.columns.is_empty(),
754 "an empty filter means every column"
755 );
756 }
757
758 #[test]
759 fn select_carries_the_query_timestamp_and_output_limit() {
760 let request = select_request(
761 "* from [//tmp/t]",
762 &SelectOptions {
763 timestamp: Some(99),
764 output_row_limit: Some(10),
765 },
766 );
767 assert_eq!(request.query, "* from [//tmp/t]");
768 assert_eq!(request.timestamp, Some(99));
769 assert_eq!(request.output_row_limit, Some(10));
770 }
771
772 #[test]
773 fn modify_names_the_transaction_and_one_type_per_row() {
774 let transaction = Guid::random();
775 let request = modify_request(
776 transaction,
777 "//tmp/table",
778 &["key", "value"],
779 3,
780 RowModificationType::Delete,
781 );
782
783 assert_eq!(Guid::from_proto(&request.transaction_id), transaction);
784 assert_eq!(request.path, b"//tmp/table".to_vec());
785 assert_eq!(
786 request.row_modification_types,
787 vec![RowModificationType::Delete as i32; 3],
788 "one entry per row, parallel to the rows in the attachment"
789 );
790 assert!(
791 request.row_legacy_read_locks.is_empty()
792 && request.row_legacy_locks.is_empty()
793 && request.row_locks.is_empty(),
794 "the lock arrays are all-or-nothing per request; a partially filled one breaks the server's one-per-row invariant"
795 );
796 }
797
798 #[test]
799 fn only_a_tablet_transaction_is_sticky() {
800 let options = StartTransactionOptions::default();
801 let tablet = start_transaction_request(TransactionType::Tablet, &options);
802 assert_eq!(tablet.r#type, 1);
803 assert_eq!(
804 tablet.sticky,
805 Some(true),
806 "a tablet tx belongs to one proxy"
807 );
808 assert_eq!(
809 tablet.timeout,
810 Some(options.timeout.as_micros() as i64),
811 "microseconds, not milliseconds"
812 );
813
814 let master = start_transaction_request(TransactionType::Master, &options);
815 assert_eq!(master.r#type, 0);
816 assert_eq!(master.sticky, Some(false));
817 }
818
819 #[test]
820 fn the_descriptor_numbers_columns_in_order() {
821 let descriptor = name_table_descriptor(&["key", "value", "extra"]);
822 assert_eq!(descriptor.wire_format_version, Some(1));
823 assert_eq!(
824 descriptor.rowset_kind,
825 Some(proto::api::ERowsetKind::RkUnversioned as i32)
826 );
827 let names: Vec<_> = descriptor
828 .name_table_entries
829 .iter()
830 .map(|entry| entry.name.clone().unwrap())
831 .collect();
832 assert_eq!(names, ["key", "value", "extra"]);
833 }
834
835 #[test]
836 fn attachments_are_concatenated_before_decoding() {
837 let rows = vec![
840 Some(vec![UnversionedValue::new(0, Value::Int64(1))]),
841 Some(vec![UnversionedValue::new(0, Value::Int64(2))]),
842 ];
843 let encoded = wire::encode_rowset(&rows).unwrap();
844 let split = encoded.len() / 2;
845 let attachments = vec![encoded.slice(0..split), encoded.slice(split..)];
846
847 let decoded = decode_rowset_attachments(&attachments, None).unwrap();
848 assert_eq!(decoded, rows);
849 }
850
851 #[test]
852 fn no_attachments_means_no_rows() {
853 assert_eq!(decode_rowset_attachments(&[], None).unwrap(), Vec::new());
854 }
855
856 #[test]
857 fn an_unknown_wire_format_version_is_refused() {
858 let descriptor = proto::api::TRowsetDescriptor {
859 wire_format_version: Some(99),
860 ..Default::default()
861 };
862 let error = decode_rowset_attachments(&[], Some(&descriptor)).unwrap_err();
863 assert!(
864 error.to_string().contains("wire format version 99"),
865 "unexpected error: {error}"
866 );
867 }
868}