google_cloud_bigquery/write/
pending.rs1use super::base::BaseWriter;
16use super::error::CommitError;
17use super::format::DataFormat;
18use crate::Result;
19use crate::model::{BatchCommitWriteStreamsResponse, FinalizeWriteStreamResponse};
20use crate::write::builder::AppendWithOffset;
21use crate::write::transport::Transport;
22use std::sync::Arc;
23
24#[derive(Debug)]
33pub struct PendingWriter<F> {
34 pub(crate) inner: BaseWriter<F>,
35}
36
37impl<F> PendingWriter<F>
38where
39 F: DataFormat,
40{
41 pub(crate) fn new(inner: Arc<Transport>, write_stream: String, format: F) -> Self {
42 Self {
43 inner: BaseWriter::new(inner, write_stream, format),
44 }
45 }
46
47 pub fn write_stream(&self) -> &str {
49 &self.inner.write_stream
50 }
51
52 pub fn append(&self, rows: F::Rows) -> AppendWithOffset {
54 AppendWithOffset::new(
55 self.inner.runner.req_tx.clone(),
56 self.inner.append_request(rows),
57 )
58 }
59
60 pub async fn finalize(&self) -> Result<FinalizeWriteStreamResponse> {
62 self.inner.finalize().await
63 }
64
65 pub async fn commit(
70 &self,
71 ) -> std::result::Result<BatchCommitWriteStreamsResponse, CommitError> {
72 let parent = self
75 .inner
76 .write_stream
77 .split_once("/streams/")
78 .map_or(self.inner.write_stream.as_str(), |(p, _)| p)
79 .to_string();
80
81 let resp = self
82 .inner
83 .client
84 .batch_commit_write_streams()
85 .set_parent(parent)
86 .set_write_streams(vec![self.inner.write_stream.clone()])
87 .send()
88 .await?;
89
90 if !resp.stream_errors.is_empty() || resp.commit_time.is_none() {
91 return Err(CommitError::FailedTransaction {
92 stream_errors: resp.stream_errors,
93 });
94 }
95
96 Ok(resp)
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::model::storage_error::StorageErrorCode;
104 use crate::write::test::*;
105 use bigquery_grpc_mock::google::cloud::bigquery::storage::v1;
106 use bigquery_grpc_mock::{MockBigQueryWrite, start};
107 use gaxi::grpc::tonic::{Response as TonicResponse, Status as TonicStatus};
108 use google_cloud_gax::error::rpc::Code;
109 use tokio::sync::mpsc;
110
111 #[tokio::test]
112 async fn basic_success() -> anyhow::Result<()> {
113 let (response_tx, response_rx) = mpsc::channel(10);
114
115 let mut mock = MockBigQueryWrite::new();
116 mock.expect_append_rows()
117 .return_once(|_| Ok(TonicResponse::from(response_rx)));
118
119 mock.expect_finalize_write_stream().return_once(|_| {
120 Ok(TonicResponse::new(
121 v1::FinalizeWriteStreamResponse::default(),
122 ))
123 });
124
125 mock.expect_batch_commit_write_streams().return_once(|_| {
126 Ok(TonicResponse::new(v1::BatchCommitWriteStreamsResponse {
127 commit_time: Some(prost_types::Timestamp {
128 seconds: 1_700_000_000,
129 nanos: 0,
130 }),
131 stream_errors: Vec::new(),
132 }))
133 });
134
135 let (endpoint, _server) = start("0.0.0.0:0", mock).await?;
136 let transport = Arc::new(test_transport(endpoint).await?);
137
138 let writer = PendingWriter::new(transport, write_stream(), format());
139 assert_eq!(writer.write_stream(), write_stream());
140
141 response_tx.send(Ok(convert(&test_response(1)))).await?;
142 let resp = writer.append(rows(1)).send().await?;
143 assert_eq!(resp.offset, Some(1));
144
145 writer.finalize().await?;
146 let commit_resp = writer.commit().await?;
147 assert!(commit_resp.commit_time.is_some());
148
149 Ok(())
150 }
151
152 #[tokio::test]
153 async fn commit_stream_errors() -> anyhow::Result<()> {
154 let mut mock = MockBigQueryWrite::new();
155 mock.expect_batch_commit_write_streams().return_once(|_| {
156 Ok(TonicResponse::new(v1::BatchCommitWriteStreamsResponse {
157 commit_time: None,
158 stream_errors: vec![v1::StorageError {
159 code: v1::storage_error::StorageErrorCode::InvalidStreamState as i32,
160 entity: write_stream(),
161 error_message: "stream is not finalized".to_string(),
162 }],
163 }))
164 });
165
166 let (endpoint, _server) = start("0.0.0.0:0", mock).await?;
167 let transport = Arc::new(test_transport(endpoint).await?);
168 let writer = PendingWriter::new(transport, write_stream(), format());
169
170 let err = writer.commit().await.expect_err("commit should fail");
171 let CommitError::FailedTransaction { stream_errors } = err else {
172 anyhow::bail!("expected FailedTransaction, got: {err:?}");
173 };
174 assert_eq!(stream_errors.len(), 1);
175 assert_eq!(stream_errors[0].code, StorageErrorCode::InvalidStreamState);
176 assert_eq!(stream_errors[0].entity, write_stream());
177 assert_eq!(stream_errors[0].error_message, "stream is not finalized");
178
179 Ok(())
180 }
181
182 #[tokio::test]
183 async fn commit_missing_commit_time() -> anyhow::Result<()> {
184 let mut mock = MockBigQueryWrite::new();
185 mock.expect_batch_commit_write_streams().return_once(|_| {
186 Ok(TonicResponse::new(
187 v1::BatchCommitWriteStreamsResponse::default(),
188 ))
189 });
190
191 let (endpoint, _server) = start("0.0.0.0:0", mock).await?;
192 let transport = Arc::new(test_transport(endpoint).await?);
193 let writer = PendingWriter::new(transport, write_stream(), format());
194
195 let err = writer.commit().await.expect_err("commit should fail");
196 let CommitError::FailedTransaction { stream_errors } = err else {
197 anyhow::bail!("expected FailedTransaction, got: {err:?}");
198 };
199 assert!(stream_errors.is_empty());
200
201 Ok(())
202 }
203
204 #[tokio::test]
205 async fn commit_rpc_error() -> anyhow::Result<()> {
206 let mut mock = MockBigQueryWrite::new();
207 mock.expect_batch_commit_write_streams()
208 .return_once(|_| Err(TonicStatus::failed_precondition("fail")));
209
210 let (endpoint, _server) = start("0.0.0.0:0", mock).await?;
211 let transport = Arc::new(test_transport(endpoint).await?);
212 let writer = PendingWriter::new(transport, write_stream(), format());
213
214 let err = writer.commit().await.expect_err("commit should fail");
215 let CommitError::Rpc { source } = err else {
216 anyhow::bail!("expected Rpc error, got: {err:?}");
217 };
218 let status = source.status().expect("error should have status");
219 assert_eq!(status.code, Code::FailedPrecondition);
220 assert_eq!(status.message, "fail");
221
222 Ok(())
223 }
224}