1use std::sync::Arc;
4use std::time::Duration;
5
6use arrow_array::{Array, RecordBatch};
7use arrow_schema::{DataType, Schema, SchemaRef, TimeUnit};
8
9use crate::config::ConnectorConfig;
10use crate::error::ConnectorError;
11use crate::reference::ReferenceTableSource;
12
13use super::await_owned_driver;
14
15mod schema_resolution;
16
17const SNAPSHOT_ROWS_PER_BATCH: usize = 4_096;
18const MAX_SNAPSHOT_BATCH_BYTES: usize = 64 * 1024 * 1024;
19const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
20const QUERY_TIMEOUT: Duration = Duration::from_secs(30);
21const SNAPSHOT_CURSOR: &str = "laminar_reference_snapshot";
22
23fn timeout_error(timeout: Duration) -> ConnectorError {
24 ConnectorError::Timeout(u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX))
25}
26
27async fn await_reference_driver<T>(
28 operation: &'static str,
29 future: impl std::future::Future<Output = Result<T, ConnectorError>> + Send + 'static,
30) -> Result<T, ConnectorError>
31where
32 T: Send + 'static,
33{
34 await_owned_driver(future, move |error| {
35 ConnectorError::Internal(format!(
36 "postgres reference {operation} task failed: {error}"
37 ))
38 })
39 .await
40}
41
42struct PostgresSession {
43 client: tokio_postgres::Client,
44 connection: tokio::task::JoinHandle<()>,
45}
46
47impl Drop for PostgresSession {
48 fn drop(&mut self) {
49 self.connection.abort();
50 }
51}
52
53fn spawn_connection<F>(connection: F) -> tokio::task::JoinHandle<()>
54where
55 F: std::future::Future<Output = Result<(), tokio_postgres::Error>> + Send + 'static,
56{
57 tokio::spawn(async move {
58 if let Err(error) = connection.await {
59 tracing::warn!(%error, "postgres reference connection failed");
60 }
61 })
62}
63
64async fn owned_batch_execute(
65 session: PostgresSession,
66 sql: String,
67 operation: &'static str,
68) -> Result<PostgresSession, ConnectorError> {
69 await_reference_driver(operation, async move {
70 match tokio::time::timeout(QUERY_TIMEOUT, session.client.batch_execute(&sql)).await {
71 Ok(Ok(())) => Ok(session),
72 Ok(Err(error)) => Err(ConnectorError::ReadError(format!(
73 "postgres {operation}: {error}"
74 ))),
75 Err(_) => Err(timeout_error(QUERY_TIMEOUT)),
76 }
77 })
78 .await
79}
80
81async fn owned_prepare(
82 session: PostgresSession,
83 sql: String,
84 operation: &'static str,
85) -> Result<(PostgresSession, tokio_postgres::Statement), ConnectorError> {
86 await_reference_driver(operation, async move {
87 match tokio::time::timeout(QUERY_TIMEOUT, session.client.prepare(&sql)).await {
88 Ok(Ok(statement)) => Ok((session, statement)),
89 Ok(Err(error)) => Err(ConnectorError::ReadError(format!(
90 "postgres {operation}: {error}"
91 ))),
92 Err(_) => Err(timeout_error(QUERY_TIMEOUT)),
93 }
94 })
95 .await
96}
97
98async fn owned_query(
99 session: PostgresSession,
100 sql: String,
101 operation: &'static str,
102) -> Result<(PostgresSession, Vec<tokio_postgres::Row>), ConnectorError> {
103 await_reference_driver(operation, async move {
104 match tokio::time::timeout(QUERY_TIMEOUT, session.client.query(&sql, &[])).await {
105 Ok(Ok(rows)) => Ok((session, rows)),
106 Ok(Err(error)) => Err(ConnectorError::ReadError(format!(
107 "postgres {operation}: {error}"
108 ))),
109 Err(_) => Err(timeout_error(QUERY_TIMEOUT)),
110 }
111 })
112 .await
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116enum State {
117 Ready,
118 Reading,
119 Done,
120 Closed,
121}
122
123pub struct PostgresReferenceTableSource {
125 config: ConnectorConfig,
126 declared_schema: SchemaRef,
127 state: State,
128 session: Option<PostgresSession>,
129}
130
131impl PostgresReferenceTableSource {
132 #[must_use]
134 pub fn new(config: ConnectorConfig, declared_schema: SchemaRef) -> Self {
135 Self {
136 config,
137 declared_schema,
138 state: State::Ready,
139 session: None,
140 }
141 }
142
143 fn postgres_config(&self) -> Result<tokio_postgres::Config, ConnectorError> {
144 let properties = self.config.properties();
145 for (left, right) in [
146 ("connection", "connection_string"),
147 ("database", "dbname"),
148 ("user", "username"),
149 ] {
150 if properties.contains_key(left) && properties.contains_key(right) {
151 return Err(ConnectorError::ConfigurationError(format!(
152 "postgres reference cannot configure both '{left}' and '{right}'"
153 )));
154 }
155 }
156 for key in ["host", "database", "dbname", "user", "username"] {
157 if properties
158 .get(key)
159 .is_some_and(|value| value.trim().is_empty())
160 {
161 return Err(ConnectorError::ConfigurationError(format!(
162 "postgres reference '{key}' must not be empty"
163 )));
164 }
165 }
166 let connection_string = properties
167 .get("connection_string")
168 .or_else(|| properties.get("connection"));
169 let mut config = if let Some(connection_string) = connection_string {
170 if connection_string.trim().is_empty() {
171 return Err(ConnectorError::ConfigurationError(
172 "postgres reference connection string must not be empty".into(),
173 ));
174 }
175 if let Some(conflict) = [
176 "host", "port", "database", "dbname", "user", "username", "password", "options",
177 ]
178 .into_iter()
179 .find(|key| properties.contains_key(*key))
180 {
181 return Err(ConnectorError::ConfigurationError(format!(
182 "postgres reference cannot combine a connection string with '{conflict}'"
183 )));
184 }
185 connection_string.parse().map_err(|error| {
186 ConnectorError::ConfigurationError(format!(
187 "invalid postgres reference connection: {error}"
188 ))
189 })?
190 } else {
191 let mut config = tokio_postgres::Config::new();
192 if let Some(host) = properties.get("host") {
193 config.host(host);
194 }
195 if let Some(port) = properties.get("port") {
196 let port = port.parse::<u16>().map_err(|error| {
197 ConnectorError::ConfigurationError(format!(
198 "invalid postgres reference port '{port}': {error}"
199 ))
200 })?;
201 if port == 0 {
202 return Err(ConnectorError::ConfigurationError(
203 "postgres reference port must be greater than zero".into(),
204 ));
205 }
206 config.port(port);
207 }
208 if let Some(database) = properties
209 .get("database")
210 .or_else(|| properties.get("dbname"))
211 {
212 config.dbname(database);
213 }
214 if let Some(user) = properties
215 .get("user")
216 .or_else(|| properties.get("username"))
217 {
218 config.user(user);
219 }
220 if let Some(password) = properties.get("password") {
221 config.password(password);
222 }
223 if let Some(options) = properties.get("options") {
224 if options.contains('\0') {
225 return Err(ConnectorError::ConfigurationError(
226 "postgres reference options cannot contain NUL".into(),
227 ));
228 }
229 config.options(options);
230 }
231 config
232 };
233 if config.get_ports().contains(&0) {
234 return Err(ConnectorError::ConfigurationError(
235 "postgres reference port must be greater than zero".into(),
236 ));
237 }
238
239 if properties.contains_key("sslmode") {
240 return Err(ConnectorError::ConfigurationError(
241 "postgres reference uses ssl.mode (disable or verify-full), not sslmode".into(),
242 ));
243 }
244 let ssl_mode = self.ssl_mode()?;
245 let ca_path = properties.get("ssl.ca.cert.path");
246 if ca_path.is_some_and(|path| path.trim().is_empty()) {
247 return Err(ConnectorError::ConfigurationError(
248 "postgres reference ssl.ca.cert.path must not be empty".into(),
249 ));
250 }
251 if ssl_mode == crate::postgres::SslMode::Disable && ca_path.is_some() {
252 return Err(ConnectorError::ConfigurationError(
253 "ssl.ca.cert.path requires ssl.mode=verify-full".into(),
254 ));
255 }
256 config.ssl_mode(match ssl_mode {
257 crate::postgres::SslMode::Disable => tokio_postgres::config::SslMode::Disable,
258 crate::postgres::SslMode::VerifyFull => tokio_postgres::config::SslMode::Require,
259 });
260 config.connect_timeout(CONNECT_TIMEOUT);
261 Ok(config)
262 }
263
264 fn snapshot_query(&self) -> Result<String, ConnectorError> {
265 let table = self.config.get("table").ok_or_else(|| {
266 ConnectorError::ConfigurationError(
267 "postgres reference source requires a 'table' property".into(),
268 )
269 })?;
270 let table = quote_qualified_identifier(table)?;
271 let columns = self
272 .declared_schema
273 .fields()
274 .iter()
275 .map(|field| quote_identifier(field.name()))
276 .collect::<Result<Vec<_>, _>>()?;
277 if columns.is_empty() {
278 return Err(ConnectorError::ConfigurationError(
279 "postgres reference source requires at least one declared column".into(),
280 ));
281 }
282 Ok(format!("SELECT {} FROM {table}", columns.join(", ")))
283 }
284
285 fn projected_snapshot_query(
286 &self,
287 columns: &[tokio_postgres::Column],
288 ) -> Result<String, ConnectorError> {
289 if columns.len() != self.declared_schema.fields().len() {
290 return Err(ConnectorError::ReadError(format!(
291 "postgres snapshot has {} columns but {} were declared",
292 columns.len(),
293 self.declared_schema.fields().len()
294 )));
295 }
296 let expressions = columns
297 .iter()
298 .zip(self.declared_schema.fields())
299 .map(|(column, declared)| {
300 if column.name() != declared.name() {
301 return Err(ConnectorError::ReadError(format!(
302 "postgres column '{}' does not match declared column '{}'",
303 column.name(),
304 declared.name()
305 )));
306 }
307 reference_select_expression(column.name(), column.type_(), declared.data_type())
308 })
309 .collect::<Result<Vec<_>, _>>()?;
310 let table = self.config.get("table").ok_or_else(|| {
311 ConnectorError::ConfigurationError(
312 "postgres reference source requires a 'table' property".into(),
313 )
314 })?;
315 Ok(format!(
316 "SELECT {} FROM {}",
317 expressions.join(", "),
318 quote_qualified_identifier(table)?
319 ))
320 }
321
322 fn ssl_mode(&self) -> Result<crate::postgres::SslMode, ConnectorError> {
323 self.config
324 .get("ssl.mode")
325 .map(str::parse)
326 .transpose()
327 .map_err(|error| {
328 ConnectorError::ConfigurationError(format!(
329 "invalid postgres reference ssl.mode: {error}"
330 ))
331 })
332 .map(std::option::Option::unwrap_or_default)
333 }
334
335 async fn connect_session(&self) -> Result<PostgresSession, ConnectorError> {
336 let pg_config = self.postgres_config()?;
337 let ssl_mode = self.ssl_mode()?;
338 let ca_path = self
339 .config
340 .get("ssl.ca.cert.path")
341 .map(std::path::Path::new);
342 match ssl_mode {
343 crate::postgres::SslMode::Disable => {
344 await_reference_driver("connect", async move {
345 let (client, connection) = tokio::time::timeout(
346 CONNECT_TIMEOUT,
347 pg_config.connect(tokio_postgres::NoTls),
348 )
349 .await
350 .map_err(|_| timeout_error(CONNECT_TIMEOUT))?
351 .map_err(|error| {
352 ConnectorError::ConnectionFailed(format!(
353 "postgres reference connect: {error}"
354 ))
355 })?;
356 Ok(PostgresSession {
357 client,
358 connection: spawn_connection(connection),
359 })
360 })
361 .await
362 }
363 crate::postgres::SslMode::VerifyFull => {
364 let tls = crate::postgres::make_rustls_connector(ca_path)?;
365 await_reference_driver("TLS connect", async move {
366 let (client, connection) =
367 tokio::time::timeout(CONNECT_TIMEOUT, pg_config.connect(tls))
368 .await
369 .map_err(|_| timeout_error(CONNECT_TIMEOUT))?
370 .map_err(|error| {
371 ConnectorError::ConnectionFailed(format!(
372 "postgres reference verified TLS connect: {error}"
373 ))
374 })?;
375 Ok(PostgresSession {
376 client,
377 connection: spawn_connection(connection),
378 })
379 })
380 .await
381 }
382 }
383 }
384
385 async fn validate_native_binding(
386 &mut self,
387 session: &PostgresSession,
388 ) -> Result<(), ConnectorError> {
389 let current = schema_resolution::resolve(
390 &session.client,
391 &self.config,
392 Some(self.declared_schema.clone()),
393 )
394 .await?;
395 if self
396 .config
397 .schema_binding()
398 .is_some_and(|expected| expected.value != current.value)
399 {
400 return Err(ConnectorError::SchemaMismatch("PostgreSQL reference relation identity or layout changed; migrate the committed contract".into()));
401 }
402 self.config.set_schema_binding(current)?;
403 Ok(())
404 }
405
406 async fn start_snapshot(&mut self) -> Result<(), ConnectorError> {
407 let probe_query = self.snapshot_query()?;
408 let session = self.connect_session().await?;
409 let session = owned_batch_execute(
410 session,
411 "BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY".into(),
412 "begin snapshot",
413 )
414 .await?;
415 let qualified = quote_qualified_identifier(self.config.require("table")?)?;
416 let session = owned_batch_execute(
417 session,
418 format!("LOCK TABLE {qualified} IN ACCESS SHARE MODE"),
419 "lock snapshot relation",
420 )
421 .await?;
422 self.validate_native_binding(&session).await?;
423 let (session, probe) =
424 owned_prepare(session, probe_query, "prepare snapshot probe").await?;
425 let query = self.projected_snapshot_query(probe.columns())?;
426 let (session, statement) =
427 owned_prepare(session, query.clone(), "prepare snapshot projection").await?;
428 validate_postgres_schema(statement.columns(), self.declared_schema.as_ref())?;
429 let declare = format!("DECLARE {SNAPSHOT_CURSOR} NO SCROLL CURSOR FOR {query}");
430 let session = owned_batch_execute(session, declare, "declare snapshot cursor").await?;
431
432 self.session = Some(session);
433 self.state = State::Reading;
434 Ok(())
435 }
436
437 async fn read_snapshot_batch(&mut self) -> Result<Option<RecordBatch>, ConnectorError> {
438 let session = self.session.take().ok_or_else(|| {
439 ConnectorError::Internal("postgres reference session is not initialized".into())
440 })?;
441 let fetch = format!("FETCH FORWARD {SNAPSHOT_ROWS_PER_BATCH} FROM {SNAPSHOT_CURSOR}");
442 let (session, rows) = owned_query(session, fetch, "fetch snapshot cursor").await?;
443 self.session = Some(session);
444 let exhausted = rows.len() < SNAPSHOT_ROWS_PER_BATCH;
445
446 if rows.is_empty() {
447 debug_assert!(exhausted);
448 self.finish_snapshot().await?;
449 return Ok(None);
450 }
451
452 let columns = self
453 .declared_schema
454 .fields()
455 .iter()
456 .enumerate()
457 .map(|(index, field)| build_column(&rows, index, field.data_type()))
458 .collect::<Result<Vec<_>, _>>()?;
459 validate_reference_columns(self.declared_schema.as_ref(), &columns)?;
460 enforce_snapshot_batch_bytes(&columns)?;
461 let batch =
462 RecordBatch::try_new(self.declared_schema.clone(), columns).map_err(|error| {
463 ConnectorError::ReadError(format!(
464 "postgres snapshot does not satisfy the declared schema: {error}"
465 ))
466 })?;
467 if exhausted {
468 self.finish_snapshot().await?;
469 }
470 Ok(Some(batch))
471 }
472
473 async fn finish_snapshot(&mut self) -> Result<(), ConnectorError> {
474 let session = self.session.take().ok_or_else(|| {
475 ConnectorError::Internal("postgres reference session is not initialized".into())
476 })?;
477 let _session = owned_batch_execute(session, "COMMIT".into(), "finish snapshot").await?;
478 self.state = State::Done;
479 Ok(())
480 }
481
482 fn fail_closed(&mut self) {
483 self.session = None;
484 self.state = State::Closed;
485 }
486}
487
488#[async_trait::async_trait]
489impl ReferenceTableSource for PostgresReferenceTableSource {
490 async fn resolve_schema(
491 &mut self,
492 config: &ConnectorConfig,
493 explicit: Option<SchemaRef>,
494 ) -> Result<crate::schema::resolution::SchemaBinding, ConnectorError> {
495 let session = self.connect_session().await?;
496 let binding = tokio::time::timeout(
497 QUERY_TIMEOUT,
498 schema_resolution::resolve(&session.client, config, explicit),
499 )
500 .await
501 .map_err(|_| timeout_error(QUERY_TIMEOUT))??;
502 drop(session);
503 Ok(binding)
504 }
505
506 async fn poll_snapshot(&mut self) -> Result<Option<RecordBatch>, ConnectorError> {
507 match self.state {
508 State::Done => return Ok(None),
509 State::Closed => {
510 return Err(ConnectorError::InvalidState {
511 expected: "open reference snapshot source".into(),
512 actual: "closed".into(),
513 });
514 }
515 State::Ready | State::Reading => {}
516 }
517
518 let result = async {
519 if self.state == State::Ready {
520 self.start_snapshot().await?;
521 }
522 self.read_snapshot_batch().await
523 }
524 .await;
525 if result.is_err() {
526 self.fail_closed();
527 }
528 result
529 }
530
531 async fn close(&mut self) -> Result<(), ConnectorError> {
532 let session = if self.state == State::Reading {
533 self.session.take()
534 } else {
535 None
536 };
537 self.fail_closed();
540 if let Some(session) = session {
541 let _session =
542 owned_batch_execute(session, "ROLLBACK".into(), "close snapshot transaction")
543 .await?;
544 }
545 Ok(())
546 }
547}
548
549fn quote_qualified_identifier(identifier: &str) -> Result<String, ConnectorError> {
550 identifier
551 .split('.')
552 .map(quote_identifier)
553 .collect::<Result<Vec<_>, _>>()
554 .map(|parts| parts.join("."))
555}
556
557fn quote_identifier(identifier: &str) -> Result<String, ConnectorError> {
558 if identifier.is_empty() || identifier.contains('\0') {
559 return Err(ConnectorError::ConfigurationError(
560 "postgres identifiers must be non-empty and cannot contain NUL".into(),
561 ));
562 }
563 Ok(format!("\"{}\"", identifier.replace('"', "\"\"")))
564}
565
566fn validate_reference_columns(
567 schema: &Schema,
568 columns: &[Arc<dyn Array>],
569) -> Result<(), ConnectorError> {
570 for (field, column) in schema.fields().iter().zip(columns) {
571 if !field.is_nullable() && column.null_count() != 0 {
572 return Err(ConnectorError::ReadError(format!(
573 "postgres reference column '{}' contains {} null values but is declared NOT NULL",
574 field.name(),
575 column.null_count()
576 )));
577 }
578 }
579 Ok(())
580}
581
582fn enforce_snapshot_batch_bytes(columns: &[Arc<dyn Array>]) -> Result<(), ConnectorError> {
583 enforce_snapshot_batch_bytes_with_limit(columns, MAX_SNAPSHOT_BATCH_BYTES)
584}
585
586fn enforce_snapshot_batch_bytes_with_limit(
587 columns: &[Arc<dyn Array>],
588 limit: usize,
589) -> Result<(), ConnectorError> {
590 let bytes = columns.iter().try_fold(0_usize, |total, column| {
591 total
592 .checked_add(column.get_array_memory_size())
593 .ok_or_else(|| {
594 ConnectorError::ReadError("postgres snapshot byte count overflow".into())
595 })
596 })?;
597 if bytes > limit {
598 return Err(ConnectorError::ReadError(format!(
599 "postgres snapshot batch retains {bytes} bytes, exceeding the fixed {limit}-byte limit"
600 )));
601 }
602 Ok(())
603}
604
605fn reference_select_expression(
606 name: &str,
607 pg_type: &tokio_postgres::types::Type,
608 declared_type: &DataType,
609) -> Result<String, ConnectorError> {
610 let identifier = quote_identifier(name)?;
611 match postgres_type_to_arrow(pg_type) {
612 Some(source_type) if &source_type == declared_type => Ok(identifier),
613 None if declared_type == &DataType::Utf8 => {
614 Ok(format!("CAST({identifier} AS TEXT) AS {identifier}"))
615 }
616 Some(source_type) => Err(ConnectorError::ReadError(format!(
617 "postgres column '{name}' has type {source_type}, expected {declared_type}"
618 ))),
619 None => Err(ConnectorError::ReadError(format!(
620 "postgres column '{name}' has unsupported type {pg_type}"
621 ))),
622 }
623}
624
625fn validate_postgres_schema(
626 columns: &[tokio_postgres::Column],
627 declared_schema: &Schema,
628) -> Result<(), ConnectorError> {
629 if columns.len() != declared_schema.fields().len() {
630 return Err(ConnectorError::ReadError(format!(
631 "postgres snapshot has {} columns but {} were declared",
632 columns.len(),
633 declared_schema.fields().len()
634 )));
635 }
636
637 for (column, declared) in columns.iter().zip(declared_schema.fields()) {
638 let source_type = postgres_type_to_arrow(column.type_()).ok_or_else(|| {
639 ConnectorError::ReadError(format!(
640 "postgres column '{}' has unsupported type {}",
641 column.name(),
642 column.type_()
643 ))
644 })?;
645 if column.name() != declared.name() || &source_type != declared.data_type() {
646 return Err(ConnectorError::ReadError(format!(
647 "postgres column '{}' has type {source_type}, expected declared column '{}' with type {}",
648 column.name(),
649 declared.name(),
650 declared.data_type()
651 )));
652 }
653 }
654 Ok(())
655}
656
657fn build_column(
658 rows: &[tokio_postgres::Row],
659 index: usize,
660 data_type: &DataType,
661) -> Result<Arc<dyn Array>, ConnectorError> {
662 macro_rules! primitive {
663 ($native:ty, $array:ty) => {{
664 let values: Vec<Option<$native>> = collect_column(rows, index)?;
665 Arc::new(<$array>::from(values)) as Arc<dyn Array>
666 }};
667 }
668
669 let column = match data_type {
670 DataType::Boolean => primitive!(bool, arrow_array::BooleanArray),
671 DataType::Int16 => primitive!(i16, arrow_array::Int16Array),
672 DataType::Int32 => primitive!(i32, arrow_array::Int32Array),
673 DataType::Int64 => primitive!(i64, arrow_array::Int64Array),
674 DataType::Float32 => primitive!(f32, arrow_array::Float32Array),
675 DataType::Float64 => primitive!(f64, arrow_array::Float64Array),
676 DataType::Utf8 => {
677 let values: Vec<Option<String>> = collect_column(rows, index)?;
678 let values = values.iter().map(Option::as_deref).collect::<Vec<_>>();
679 Arc::new(arrow_array::StringArray::from(values))
680 }
681 DataType::Binary => {
682 let values: Vec<Option<Vec<u8>>> = collect_column(rows, index)?;
683 let values = values
684 .iter()
685 .map(|value| value.as_deref())
686 .collect::<Vec<_>>();
687 Arc::new(arrow_array::BinaryArray::from(values))
688 }
689 DataType::Date32 => {
690 let epoch = chrono::NaiveDate::from_ymd_opt(1970, 1, 1).expect("valid epoch");
691 let values: Vec<Option<chrono::NaiveDate>> = collect_column(rows, index)?;
692 let values = values
693 .into_iter()
694 .map(|value| {
695 value
696 .map(|date| {
697 i32::try_from(date.signed_duration_since(epoch).num_days()).map_err(
698 |_| {
699 ConnectorError::ReadError(
700 "postgres date is outside the Arrow Date32 range".into(),
701 )
702 },
703 )
704 })
705 .transpose()
706 })
707 .collect::<Result<Vec<_>, _>>()?;
708 Arc::new(arrow_array::Date32Array::from(values))
709 }
710 DataType::Timestamp(TimeUnit::Microsecond, None) => {
711 let values: Vec<Option<chrono::NaiveDateTime>> = collect_column(rows, index)?;
712 let values = values
713 .into_iter()
714 .map(|value| value.map(|timestamp| timestamp.and_utc().timestamp_micros()))
715 .collect::<Vec<_>>();
716 Arc::new(arrow_array::TimestampMicrosecondArray::from(values))
717 }
718 DataType::Timestamp(TimeUnit::Microsecond, Some(timezone))
719 if timezone.as_ref() == "UTC" =>
720 {
721 let values: Vec<Option<chrono::DateTime<chrono::Utc>>> = collect_column(rows, index)?;
722 let values = values
723 .into_iter()
724 .map(|value| value.map(|timestamp| timestamp.timestamp_micros()))
725 .collect::<Vec<_>>();
726 Arc::new(arrow_array::TimestampMicrosecondArray::from(values).with_timezone("UTC"))
727 }
728 unsupported => {
729 return Err(ConnectorError::ReadError(format!(
730 "declared PostgreSQL reference type {unsupported} is not supported"
731 )));
732 }
733 };
734 Ok(column)
735}
736
737fn collect_column<'a, T>(
738 rows: &'a [tokio_postgres::Row],
739 index: usize,
740) -> Result<Vec<Option<T>>, ConnectorError>
741where
742 T: tokio_postgres::types::FromSql<'a>,
743{
744 rows.iter()
745 .map(|row| {
746 row.try_get::<_, Option<T>>(index).map_err(|error| {
747 ConnectorError::ReadError(format!(
748 "postgres column {index} could not be decoded: {error}"
749 ))
750 })
751 })
752 .collect()
753}
754
755pub(super) fn postgres_type_to_arrow(pg_type: &tokio_postgres::types::Type) -> Option<DataType> {
756 use tokio_postgres::types::Type;
757
758 match *pg_type {
759 Type::BOOL => Some(DataType::Boolean),
760 Type::INT2 => Some(DataType::Int16),
761 Type::INT4 => Some(DataType::Int32),
762 Type::INT8 => Some(DataType::Int64),
763 Type::FLOAT4 => Some(DataType::Float32),
764 Type::FLOAT8 => Some(DataType::Float64),
765 Type::TEXT | Type::VARCHAR | Type::BPCHAR | Type::NAME => Some(DataType::Utf8),
766 Type::BYTEA => Some(DataType::Binary),
767 Type::DATE => Some(DataType::Date32),
768 Type::TIMESTAMP => Some(DataType::Timestamp(TimeUnit::Microsecond, None)),
769 Type::TIMESTAMPTZ => Some(DataType::Timestamp(
770 TimeUnit::Microsecond,
771 Some("UTC".into()),
772 )),
773 _ => None,
774 }
775}
776
777#[cfg(test)]
778mod tests;