1use core::pin::Pin;
16use core::time::Duration;
17use std::collections::{BTreeMap, HashMap, VecDeque};
18
19use anyhow::{Context, anyhow};
20use async_trait::async_trait;
21use base64::Engine;
22use base64::prelude::BASE64_STANDARD;
23use futures::future::pending;
24use futures::prelude::Future;
25use futures::{Stream, StreamExt};
26use futures_async_stream::try_stream;
27use gcp_bigquery_client::Client;
28use gcp_bigquery_client::error::BQError;
29use gcp_bigquery_client::model::query_request::QueryRequest;
30use gcp_bigquery_client::model::query_response::ResultSet;
31use gcp_bigquery_client::model::table::Table;
32use gcp_bigquery_client::model::table_field_schema::TableFieldSchema;
33use gcp_bigquery_client::model::table_schema::TableSchema;
34use google_cloud_bigquery::grpc::apiv1::conn_pool::ConnectionManager;
35use google_cloud_gax::conn::{ConnectionOptions, Environment};
36use google_cloud_gax::grpc::{Request, Response, Status};
37use google_cloud_googleapis::cloud::bigquery::storage::v1::append_rows_request::{
38 MissingValueInterpretation, ProtoData, Rows as AppendRowsRequestRows,
39};
40use google_cloud_googleapis::cloud::bigquery::storage::v1::{
41 AppendRowsRequest, AppendRowsResponse, ProtoRows, ProtoSchema,
42};
43use google_cloud_pubsub::client::google_cloud_auth;
44use google_cloud_pubsub::client::google_cloud_auth::credentials::CredentialsFile;
45use phf::{Set, phf_set};
46use prost_reflect::{FieldDescriptor, MessageDescriptor};
47use prost_types::{
48 DescriptorProto, FieldDescriptorProto, FileDescriptorProto, FileDescriptorSet,
49 field_descriptor_proto,
50};
51use risingwave_common::array::{Op, StreamChunk};
52use risingwave_common::catalog::{Field, Schema};
53use risingwave_common::types::DataType;
54use serde::Deserialize;
55use serde_with::{DisplayFromStr, serde_as};
56use simd_json::prelude::ArrayTrait;
57use tokio::sync::mpsc;
58use url::Url;
59use uuid::Uuid;
60use with_options::WithOptions;
61use yup_oauth2::ServiceAccountKey;
62
63use super::encoder::{ProtoEncoder, ProtoHeader, RowEncoder, SerTo};
64use super::log_store::{LogStoreReadItem, TruncateOffset};
65use super::{
66 LogSinker, SINK_TYPE_APPEND_ONLY, SINK_TYPE_OPTION, SINK_TYPE_UPSERT, SinkError, SinkLogReader,
67};
68use crate::aws_utils::load_file_descriptor_from_s3;
69use crate::connector_common::AwsAuthProps;
70use crate::enforce_secret::EnforceSecret;
71use crate::sink::{Result, Sink, SinkParam, SinkWriterParam};
72
73pub const BIGQUERY_SINK: &str = "bigquery";
74pub const CHANGE_TYPE: &str = "_CHANGE_TYPE";
75const DEFAULT_GRPC_CHANNEL_NUMS: usize = 4;
76const CONNECT_TIMEOUT: Option<Duration> = Some(Duration::from_secs(30));
77const CONNECTION_TIMEOUT: Option<Duration> = None;
78const BIGQUERY_SEND_FUTURE_BUFFER_MAX_SIZE: usize = 65536;
79const MAX_ROW_SIZE: usize = 8 * 1024 * 1024;
81
82#[serde_as]
83#[derive(Deserialize, Debug, Clone, WithOptions)]
84pub struct BigQueryCommon {
85 #[serde(rename = "bigquery.local.path")]
86 pub local_path: Option<String>,
87 #[serde(rename = "bigquery.s3.path")]
88 pub s3_path: Option<String>,
89 #[serde(rename = "bigquery.project")]
90 pub project: String,
91 #[serde(rename = "bigquery.dataset")]
92 pub dataset: String,
93 #[serde(rename = "bigquery.table")]
94 pub table: String,
95 #[serde(default, alias = "create_table_if_not_exists")] #[serde_as(as = "DisplayFromStr")]
97 pub auto_create: bool,
98 #[serde(rename = "bigquery.credentials")]
99 pub credentials: Option<String>,
100}
101
102impl EnforceSecret for BigQueryCommon {
103 const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
104 "bigquery.credentials",
105 };
106}
107
108struct BigQueryFutureManager {
109 offset_queue: VecDeque<(TruncateOffset, usize)>,
116 resp_stream: Pin<Box<dyn Stream<Item = Result<()>> + Send>>,
117}
118impl BigQueryFutureManager {
119 pub fn new(
120 max_future_num: usize,
121 resp_stream: impl Stream<Item = Result<()>> + Send + 'static,
122 ) -> Self {
123 let offset_queue = VecDeque::with_capacity(max_future_num);
124 Self {
125 offset_queue,
126 resp_stream: Box::pin(resp_stream),
127 }
128 }
129
130 pub fn add_offset(&mut self, offset: TruncateOffset, resp_num: usize) {
131 self.offset_queue.push_back((offset, resp_num));
132 }
133
134 pub async fn next_offset(&mut self) -> Result<TruncateOffset> {
135 if let Some((_offset, remaining_resp_num)) = self.offset_queue.front_mut() {
136 if *remaining_resp_num == 0 {
137 return Ok(self.offset_queue.pop_front().unwrap().0);
138 }
139 while *remaining_resp_num > 0 {
140 self.resp_stream
141 .next()
142 .await
143 .ok_or_else(|| SinkError::BigQuery(anyhow::anyhow!("end of stream")))??;
144 *remaining_resp_num -= 1;
145 }
146 Ok(self.offset_queue.pop_front().unwrap().0)
147 } else {
148 pending().await
149 }
150 }
151}
152pub struct BigQueryLogSinker {
153 writer: BigQuerySinkWriter,
154 bigquery_future_manager: BigQueryFutureManager,
155 future_num: usize,
156}
157impl BigQueryLogSinker {
158 pub fn new(
159 writer: BigQuerySinkWriter,
160 resp_stream: impl Stream<Item = Result<()>> + Send + 'static,
161 future_num: usize,
162 ) -> Self {
163 Self {
164 writer,
165 bigquery_future_manager: BigQueryFutureManager::new(future_num, resp_stream),
166 future_num,
167 }
168 }
169}
170
171#[async_trait]
172impl LogSinker for BigQueryLogSinker {
173 async fn consume_log_and_sink(mut self, mut log_reader: impl SinkLogReader) -> Result<!> {
174 log_reader.start_from(None).await?;
175 loop {
176 tokio::select!(
177 offset = self.bigquery_future_manager.next_offset() => {
178 log_reader.truncate(offset?)?;
179 }
180 item_result = log_reader.next_item(), if self.bigquery_future_manager.offset_queue.len() <= self.future_num => {
181 let (epoch, item) = item_result?;
182 match item {
183 LogStoreReadItem::StreamChunk { chunk_id, chunk } => {
184 let resp_num = self.writer.write_chunk(chunk)?;
185 self.bigquery_future_manager
186 .add_offset(TruncateOffset::Chunk { epoch, chunk_id },resp_num);
187 }
188 LogStoreReadItem::Barrier { .. } => {
189 self.bigquery_future_manager
190 .add_offset(TruncateOffset::Barrier { epoch },0);
191 }
192 }
193 }
194 )
195 }
196 }
197}
198
199impl BigQueryCommon {
200 async fn build_client(&self, aws_auth_props: &AwsAuthProps) -> Result<Client> {
201 let auth_json = self.get_auth_json_from_path(aws_auth_props).await?;
202
203 let service_account =
204 if let Ok(auth_json_from_base64) = BASE64_STANDARD.decode(auth_json.clone()) {
205 serde_json::from_slice::<ServiceAccountKey>(&auth_json_from_base64)
206 } else {
207 serde_json::from_str::<ServiceAccountKey>(&auth_json)
208 }
209 .map_err(|e| SinkError::BigQuery(e.into()))?;
210
211 let client: Client = Client::from_service_account_key(service_account, false)
212 .await
213 .map_err(|err| SinkError::BigQuery(anyhow::anyhow!(err)))?;
214 Ok(client)
215 }
216
217 async fn build_writer_client(
218 &self,
219 aws_auth_props: &AwsAuthProps,
220 ) -> Result<(StorageWriterClient, impl Stream<Item = Result<()>> + use<>)> {
221 let auth_json = self.get_auth_json_from_path(aws_auth_props).await?;
222
223 let credentials_file =
224 if let Ok(auth_json_from_base64) = BASE64_STANDARD.decode(auth_json.clone()) {
225 serde_json::from_slice::<CredentialsFile>(&auth_json_from_base64)
226 } else {
227 serde_json::from_str::<CredentialsFile>(&auth_json)
228 }
229 .map_err(|e| SinkError::BigQuery(e.into()))?;
230
231 StorageWriterClient::new(credentials_file).await
232 }
233
234 async fn get_auth_json_from_path(&self, aws_auth_props: &AwsAuthProps) -> Result<String> {
235 if let Some(credentials) = &self.credentials {
236 Ok(credentials.clone())
237 } else if let Some(local_path) = &self.local_path {
238 std::fs::read_to_string(local_path)
239 .map_err(|err| SinkError::BigQuery(anyhow::anyhow!(err)))
240 } else if let Some(s3_path) = &self.s3_path {
241 let url =
242 Url::parse(s3_path).map_err(|err| SinkError::BigQuery(anyhow::anyhow!(err)))?;
243 let auth_vec = load_file_descriptor_from_s3(&url, aws_auth_props)
244 .await
245 .map_err(|err| SinkError::BigQuery(anyhow::anyhow!(err)))?;
246 Ok(String::from_utf8(auth_vec).map_err(|e| SinkError::BigQuery(e.into()))?)
247 } else {
248 Err(SinkError::BigQuery(anyhow::anyhow!(
249 "`bigquery.local.path` and `bigquery.s3.path` set at least one, configure as needed."
250 )))
251 }
252 }
253}
254
255#[serde_as]
256#[derive(Clone, Debug, Deserialize, WithOptions)]
257pub struct BigQueryConfig {
258 #[serde(flatten)]
259 pub common: BigQueryCommon,
260 #[serde(flatten)]
261 pub aws_auth_props: AwsAuthProps,
262 pub r#type: String, }
264
265impl EnforceSecret for BigQueryConfig {
266 fn enforce_one(prop: &str) -> crate::error::ConnectorResult<()> {
267 BigQueryCommon::enforce_one(prop)?;
268 AwsAuthProps::enforce_one(prop)?;
269 Ok(())
270 }
271}
272
273impl BigQueryConfig {
274 pub fn from_btreemap(properties: BTreeMap<String, String>) -> Result<Self> {
275 let config =
276 serde_json::from_value::<BigQueryConfig>(serde_json::to_value(properties).unwrap())
277 .map_err(|e| SinkError::Config(anyhow!(e)))?;
278 if config.r#type != SINK_TYPE_APPEND_ONLY && config.r#type != SINK_TYPE_UPSERT {
279 return Err(SinkError::Config(anyhow!(
280 "`{}` must be {}, or {}",
281 SINK_TYPE_OPTION,
282 SINK_TYPE_APPEND_ONLY,
283 SINK_TYPE_UPSERT
284 )));
285 }
286 Ok(config)
287 }
288}
289
290#[derive(Debug)]
291pub struct BigQuerySink {
292 pub config: BigQueryConfig,
293 schema: Schema,
294 pk_indices: Vec<usize>,
295 is_append_only: bool,
296}
297
298impl EnforceSecret for BigQuerySink {
299 fn enforce_secret<'a>(
300 prop_iter: impl Iterator<Item = &'a str>,
301 ) -> crate::error::ConnectorResult<()> {
302 for prop in prop_iter {
303 BigQueryConfig::enforce_one(prop)?;
304 }
305 Ok(())
306 }
307}
308
309impl BigQuerySink {
310 pub fn new(
311 config: BigQueryConfig,
312 schema: Schema,
313 pk_indices: Vec<usize>,
314 is_append_only: bool,
315 ) -> Result<Self> {
316 Ok(Self {
317 config,
318 schema,
319 pk_indices,
320 is_append_only,
321 })
322 }
323}
324
325impl BigQuerySink {
326 fn is_decimal_type_compatible(bigquery_type: &str) -> bool {
327 let normalized = bigquery_type.trim().to_ascii_uppercase();
332 matches!(
333 normalized
334 .split_once('(')
335 .map_or(normalized.as_str(), |(prefix, _)| prefix),
336 "NUMERIC" | "BIGNUMERIC"
337 )
338 }
339
340 fn is_data_type_compatible(rw_data_type: &DataType, bigquery_type: &str) -> Result<bool> {
341 if matches!(rw_data_type, DataType::Decimal) {
342 return Ok(Self::is_decimal_type_compatible(bigquery_type));
343 }
344
345 Ok(Self::get_string_and_check_support_from_datatype(rw_data_type)? == bigquery_type)
346 }
347
348 fn check_column_name_and_type(
349 &self,
350 big_query_columns_desc: HashMap<String, String>,
351 ) -> Result<()> {
352 let rw_fields_name = self.schema.fields();
353 if big_query_columns_desc.is_empty() {
354 return Err(SinkError::BigQuery(anyhow::anyhow!(
355 "Cannot find table in bigquery"
356 )));
357 }
358 if rw_fields_name.len().ne(&big_query_columns_desc.len()) {
359 return Err(SinkError::BigQuery(anyhow::anyhow!(
360 "The length of the RisingWave column {} must be equal to the length of the bigquery column {}",
361 rw_fields_name.len(),
362 big_query_columns_desc.len()
363 )));
364 }
365
366 for i in rw_fields_name {
367 let value = big_query_columns_desc.get(&i.name).ok_or_else(|| {
368 SinkError::BigQuery(anyhow::anyhow!(
369 "Column `{:?}` on RisingWave side is not found on BigQuery side.",
370 i.name
371 ))
372 })?;
373 let data_type_string = Self::get_string_and_check_support_from_datatype(&i.data_type)?;
374 if !Self::is_data_type_compatible(&i.data_type, value)? {
375 return Err(SinkError::BigQuery(anyhow::anyhow!(
376 "Data type mismatch for column `{:?}`. BigQuery side: `{:?}`, RisingWave side: `{:?}`. ",
377 i.name,
378 value,
379 data_type_string
380 )));
381 };
382 }
383 Ok(())
384 }
385
386 fn get_string_and_check_support_from_datatype(rw_data_type: &DataType) -> Result<String> {
387 match rw_data_type {
388 DataType::Boolean => Ok("BOOL".to_owned()),
389 DataType::Int16 => Ok("INT64".to_owned()),
390 DataType::Int32 => Ok("INT64".to_owned()),
391 DataType::Int64 => Ok("INT64".to_owned()),
392 DataType::Float32 => Err(SinkError::BigQuery(anyhow::anyhow!(
393 "REAL is not supported for BigQuery sink. Please convert to FLOAT64 or other supported types."
394 ))),
395 DataType::Float64 => Ok("FLOAT64".to_owned()),
396 DataType::Decimal => Ok("NUMERIC".to_owned()),
397 DataType::Date => Ok("DATE".to_owned()),
398 DataType::Varchar => Ok("STRING".to_owned()),
399 DataType::Time => Ok("TIME".to_owned()),
400 DataType::Timestamp => Ok("DATETIME".to_owned()),
401 DataType::Timestamptz => Ok("TIMESTAMP".to_owned()),
402 DataType::Interval => Ok("INTERVAL".to_owned()),
403 DataType::Struct(structs) => {
404 let mut elements_vec = vec![];
405 for (name, datatype) in structs.iter() {
406 let element_string =
407 Self::get_string_and_check_support_from_datatype(datatype)?;
408 elements_vec.push(format!("{} {}", name, element_string));
409 }
410 Ok(format!("STRUCT<{}>", elements_vec.join(", ")))
411 }
412 DataType::List(l) => {
413 let element_string = Self::get_string_and_check_support_from_datatype(l.elem())?;
414 Ok(format!("ARRAY<{}>", element_string))
415 }
416 DataType::Bytea => Ok("BYTES".to_owned()),
417 DataType::Jsonb => Ok("JSON".to_owned()),
418 DataType::Serial => Ok("INT64".to_owned()),
419 DataType::Int256 => Err(SinkError::BigQuery(anyhow::anyhow!(
420 "INT256 is not supported for BigQuery sink."
421 ))),
422 DataType::Map(_) => Err(SinkError::BigQuery(anyhow::anyhow!(
423 "MAP is not supported for BigQuery sink."
424 ))),
425 DataType::Vector(_) => Err(SinkError::BigQuery(anyhow::anyhow!(
426 "VECTOR is not supported for BigQuery sink."
427 ))),
428 }
429 }
430
431 fn map_field(rw_field: &Field) -> Result<TableFieldSchema> {
432 let tfs = match &rw_field.data_type {
433 DataType::Boolean => TableFieldSchema::bool(&rw_field.name),
434 DataType::Int16 | DataType::Int32 | DataType::Int64 | DataType::Serial => {
435 TableFieldSchema::integer(&rw_field.name)
436 }
437 DataType::Float32 => {
438 return Err(SinkError::BigQuery(anyhow::anyhow!(
439 "REAL is not supported for BigQuery sink. Please convert to FLOAT64 or other supported types."
440 )));
441 }
442 DataType::Float64 => TableFieldSchema::float(&rw_field.name),
443 DataType::Decimal => TableFieldSchema::numeric(&rw_field.name),
444 DataType::Date => TableFieldSchema::date(&rw_field.name),
445 DataType::Varchar => TableFieldSchema::string(&rw_field.name),
446 DataType::Time => TableFieldSchema::time(&rw_field.name),
447 DataType::Timestamp => TableFieldSchema::date_time(&rw_field.name),
448 DataType::Timestamptz => TableFieldSchema::timestamp(&rw_field.name),
449 DataType::Interval => {
450 return Err(SinkError::BigQuery(anyhow::anyhow!(
451 "INTERVAL is not supported for BigQuery sink. Please convert to VARCHAR or other supported types."
452 )));
453 }
454 DataType::Struct(st) => {
455 let mut sub_fields = Vec::with_capacity(st.len());
456 for (name, dt) in st.iter() {
457 let rw_field = Field::with_name(dt.clone(), name);
458 let field = Self::map_field(&rw_field)?;
459 sub_fields.push(field);
460 }
461 TableFieldSchema::record(&rw_field.name, sub_fields)
462 }
463 DataType::List(lt) => {
464 let inner_field =
465 Self::map_field(&Field::with_name(lt.elem().clone(), &rw_field.name))?;
466 TableFieldSchema {
467 mode: Some("REPEATED".to_owned()),
468 ..inner_field
469 }
470 }
471
472 DataType::Bytea => TableFieldSchema::bytes(&rw_field.name),
473 DataType::Jsonb => TableFieldSchema::json(&rw_field.name),
474 DataType::Int256 => {
475 return Err(SinkError::BigQuery(anyhow::anyhow!(
476 "INT256 is not supported for BigQuery sink."
477 )));
478 }
479 DataType::Map(_) => {
480 return Err(SinkError::BigQuery(anyhow::anyhow!(
481 "MAP is not supported for BigQuery sink."
482 )));
483 }
484 DataType::Vector(_) => {
485 return Err(SinkError::BigQuery(anyhow::anyhow!(
486 "VECTOR is not supported for BigQuery sink."
487 )));
488 }
489 };
490 Ok(tfs)
491 }
492
493 async fn create_table(
494 &self,
495 client: &Client,
496 project_id: &str,
497 dataset_id: &str,
498 table_id: &str,
499 fields: &Vec<Field>,
500 ) -> Result<Table> {
501 let dataset = client
502 .dataset()
503 .get(project_id, dataset_id)
504 .await
505 .map_err(|e| SinkError::BigQuery(e.into()))?;
506 let fields: Vec<_> = fields.iter().map(Self::map_field).collect::<Result<_>>()?;
507 let table = Table::from_dataset(&dataset, table_id, TableSchema::new(fields));
508
509 client
510 .table()
511 .create(table)
512 .await
513 .map_err(|e| SinkError::BigQuery(e.into()))
514 }
515}
516
517impl Sink for BigQuerySink {
518 type LogSinker = BigQueryLogSinker;
519
520 const SINK_NAME: &'static str = BIGQUERY_SINK;
521
522 async fn new_log_sinker(&self, _writer_param: SinkWriterParam) -> Result<Self::LogSinker> {
523 let (writer, resp_stream) = BigQuerySinkWriter::new(
524 self.config.clone(),
525 self.schema.clone(),
526 self.pk_indices.clone(),
527 self.is_append_only,
528 )
529 .await?;
530 Ok(BigQueryLogSinker::new(
531 writer,
532 resp_stream,
533 BIGQUERY_SEND_FUTURE_BUFFER_MAX_SIZE,
534 ))
535 }
536
537 async fn validate(&self) -> Result<()> {
538 risingwave_common::license::Feature::BigQuerySink
539 .check_available()
540 .map_err(|e| anyhow::anyhow!(e))?;
541 if !self.is_append_only && self.pk_indices.is_empty() {
542 return Err(SinkError::Config(anyhow!(
543 "Primary key not defined for upsert bigquery sink (please define in `primary_key` field)"
544 )));
545 }
546 let client = self
547 .config
548 .common
549 .build_client(&self.config.aws_auth_props)
550 .await?;
551 let BigQueryCommon {
552 project: project_id,
553 dataset: dataset_id,
554 table: table_id,
555 ..
556 } = &self.config.common;
557
558 if self.config.common.auto_create {
559 match client
560 .table()
561 .get(project_id, dataset_id, table_id, None)
562 .await
563 {
564 Err(BQError::ResponseError { error }) if error.error.code == 404 => {
565 return self
567 .create_table(
568 &client,
569 project_id,
570 dataset_id,
571 table_id,
572 &self.schema.fields,
573 )
574 .await
575 .map(|_| ());
576 }
577 Err(e) => return Err(SinkError::BigQuery(e.into())),
578 _ => {}
579 }
580 }
581
582 let rs = client
583 .job()
584 .query(
585 &self.config.common.project,
586 QueryRequest::new(format!(
587 "SELECT column_name, data_type FROM `{}.{}.INFORMATION_SCHEMA.COLUMNS` WHERE table_name = '{}'",
588 project_id, dataset_id, table_id,
589 )),
590 ).await.map_err(|e| SinkError::BigQuery(e.into()))?;
591 let mut rs = ResultSet::new_from_query_response(rs);
592
593 let mut big_query_schema = HashMap::default();
594 while rs.next_row() {
595 big_query_schema.insert(
596 rs.get_string_by_name("column_name")
597 .map_err(|e| SinkError::BigQuery(e.into()))?
598 .ok_or_else(|| {
599 SinkError::BigQuery(anyhow::anyhow!("Cannot find column_name"))
600 })?,
601 rs.get_string_by_name("data_type")
602 .map_err(|e| SinkError::BigQuery(e.into()))?
603 .ok_or_else(|| {
604 SinkError::BigQuery(anyhow::anyhow!("Cannot find column_name"))
605 })?,
606 );
607 }
608
609 self.check_column_name_and_type(big_query_schema)?;
610 Ok(())
611 }
612}
613
614pub struct BigQuerySinkWriter {
615 pub config: BigQueryConfig,
616 #[expect(dead_code)]
617 schema: Schema,
618 #[expect(dead_code)]
619 pk_indices: Vec<usize>,
620 client: StorageWriterClient,
621 is_append_only: bool,
622 row_encoder: ProtoEncoder,
623 writer_pb_schema: ProtoSchema,
624 #[expect(dead_code)]
625 message_descriptor: MessageDescriptor,
626 write_stream: String,
627 proto_field: Option<FieldDescriptor>,
628}
629
630impl TryFrom<SinkParam> for BigQuerySink {
631 type Error = SinkError;
632
633 fn try_from(param: SinkParam) -> std::result::Result<Self, Self::Error> {
634 let schema = param.schema();
635 let pk_indices = param.downstream_pk_or_empty();
636 let config = BigQueryConfig::from_btreemap(param.properties)?;
637 BigQuerySink::new(config, schema, pk_indices, param.sink_type.is_append_only())
638 }
639}
640
641impl BigQuerySinkWriter {
642 pub async fn new(
643 config: BigQueryConfig,
644 schema: Schema,
645 pk_indices: Vec<usize>,
646 is_append_only: bool,
647 ) -> Result<(Self, impl Stream<Item = Result<()>>)> {
648 let (client, resp_stream) = config
649 .common
650 .build_writer_client(&config.aws_auth_props)
651 .await?;
652 let mut descriptor_proto = build_protobuf_schema(
653 schema
654 .fields()
655 .iter()
656 .map(|f| (f.name.as_str(), &f.data_type)),
657 config.common.table.clone(),
658 )?;
659
660 if !is_append_only {
661 let field = FieldDescriptorProto {
662 name: Some(CHANGE_TYPE.to_owned()),
663 number: Some((schema.len() + 1) as i32),
664 r#type: Some(field_descriptor_proto::Type::String.into()),
665 ..Default::default()
666 };
667 descriptor_proto.field.push(field);
668 }
669
670 let descriptor_pool = build_protobuf_descriptor_pool(&descriptor_proto)?;
671 let message_descriptor = descriptor_pool
672 .get_message_by_name(&config.common.table)
673 .ok_or_else(|| {
674 SinkError::BigQuery(anyhow::anyhow!(
675 "Can't find message proto {}",
676 config.common.table
677 ))
678 })?;
679 let proto_field = if !is_append_only {
680 let proto_field = message_descriptor
681 .get_field_by_name(CHANGE_TYPE)
682 .ok_or_else(|| {
683 SinkError::BigQuery(anyhow::anyhow!("Can't find {}", CHANGE_TYPE))
684 })?;
685 Some(proto_field)
686 } else {
687 None
688 };
689 let row_encoder = ProtoEncoder::new(
690 schema.clone(),
691 None,
692 message_descriptor.clone(),
693 ProtoHeader::None,
694 )?;
695 Ok((
696 Self {
697 write_stream: format!(
698 "projects/{}/datasets/{}/tables/{}/streams/_default",
699 config.common.project, config.common.dataset, config.common.table
700 ),
701 config,
702 schema,
703 pk_indices,
704 client,
705 is_append_only,
706 row_encoder,
707 message_descriptor,
708 proto_field,
709 writer_pb_schema: ProtoSchema {
710 proto_descriptor: Some(descriptor_proto.clone()),
711 },
712 },
713 resp_stream,
714 ))
715 }
716
717 fn append_only(&mut self, chunk: StreamChunk) -> Result<Vec<Vec<u8>>> {
718 let mut serialized_rows: Vec<Vec<u8>> = Vec::with_capacity(chunk.capacity());
719 for (op, row) in chunk.rows() {
720 if op != Op::Insert {
721 continue;
722 }
723 serialized_rows.push(self.row_encoder.encode(row)?.ser_to()?)
724 }
725 Ok(serialized_rows)
726 }
727
728 fn upsert(&mut self, chunk: StreamChunk) -> Result<Vec<Vec<u8>>> {
729 let mut serialized_rows: Vec<Vec<u8>> = Vec::with_capacity(chunk.capacity());
730 for (op, row) in chunk.rows() {
731 if op == Op::UpdateDelete {
732 continue;
733 }
734 let mut pb_row = self.row_encoder.encode(row)?;
735 match op {
736 Op::Insert => pb_row
737 .message
738 .try_set_field(
739 self.proto_field.as_ref().unwrap(),
740 prost_reflect::Value::String("UPSERT".to_owned()),
741 )
742 .map_err(|e| SinkError::BigQuery(e.into()))?,
743 Op::Delete => pb_row
744 .message
745 .try_set_field(
746 self.proto_field.as_ref().unwrap(),
747 prost_reflect::Value::String("DELETE".to_owned()),
748 )
749 .map_err(|e| SinkError::BigQuery(e.into()))?,
750 Op::UpdateDelete => continue,
751 Op::UpdateInsert => pb_row
752 .message
753 .try_set_field(
754 self.proto_field.as_ref().unwrap(),
755 prost_reflect::Value::String("UPSERT".to_owned()),
756 )
757 .map_err(|e| SinkError::BigQuery(e.into()))?,
758 };
759
760 serialized_rows.push(pb_row.ser_to()?)
761 }
762 Ok(serialized_rows)
763 }
764
765 fn write_chunk(&mut self, chunk: StreamChunk) -> Result<usize> {
766 let serialized_rows = if self.is_append_only {
767 self.append_only(chunk)?
768 } else {
769 self.upsert(chunk)?
770 };
771 if serialized_rows.is_empty() {
772 return Ok(0);
773 }
774 let mut result = Vec::new();
775 let mut result_inner = Vec::new();
776 let mut size_count = 0;
777 for i in serialized_rows {
778 size_count += i.len();
779 if size_count > MAX_ROW_SIZE {
780 result.push(result_inner);
781 result_inner = Vec::new();
782 size_count = i.len();
783 }
784 result_inner.push(i);
785 }
786 if !result_inner.is_empty() {
787 result.push(result_inner);
788 }
789 let len = result.len();
790 for serialized_rows in result {
791 let rows = AppendRowsRequestRows::ProtoRows(ProtoData {
792 writer_schema: Some(self.writer_pb_schema.clone()),
793 rows: Some(ProtoRows { serialized_rows }),
794 });
795 self.client.append_rows(rows, self.write_stream.clone())?;
796 }
797 Ok(len)
798 }
799}
800
801#[try_stream(ok = (), error = SinkError)]
802pub async fn resp_to_stream(
803 resp_stream: impl Future<
804 Output = std::result::Result<
805 Response<google_cloud_gax::grpc::Streaming<AppendRowsResponse>>,
806 Status,
807 >,
808 >
809 + 'static
810 + Send,
811) {
812 let mut resp_stream = resp_stream
813 .await
814 .map_err(|e| SinkError::BigQuery(e.into()))?
815 .into_inner();
816 loop {
817 match resp_stream
818 .message()
819 .await
820 .map_err(|e| SinkError::BigQuery(e.into()))?
821 {
822 Some(append_rows_response) => {
823 if !append_rows_response.row_errors.is_empty() {
824 return Err(SinkError::BigQuery(anyhow::anyhow!(
825 "bigquery insert error {:?}",
826 append_rows_response.row_errors
827 )));
828 }
829 if let Some(google_cloud_googleapis::cloud::bigquery::storage::v1::append_rows_response::Response::Error(status)) = append_rows_response.response{
830 return Err(SinkError::BigQuery(anyhow::anyhow!(
831 "bigquery insert error {:?}",
832 status
833 )));
834 }
835 yield ();
836 }
837 None => {
838 return Err(SinkError::BigQuery(anyhow::anyhow!(
839 "bigquery insert error: end of resp stream",
840 )));
841 }
842 }
843 }
844}
845
846struct StorageWriterClient {
847 #[expect(dead_code)]
848 environment: Environment,
849 request_sender: mpsc::UnboundedSender<AppendRowsRequest>,
850}
851impl StorageWriterClient {
852 pub async fn new(
853 credentials: CredentialsFile,
854 ) -> Result<(Self, impl Stream<Item = Result<()>>)> {
855 let ts_grpc = google_cloud_auth::token::DefaultTokenSourceProvider::new_with_credentials(
856 Self::bigquery_grpc_auth_config(),
857 Box::new(credentials),
858 )
859 .await
860 .map_err(|e| SinkError::BigQuery(e.into()))?;
861 let conn_options = ConnectionOptions {
862 connect_timeout: CONNECT_TIMEOUT,
863 timeout: CONNECTION_TIMEOUT,
864 ..Default::default()
865 };
866 let environment = Environment::GoogleCloud(Box::new(ts_grpc));
867 let conn = ConnectionManager::new(DEFAULT_GRPC_CHANNEL_NUMS, &environment, &conn_options)
868 .await
869 .map_err(|e| SinkError::BigQuery(e.into()))?;
870 let mut client = conn.writer();
871
872 let (tx, rx) = mpsc::unbounded_channel();
873 let stream = tokio_stream::wrappers::UnboundedReceiverStream::new(rx);
874
875 let resp = async move { client.append_rows(Request::new(stream)).await };
876 let resp_stream = resp_to_stream(resp);
877
878 Ok((
879 StorageWriterClient {
880 environment,
881 request_sender: tx,
882 },
883 resp_stream,
884 ))
885 }
886
887 pub fn append_rows(&mut self, row: AppendRowsRequestRows, write_stream: String) -> Result<()> {
888 let append_req = AppendRowsRequest {
889 write_stream,
890 offset: None,
891 trace_id: Uuid::new_v4().hyphenated().to_string(),
892 missing_value_interpretations: HashMap::default(),
893 rows: Some(row),
894 default_missing_value_interpretation: MissingValueInterpretation::DefaultValue as i32,
895 };
896 self.request_sender
897 .send(append_req)
898 .map_err(|e| SinkError::BigQuery(e.into()))?;
899 Ok(())
900 }
901
902 fn bigquery_grpc_auth_config() -> google_cloud_auth::project::Config<'static> {
903 let mut auth_config = google_cloud_auth::project::Config::default();
904 auth_config =
905 auth_config.with_audience(google_cloud_bigquery::grpc::apiv1::conn_pool::AUDIENCE);
906 auth_config =
907 auth_config.with_scopes(&google_cloud_bigquery::grpc::apiv1::conn_pool::SCOPES);
908 auth_config
909 }
910}
911
912fn build_protobuf_descriptor_pool(desc: &DescriptorProto) -> Result<prost_reflect::DescriptorPool> {
913 let file_descriptor = FileDescriptorProto {
914 message_type: vec![desc.clone()],
915 name: Some("bigquery".to_owned()),
916 ..Default::default()
917 };
918
919 prost_reflect::DescriptorPool::from_file_descriptor_set(FileDescriptorSet {
920 file: vec![file_descriptor],
921 })
922 .context("failed to build descriptor pool")
923 .map_err(SinkError::BigQuery)
924}
925
926fn build_protobuf_schema<'a>(
927 fields: impl Iterator<Item = (&'a str, &'a DataType)>,
928 name: String,
929) -> Result<DescriptorProto> {
930 let mut proto = DescriptorProto {
931 name: Some(name),
932 ..Default::default()
933 };
934 let mut struct_vec = vec![];
935 let field_vec = fields
936 .enumerate()
937 .map(|(index, (name, data_type))| {
938 let (field, des_proto) =
939 build_protobuf_field(data_type, (index + 1) as i32, name.to_owned())?;
940 if let Some(sv) = des_proto {
941 struct_vec.push(sv);
942 }
943 Ok(field)
944 })
945 .collect::<Result<Vec<_>>>()?;
946 proto.field = field_vec;
947 proto.nested_type = struct_vec;
948 Ok(proto)
949}
950
951fn build_protobuf_field(
952 data_type: &DataType,
953 index: i32,
954 name: String,
955) -> Result<(FieldDescriptorProto, Option<DescriptorProto>)> {
956 let mut field = FieldDescriptorProto {
957 name: Some(name.clone()),
958 number: Some(index),
959 ..Default::default()
960 };
961 match data_type {
962 DataType::Boolean => field.r#type = Some(field_descriptor_proto::Type::Bool.into()),
963 DataType::Int32 => field.r#type = Some(field_descriptor_proto::Type::Int32.into()),
964 DataType::Int16 | DataType::Int64 => {
965 field.r#type = Some(field_descriptor_proto::Type::Int64.into())
966 }
967 DataType::Float64 => field.r#type = Some(field_descriptor_proto::Type::Double.into()),
968 DataType::Decimal => field.r#type = Some(field_descriptor_proto::Type::String.into()),
969 DataType::Date => field.r#type = Some(field_descriptor_proto::Type::Int32.into()),
970 DataType::Varchar => field.r#type = Some(field_descriptor_proto::Type::String.into()),
971 DataType::Time => field.r#type = Some(field_descriptor_proto::Type::String.into()),
972 DataType::Timestamp => field.r#type = Some(field_descriptor_proto::Type::String.into()),
973 DataType::Timestamptz => field.r#type = Some(field_descriptor_proto::Type::String.into()),
974 DataType::Interval => field.r#type = Some(field_descriptor_proto::Type::String.into()),
975 DataType::Struct(s) => {
976 field.r#type = Some(field_descriptor_proto::Type::Message.into());
977 let name = format!("Struct{}", name);
978 let sub_proto = build_protobuf_schema(s.iter(), name.clone())?;
979 field.type_name = Some(name);
980 return Ok((field, Some(sub_proto)));
981 }
982 DataType::List(l) => {
983 let (mut field, proto) = build_protobuf_field(l.elem(), index, name)?;
984 field.label = Some(field_descriptor_proto::Label::Repeated.into());
985 return Ok((field, proto));
986 }
987 DataType::Bytea => field.r#type = Some(field_descriptor_proto::Type::Bytes.into()),
988 DataType::Jsonb => field.r#type = Some(field_descriptor_proto::Type::String.into()),
989 DataType::Serial => field.r#type = Some(field_descriptor_proto::Type::Int64.into()),
990 DataType::Float32 | DataType::Int256 => {
991 return Err(SinkError::BigQuery(anyhow::anyhow!(
992 "Don't support Float32 and Int256"
993 )));
994 }
995 DataType::Map(_) => return Err(SinkError::BigQuery(anyhow::anyhow!("Don't support Map"))),
996 DataType::Vector(_) => {
997 return Err(SinkError::BigQuery(anyhow::anyhow!("Don't support Vector")));
998 }
999 }
1000 Ok((field, None))
1001}
1002
1003#[cfg(test)]
1004mod test {
1005
1006 use std::assert_matches;
1007 use std::collections::HashMap;
1008
1009 use risingwave_common::catalog::{Field, Schema};
1010 use risingwave_common::types::{DataType, StructType};
1011
1012 use crate::connector_common::AwsAuthProps;
1013 use crate::sink::big_query::{
1014 BigQueryCommon, BigQueryConfig, BigQuerySink, build_protobuf_descriptor_pool,
1015 build_protobuf_schema,
1016 };
1017
1018 #[tokio::test]
1019 async fn test_type_check() {
1020 let big_query_type_string = "ARRAY<STRUCT<v1 ARRAY<INT64>, v2 STRUCT<v1 INT64, v2 INT64>>>";
1021 let rw_datatype = DataType::list(DataType::Struct(StructType::new(vec![
1022 ("v1".to_owned(), DataType::Int64.list()),
1023 (
1024 "v2".to_owned(),
1025 DataType::Struct(StructType::new(vec![
1026 ("v1".to_owned(), DataType::Int64),
1027 ("v2".to_owned(), DataType::Int64),
1028 ])),
1029 ),
1030 ])));
1031 assert_eq!(
1032 BigQuerySink::get_string_and_check_support_from_datatype(&rw_datatype).unwrap(),
1033 big_query_type_string
1034 );
1035 }
1036
1037 #[tokio::test]
1038 async fn test_schema_check() {
1039 let schema = Schema {
1040 fields: vec![
1041 Field::with_name(DataType::Int64, "v1"),
1042 Field::with_name(DataType::Float64, "v2"),
1043 Field::with_name(
1044 DataType::list(DataType::Struct(StructType::new(vec![
1045 ("v1".to_owned(), DataType::Int64.list()),
1046 (
1047 "v3".to_owned(),
1048 DataType::Struct(StructType::new(vec![
1049 ("v1".to_owned(), DataType::Int64),
1050 ("v2".to_owned(), DataType::Int64),
1051 ])),
1052 ),
1053 ]))),
1054 "v3",
1055 ),
1056 ],
1057 };
1058 let fields = schema
1059 .fields()
1060 .iter()
1061 .map(|f| (f.name.as_str(), &f.data_type));
1062 let desc = build_protobuf_schema(fields, "t1".to_owned()).unwrap();
1063 let pool = build_protobuf_descriptor_pool(&desc).unwrap();
1064 let t1_message = pool.get_message_by_name("t1").unwrap();
1065 assert_matches!(
1066 t1_message.get_field_by_name("v1").unwrap().kind(),
1067 prost_reflect::Kind::Int64
1068 );
1069 assert_matches!(
1070 t1_message.get_field_by_name("v2").unwrap().kind(),
1071 prost_reflect::Kind::Double
1072 );
1073 assert_matches!(
1074 t1_message.get_field_by_name("v3").unwrap().kind(),
1075 prost_reflect::Kind::Message(_)
1076 );
1077
1078 let v3_message = pool.get_message_by_name("t1.Structv3").unwrap();
1079 assert_matches!(
1080 v3_message.get_field_by_name("v1").unwrap().kind(),
1081 prost_reflect::Kind::Int64
1082 );
1083 assert!(v3_message.get_field_by_name("v1").unwrap().is_list());
1084
1085 let v3_v3_message = pool.get_message_by_name("t1.Structv3.Structv3").unwrap();
1086 assert_matches!(
1087 v3_v3_message.get_field_by_name("v1").unwrap().kind(),
1088 prost_reflect::Kind::Int64
1089 );
1090 assert_matches!(
1091 v3_v3_message.get_field_by_name("v2").unwrap().kind(),
1092 prost_reflect::Kind::Int64
1093 );
1094 }
1095
1096 #[test]
1097 fn test_decimal_type_family_compatibility() {
1098 assert!(BigQuerySink::is_decimal_type_compatible("NUMERIC"));
1099 assert!(BigQuerySink::is_decimal_type_compatible("numeric(31, 2)"));
1100 assert!(BigQuerySink::is_decimal_type_compatible("BIGNUMERIC"));
1101 assert!(BigQuerySink::is_decimal_type_compatible(
1102 "bignumeric(35, 12)"
1103 ));
1104 assert!(!BigQuerySink::is_decimal_type_compatible("STRING"));
1105 }
1106
1107 #[test]
1108 fn test_decimal_schema_check_accepts_parameterized_numeric_types() {
1109 let sink = BigQuerySink {
1110 config: BigQueryConfig {
1111 common: BigQueryCommon {
1112 local_path: None,
1113 s3_path: None,
1114 project: "project".to_owned(),
1115 dataset: "dataset".to_owned(),
1116 table: "table".to_owned(),
1117 auto_create: false,
1118 credentials: None,
1119 },
1120 aws_auth_props: AwsAuthProps {
1121 region: None,
1122 endpoint: None,
1123 access_key: None,
1124 secret_key: None,
1125 session_token: None,
1126 arn: None,
1127 external_id: None,
1128 profile: None,
1129 msk_signer_timeout_sec: None,
1130 },
1131 r#type: "append-only".to_owned(),
1132 },
1133 schema: Schema {
1134 fields: vec![Field::with_name(DataType::Decimal, "capitalizedcost")],
1135 },
1136 pk_indices: vec![],
1137 is_append_only: true,
1138 };
1139
1140 sink.check_column_name_and_type(HashMap::from([(
1141 "capitalizedcost".to_owned(),
1142 "NUMERIC(31, 2)".to_owned(),
1143 )]))
1144 .unwrap();
1145
1146 sink.check_column_name_and_type(HashMap::from([(
1147 "capitalizedcost".to_owned(),
1148 "BIGNUMERIC(35, 12)".to_owned(),
1149 )]))
1150 .unwrap();
1151 }
1152}