google_cloud_bigquery/write/arrow/
writer_builder.rs1use super::super::generated::gapic_storage::client::BigQueryWrite;
16use super::super::transport::Transport;
17use super::{BufferedWriter, CommittedWriter, DefaultWriter, PendingWriter, Writer};
18use crate::model::write_stream::Type;
19use crate::model::{ArrowSchema, WriteStream};
20use crate::write::error::{AttachError, AttachResult};
21use crate::{Error, Result};
22use gaxi::path_parameter::{PathMismatchBuilder, try_match};
23use gaxi::routing_parameter::Segment;
24use google_cloud_gax::error::binding::BindingError;
25use std::sync::Arc;
26
27#[derive(Clone, Debug)]
29pub struct WriterBuilder {
30 inner: Arc<Transport>,
31 schema: ArrowSchema,
32}
33
34impl WriterBuilder {
35 pub(crate) fn new(inner: Arc<Transport>, schema: ArrowSchema) -> Self {
36 Self { inner, schema }
37 }
38
39 pub fn default<T: Into<String>>(self, table: T) -> Result<DefaultWriter> {
59 let table = table.into();
60 validate_table(table.as_str())?;
61 let mut write_stream = table;
62 write_stream.push_str("/streams/_default");
63 Ok(DefaultWriter::new(self.inner, write_stream, self.schema))
64 }
65
66 pub async fn pending<T: Into<String>>(self, table: T) -> Result<PendingWriter> {
85 let table = table.into();
86 validate_table(table.as_str())?;
87
88 let client = BigQueryWrite::from_stub::<Transport>(self.inner.clone());
89 let write_stream = client
90 .create_write_stream()
91 .set_parent(table)
92 .set_write_stream(WriteStream::new().set_type(Type::Pending))
93 .send()
94 .await?;
95
96 Ok(PendingWriter::new(
97 self.inner,
98 write_stream.name,
99 self.schema,
100 ))
101 }
102
103 pub async fn committed<T: Into<String>>(self, table: T) -> Result<CommittedWriter> {
122 let table = table.into();
123 validate_table(table.as_str())?;
124
125 let client = BigQueryWrite::from_stub::<Transport>(self.inner.clone());
126 let write_stream = client
127 .create_write_stream()
128 .set_parent(table)
129 .set_write_stream(WriteStream::new().set_type(Type::Committed))
130 .send()
131 .await?;
132
133 Ok(CommittedWriter::new(
134 self.inner,
135 write_stream.name,
136 self.schema,
137 ))
138 }
139
140 pub async fn buffered<T: Into<String>>(self, table: T) -> Result<BufferedWriter> {
159 let table = table.into();
160 validate_table(table.as_str())?;
161
162 let client = BigQueryWrite::from_stub::<Transport>(self.inner.clone());
163 let write_stream = client
164 .create_write_stream()
165 .set_parent(table)
166 .set_write_stream(WriteStream::new().set_type(Type::Buffered))
167 .send()
168 .await?;
169
170 Ok(BufferedWriter::new(
171 self.inner,
172 write_stream.name,
173 self.schema,
174 ))
175 }
176
177 pub async fn attach<U: Writer, S: Into<String>>(self, write_stream: S) -> AttachResult<U> {
197 let write_stream = write_stream.into();
198 validate_stream(write_stream.as_str())?;
199
200 let client = BigQueryWrite::from_stub::<Transport>(self.inner.clone());
201 let stream = client
202 .get_write_stream()
203 .set_name(&write_stream)
204 .send()
205 .await?;
206
207 let stream_type = stream.r#type.clone();
208 if stream_type != U::STREAM_TYPE {
209 return Err(AttachError::TypeMismatch {
210 expected: U::STREAM_TYPE,
211 actual: stream_type,
212 });
213 }
214 Ok(U::build(self.inner, write_stream, self.schema))
215 }
216}
217
218fn validate_table(table: &str) -> Result<()> {
219 let segments = &[
220 Segment::Literal("projects/"),
221 Segment::SingleWildcard,
222 Segment::Literal("/datasets/"),
223 Segment::SingleWildcard,
224 Segment::Literal("/tables/"),
225 Segment::SingleWildcard,
226 ];
227 try_match(Some(table), segments)
228 .ok_or_else(|| {
229 let builder = PathMismatchBuilder::default().maybe_add(
230 Some(table),
231 segments,
232 "table",
233 "projects/*/datasets/*/tables/*",
234 );
235 Error::binding(BindingError {
236 paths: vec![builder.build()],
237 })
238 })
239 .map(|_| ())
240}
241
242fn validate_stream(stream: &str) -> crate::Result<()> {
243 let segments = &[
244 Segment::Literal("projects/"),
245 Segment::SingleWildcard,
246 Segment::Literal("/datasets/"),
247 Segment::SingleWildcard,
248 Segment::Literal("/tables/"),
249 Segment::SingleWildcard,
250 Segment::Literal("/streams/"),
251 Segment::SingleWildcard,
252 ];
253 try_match(Some(stream), segments)
254 .ok_or_else(|| {
255 let builder = gaxi::path_parameter::PathMismatchBuilder::default();
256 let builder = builder.maybe_add(
257 Some(stream),
258 segments,
259 "write_stream",
260 "projects/*/datasets/*/tables/*/streams/*",
261 );
262 Error::binding(BindingError {
263 paths: vec![builder.build()],
264 })
265 })
266 .map(|_| ())
267}
268
269#[cfg(test)]
270mod tests {
271 use super::super::super::transport::tests::test_transport;
272 use super::*;
273 use bigquery_grpc_mock::google::cloud::bigquery::storage::v1::WriteStream as MockWriteStream;
274 use bigquery_grpc_mock::{MockBigQueryWrite, start};
275 use test_case::test_case;
276 use tokio::task::JoinHandle;
277
278 #[tokio::test]
279 async fn pending_success() -> anyhow::Result<()> {
280 let mut mock = MockBigQueryWrite::new();
281 mock.expect_create_write_stream().return_once(|req| {
282 let req = req.into_inner();
283 assert_eq!(req.parent, "projects/p/datasets/d/tables/t");
284 let ws = req.write_stream.expect("write_stream populated");
285 assert_eq!(Type::from(ws.r#type), Type::Pending);
286 Ok(gaxi::grpc::tonic::Response::new(MockWriteStream {
287 name: "projects/p/datasets/d/tables/t/streams/s".to_string(),
288 ..Default::default()
289 }))
290 });
291 let (endpoint, _server) = start("0.0.0.0:0", mock).await?;
292 let transport = Arc::new(test_transport(endpoint).await?);
293 let schema = ArrowSchema::new().set_serialized_schema("test");
294 let builder = WriterBuilder::new(transport, schema.clone());
295 let writer = builder.pending("projects/p/datasets/d/tables/t").await?;
296 assert_eq!(
297 writer.inner.write_stream,
298 "projects/p/datasets/d/tables/t/streams/s"
299 );
300 assert_eq!(writer.inner.schema, schema);
301 Ok(())
302 }
303
304 #[test_case("projects/p")]
305 #[test_case("projects/p/tables/t")]
306 #[test_case("projects/p/datasets/d/tables/")]
307 #[tokio::test]
308 async fn pending_bad_table_format(table: &str) -> anyhow::Result<()> {
309 let transport = Arc::new(test_transport("http://ignored:1".to_string()).await?);
310 let schema = ArrowSchema::new().set_serialized_schema("test");
311 let builder = WriterBuilder::new(transport, schema.clone());
312 let err = builder
313 .pending(table)
314 .await
315 .expect_err("should fail locally on bad format");
316 assert!(err.is_binding(), "{err:?}");
317 Ok(())
318 }
319
320 #[tokio::test]
321 async fn committed_success() -> anyhow::Result<()> {
322 let mut mock = MockBigQueryWrite::new();
323 mock.expect_create_write_stream().return_once(|req| {
324 let req = req.into_inner();
325 assert_eq!(req.parent, "projects/p/datasets/d/tables/t");
326 let ws = req.write_stream.expect("write_stream populated");
327 assert_eq!(Type::from(ws.r#type), Type::Committed);
328 Ok(gaxi::grpc::tonic::Response::new(MockWriteStream {
329 name: "projects/p/datasets/d/tables/t/streams/s".to_string(),
330 ..Default::default()
331 }))
332 });
333 let (endpoint, _server) = start("0.0.0.0:0", mock).await?;
334 let transport = Arc::new(test_transport(endpoint).await?);
335 let schema = ArrowSchema::new().set_serialized_schema("test");
336 let builder = WriterBuilder::new(transport, schema.clone());
337 let writer = builder.committed("projects/p/datasets/d/tables/t").await?;
338 assert_eq!(
339 writer.inner.write_stream,
340 "projects/p/datasets/d/tables/t/streams/s"
341 );
342 assert_eq!(writer.inner.schema, schema);
343 Ok(())
344 }
345
346 #[test_case("projects/p")]
347 #[test_case("projects/p/tables/t")]
348 #[test_case("projects/p/datasets/d/tables/")]
349 #[tokio::test]
350 async fn committed_bad_table_format(table: &str) -> anyhow::Result<()> {
351 let transport = Arc::new(test_transport("http://ignored:1".to_string()).await?);
352 let schema = ArrowSchema::new().set_serialized_schema("test");
353 let builder = WriterBuilder::new(transport, schema.clone());
354 let err = builder
355 .committed(table)
356 .await
357 .expect_err("should fail locally on bad format");
358 assert!(err.is_binding(), "{err:?}");
359 Ok(())
360 }
361
362 #[tokio::test]
363 async fn default() -> anyhow::Result<()> {
364 let transport = Arc::new(test_transport("http://ignored:1".to_string()).await?);
365 let schema = ArrowSchema::new().set_serialized_schema("test");
366 let builder = WriterBuilder::new(transport, schema.clone());
367 let writer = builder.default("projects/p/datasets/d/tables/t")?;
368 assert_eq!(
369 writer.write_stream,
370 "projects/p/datasets/d/tables/t/streams/_default"
371 );
372 assert_eq!(writer.schema, schema);
373 Ok(())
374 }
375
376 #[test_case("projects/p")]
377 #[test_case("projects/p/tables/t")]
378 #[test_case("projects/p/datasets/d/tables/")]
379 #[test_case("projects/p/instances/i/tables/t")]
380 #[test_case("projects/p/datasets/d/tables/t/streams")]
381 #[test_case("projects/p/datasets/d/tables/t/streams/_default")]
382 #[tokio::test]
383 async fn bad_table_format(table: &str) -> anyhow::Result<()> {
384 let transport = Arc::new(test_transport("http://ignored:1".to_string()).await?);
385 let schema = ArrowSchema::new().set_serialized_schema("test");
386 let builder = WriterBuilder::new(transport, schema.clone());
387 let err = builder
388 .default(table)
389 .expect_err("should fail locally on bad format");
390 assert!(err.is_binding(), "{err:?}");
391 Ok(())
392 }
393 #[tokio::test]
394 async fn buffered_success() -> anyhow::Result<()> {
395 let mut mock = MockBigQueryWrite::new();
396 mock.expect_create_write_stream().return_once(|req| {
397 let req = req.into_inner();
398 assert_eq!(req.parent, "projects/p/datasets/d/tables/t");
399 let ws = req.write_stream.expect("write_stream populated");
400 assert_eq!(Type::from(ws.r#type), Type::Buffered);
401 Ok(gaxi::grpc::tonic::Response::new(MockWriteStream {
402 name: "projects/p/datasets/d/tables/t/streams/s".to_string(),
403 ..Default::default()
404 }))
405 });
406 let (endpoint, _server) = start("0.0.0.0:0", mock).await?;
407 let transport = Arc::new(test_transport(endpoint).await?);
408 let schema = ArrowSchema::new().set_serialized_schema("test");
409 let builder = WriterBuilder::new(transport, schema.clone());
410 let writer = builder.buffered("projects/p/datasets/d/tables/t").await?;
411 assert_eq!(
412 writer.inner.write_stream,
413 "projects/p/datasets/d/tables/t/streams/s"
414 );
415 assert_eq!(writer.inner.schema, schema);
416 Ok(())
417 }
418
419 #[test_case("projects/p")]
420 #[test_case("projects/p/tables/t")]
421 #[test_case("projects/p/datasets/d/tables/")]
422 #[tokio::test]
423 async fn buffered_bad_table_format(table: &str) -> anyhow::Result<()> {
424 let transport = Arc::new(test_transport("http://ignored:1".to_string()).await?);
425 let schema = ArrowSchema::new().set_serialized_schema("test");
426 let builder = WriterBuilder::new(transport, schema.clone());
427 let err = builder
428 .buffered(table)
429 .await
430 .expect_err("should fail locally on bad format");
431 assert!(err.is_binding(), "{err:?}");
432 Ok(())
433 }
434
435 async fn attach_mock(stream_type: Type) -> anyhow::Result<(Arc<Transport>, JoinHandle<()>)> {
436 let mut mock = MockBigQueryWrite::new();
437 mock.expect_get_write_stream().return_once(move |req| {
438 let req = req.into_inner();
439 assert_eq!(req.name, "projects/p/datasets/d/tables/t/streams/s");
440 Ok(gaxi::grpc::tonic::Response::new(MockWriteStream {
441 name: "projects/p/datasets/d/tables/t/streams/s".to_string(),
442 r#type: stream_type.value().expect("known enum value"),
443 ..Default::default()
444 }))
445 });
446 let (endpoint, server) = start("0.0.0.0:0", mock).await?;
447 let transport = Arc::new(test_transport(endpoint).await?);
448 Ok((transport, server))
449 }
450
451 #[tokio::test]
452 async fn attach_committed_success() -> anyhow::Result<()> {
453 let (transport, _server) = attach_mock(Type::Committed).await?;
454 let schema = ArrowSchema::new().set_serialized_schema("test");
455 let builder = WriterBuilder::new(transport, schema.clone());
456 let writer: CommittedWriter = builder
457 .attach("projects/p/datasets/d/tables/t/streams/s")
458 .await?;
459 assert_eq!(
460 writer.inner.write_stream,
461 "projects/p/datasets/d/tables/t/streams/s"
462 );
463 assert_eq!(writer.inner.schema, schema);
464 Ok(())
465 }
466
467 #[tokio::test]
468 async fn attach_pending_success() -> anyhow::Result<()> {
469 let (transport, _server) = attach_mock(Type::Pending).await?;
470 let schema = ArrowSchema::new().set_serialized_schema("test");
471 let builder = WriterBuilder::new(transport, schema.clone());
472 let writer: PendingWriter = builder
473 .attach("projects/p/datasets/d/tables/t/streams/s")
474 .await?;
475 assert_eq!(
476 writer.inner.write_stream,
477 "projects/p/datasets/d/tables/t/streams/s"
478 );
479 assert_eq!(writer.inner.schema, schema);
480 Ok(())
481 }
482
483 #[tokio::test]
484 async fn attach_buffered_success() -> anyhow::Result<()> {
485 let (transport, _server) = attach_mock(Type::Buffered).await?;
486 let schema = ArrowSchema::new().set_serialized_schema("test");
487 let builder = WriterBuilder::new(transport, schema.clone());
488 let writer: BufferedWriter = builder
489 .attach("projects/p/datasets/d/tables/t/streams/s")
490 .await?;
491 assert_eq!(
492 writer.inner.write_stream,
493 "projects/p/datasets/d/tables/t/streams/s"
494 );
495 assert_eq!(writer.inner.schema, schema);
496 Ok(())
497 }
498
499 #[test_case("projects/p")]
500 #[test_case("projects/p/tables/t")]
501 #[test_case("projects/p/datasets/d/tables/t")]
502 #[test_case("projects/p/datasets/d/tables/t/streams/")]
503 #[tokio::test]
504 async fn attach_bad_stream_format(stream: &str) -> anyhow::Result<()> {
505 let transport = Arc::new(test_transport("http://ignored:1".to_string()).await?);
506 let schema = ArrowSchema::new().set_serialized_schema("test");
507 let builder = WriterBuilder::new(transport, schema.clone());
508 let err = builder
509 .attach::<CommittedWriter, _>(stream)
510 .await
511 .expect_err("should fail locally on bad format");
512 assert!(matches!(err, AttachError::Rpc { source: e } if e.is_binding()));
513 Ok(())
514 }
515
516 #[tokio::test]
517 async fn attach_stream_type_mismatch() -> anyhow::Result<()> {
518 let (transport, _server) = attach_mock(Type::Buffered).await?;
519 let schema = ArrowSchema::new().set_serialized_schema("test");
520 let builder = WriterBuilder::new(transport, schema.clone());
521 let err = builder
522 .attach::<CommittedWriter, _>("projects/p/datasets/d/tables/t/streams/s")
523 .await
524 .expect_err("should return type mismatch error");
525 assert!(matches!(err, AttachError::TypeMismatch { .. }));
526 assert!(err.to_string().contains("stream type mismatch: requested"));
527 Ok(())
528 }
529}