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