1use arrow_schema::{DataType, TimeUnit};
4
5use crate::error::ConnectorError;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub(super) struct PostgresType {
10 sql: &'static str,
11 ddl: &'static str,
12 parameter: Option<&'static str>,
15}
16
17impl PostgresType {
18 const fn new(sql: &'static str, ddl: &'static str) -> Self {
19 Self {
20 sql,
21 ddl,
22 parameter: None,
23 }
24 }
25
26 #[must_use]
27 pub(super) const fn sql(self) -> &'static str {
28 self.sql
29 }
30}
31
32pub(super) fn postgres_type(data_type: &DataType) -> Result<PostgresType, ConnectorError> {
37 let mapping = match data_type {
38 DataType::Boolean => PostgresType::new("bool", "BOOLEAN"),
39 DataType::Int8 | DataType::UInt8 | DataType::Int16 => PostgresType::new("int2", "SMALLINT"),
40 DataType::UInt16 | DataType::Int32 => PostgresType::new("int4", "INTEGER"),
41 DataType::UInt32 | DataType::Int64 | DataType::UInt64 => {
42 PostgresType::new("int8", "BIGINT")
43 }
44 DataType::Float32 => PostgresType::new("float4", "REAL"),
45 DataType::Float64 => PostgresType::new("float8", "DOUBLE PRECISION"),
46 DataType::Utf8 | DataType::LargeUtf8 => PostgresType::new("text", "TEXT"),
47 DataType::Binary | DataType::LargeBinary => PostgresType::new("bytea", "BYTEA"),
48 DataType::Date32 => PostgresType::new("date", "DATE"),
49 DataType::Decimal128(_, scale) if *scale >= 0 => PostgresType {
50 parameter: Some("text"),
51 ..PostgresType::new("numeric", "NUMERIC")
52 },
53 DataType::Timestamp(
54 TimeUnit::Second | TimeUnit::Millisecond | TimeUnit::Microsecond,
55 None,
56 ) => PostgresType::new("timestamp", "TIMESTAMP"),
57 DataType::Timestamp(
58 TimeUnit::Second | TimeUnit::Millisecond | TimeUnit::Microsecond,
59 Some(_),
60 ) => PostgresType::new("timestamptz", "TIMESTAMPTZ"),
61 unsupported => {
62 return Err(ConnectorError::ConfigurationError(format!(
63 "PostgreSQL sink does not support Arrow type {unsupported:?}; supported types are \
64 Boolean, signed integers, UInt8/16/32, range-checked UInt64, Float32/64, \
65 Utf8/LargeUtf8, Binary/LargeBinary, Date32, Decimal128 with a non-negative \
66 scale, and second/millisecond/microsecond Timestamp"
67 )));
68 }
69 };
70 Ok(mapping)
71}
72
73pub(super) fn arrow_to_pg_ddl_type(data_type: &DataType) -> Result<String, ConnectorError> {
75 let mapping = postgres_type(data_type)?;
76 Ok(match data_type {
77 DataType::Decimal128(precision, scale) => format!("NUMERIC({precision}, {scale})"),
78 _ => mapping.ddl.to_string(),
79 })
80}
81
82pub(super) fn arrow_type_to_pg_array_cast(
84 data_type: &DataType,
85 parameter: usize,
86) -> Result<String, ConnectorError> {
87 let mapping = postgres_type(data_type)?;
88 Ok(match mapping.parameter {
89 Some(wire) => format!("${parameter}::{wire}[]::{}[]", mapping.sql),
90 None => format!("${parameter}::{}[]", mapping.sql),
91 })
92}
93
94pub(super) fn numeric_column_holds(data_type: &DataType, type_modifier: i32) -> bool {
96 let DataType::Decimal128(precision, scale) = data_type else {
97 return true;
98 };
99 if type_modifier == -1 {
100 return true;
101 }
102 let packed = type_modifier - 4;
104 let column_precision = (packed >> 16) & 0xffff;
105 let column_scale = ((packed & 0x7ff) ^ 1024) - 1024;
106 let (precision, scale) = (i32::from(*precision), i32::from(*scale));
107 column_scale >= scale && column_precision - column_scale >= precision - scale
108}
109
110#[cfg(feature = "postgres-sink")]
115fn numeric_wire(value: i128, scale: u8, buffer: &mut Vec<u8>) {
116 let scale = usize::from(scale);
117 let mut digits = value.unsigned_abs().to_string();
118 if digits.len() <= scale {
119 digits.insert_str(0, &"0".repeat(scale + 1 - digits.len()));
120 }
121 let (integer, fraction) = digits.split_at(digits.len() - scale);
122 let integer = format!("{}{integer}", "0".repeat((4 - integer.len() % 4) % 4));
123 let fraction = format!("{fraction}{}", "0".repeat((4 - fraction.len() % 4) % 4));
124 let group = |chunk: &[u8]| {
125 chunk
126 .iter()
127 .fold(0_i16, |total, digit| total * 10 + i16::from(digit - b'0'))
128 };
129 let mut groups: Vec<i16> = integer
130 .as_bytes()
131 .chunks(4)
132 .chain(fraction.as_bytes().chunks(4))
133 .map(group)
134 .collect();
135 let mut weight = i16::try_from(integer.len() / 4).unwrap_or(i16::MAX) - 1;
136 let leading = groups.iter().take_while(|group| **group == 0).count();
137 groups.drain(..leading);
138 weight -= i16::try_from(leading).unwrap_or(i16::MAX);
139 while groups.last() == Some(&0) {
140 groups.pop();
141 }
142 if groups.is_empty() {
143 weight = 0;
144 }
145 let sign: i16 = if value < 0 && !groups.is_empty() {
146 0x4000
147 } else {
148 0
149 };
150 let header = [
152 i16::try_from(groups.len()).unwrap_or(i16::MAX),
153 weight,
154 sign,
155 i16::try_from(scale).unwrap_or(i16::MAX),
156 ];
157 for word in header.into_iter().chain(groups) {
158 buffer.extend_from_slice(&word.to_be_bytes());
159 }
160}
161
162#[cfg(feature = "postgres-sink")]
163fn checked_u64(value: u64, row: usize) -> Result<i64, ConnectorError> {
164 i64::try_from(value).map_err(|_| {
165 ConnectorError::SchemaMismatch(format!(
166 "PostgreSQL BIGINT cannot represent UInt64 value {value} at row {row}"
167 ))
168 })
169}
170
171#[cfg(feature = "postgres-sink")]
172fn checked_date32(value: i32, row: usize) -> Result<chrono::NaiveDate, ConnectorError> {
173 let epoch = chrono::NaiveDate::from_ymd_opt(1970, 1, 1).expect("valid Unix epoch");
174 epoch
175 .checked_add_signed(chrono::Duration::days(i64::from(value)))
176 .ok_or_else(|| {
177 ConnectorError::SchemaMismatch(format!(
178 "PostgreSQL sink cannot represent Date32 value {value} at row {row}"
179 ))
180 })
181}
182
183#[cfg(feature = "postgres-sink")]
185pub(super) fn validate_postgres_array_values(
186 array: &dyn arrow_array::Array,
187) -> Result<(), ConnectorError> {
188 use arrow_array::{Array as _, Date32Array, UInt64Array};
189
190 postgres_type(array.data_type())?;
191 match array.data_type() {
192 DataType::UInt64 => {
193 let values = array
194 .as_any()
195 .downcast_ref::<UInt64Array>()
196 .ok_or_else(|| ConnectorError::Internal("downcast to UInt64Array failed".into()))?;
197 for row in 0..values.len() {
198 if !values.is_null(row) {
199 checked_u64(values.value(row), row)?;
200 }
201 }
202 }
203 DataType::Date32 => {
204 let values = array
205 .as_any()
206 .downcast_ref::<Date32Array>()
207 .ok_or_else(|| ConnectorError::Internal("downcast to Date32Array failed".into()))?;
208 for row in 0..values.len() {
209 if !values.is_null(row) {
210 checked_date32(values.value(row), row)?;
211 }
212 }
213 }
214 DataType::Timestamp(unit, _) => validate_timestamp_values(array, *unit)?,
215 _ => {}
216 }
217 Ok(())
218}
219
220#[cfg(feature = "postgres-sink")]
230pub(super) fn postgres_copy_batch(
231 batch: &arrow_array::RecordBatch,
232) -> Result<arrow_array::RecordBatch, ConnectorError> {
233 use std::sync::Arc;
234
235 use arrow_array::{Array as _, ArrayRef, Int64Array, UInt64Array};
236 use arrow_schema::Schema;
237
238 let mut changed = false;
239 let mut fields = Vec::with_capacity(batch.num_columns());
240 let mut columns: Vec<ArrayRef> = Vec::with_capacity(batch.num_columns());
241 for (field, column) in batch.schema().fields().iter().zip(batch.columns()) {
242 postgres_type(column.data_type())?;
243 if let DataType::Decimal128(_, scale) = field.data_type() {
244 let values = column
245 .as_any()
246 .downcast_ref::<arrow_array::Decimal128Array>()
247 .ok_or_else(|| {
248 ConnectorError::Internal("downcast to Decimal128Array failed".into())
249 })?;
250 let scale = u8::try_from(*scale).map_err(|_| {
251 ConnectorError::Internal("negative decimal scale passed type admission".into())
252 })?;
253 let mut builder =
254 arrow_array::builder::BinaryBuilder::with_capacity(values.len(), values.len() * 16);
255 let mut wire = Vec::with_capacity(32);
256 for row in 0..values.len() {
257 if values.is_null(row) {
258 builder.append_null();
259 } else {
260 wire.clear();
261 numeric_wire(values.value(row), scale, &mut wire);
262 builder.append_value(&wire);
263 }
264 }
265 fields.push(Arc::new(
266 field.as_ref().clone().with_data_type(DataType::Binary),
267 ));
268 columns.push(Arc::new(builder.finish()));
269 changed = true;
270 } else if field.data_type() == &DataType::UInt64 {
271 let values = column
272 .as_any()
273 .downcast_ref::<UInt64Array>()
274 .ok_or_else(|| ConnectorError::Internal("downcast to UInt64Array failed".into()))?;
275 let converted = (0..values.len())
276 .map(|row| {
277 if values.is_null(row) {
278 Ok(None)
279 } else {
280 checked_u64(values.value(row), row).map(Some)
281 }
282 })
283 .collect::<Result<Vec<Option<i64>>, ConnectorError>>()?;
284 fields.push(Arc::new(
285 field.as_ref().clone().with_data_type(DataType::Int64),
286 ));
287 columns.push(Arc::new(Int64Array::from(converted)));
288 changed = true;
289 } else {
290 fields.push(field.clone());
291 columns.push(column.clone());
292 }
293 }
294
295 if changed {
296 arrow_array::RecordBatch::try_new(Arc::new(Schema::new(fields)), columns).map_err(|error| {
297 ConnectorError::Internal(format!("build PostgreSQL COPY batch: {error}"))
298 })
299 } else {
300 Ok(batch.clone())
301 }
302}
303
304#[cfg(feature = "postgres-sink")]
305fn validate_timestamp_values(
306 array: &dyn arrow_array::Array,
307 unit: TimeUnit,
308) -> Result<(), ConnectorError> {
309 use arrow_array::{
310 Array as _, TimestampMicrosecondArray, TimestampMillisecondArray, TimestampSecondArray,
311 };
312
313 macro_rules! validate {
314 ($array_type:ty) => {{
315 let values = array
316 .as_any()
317 .downcast_ref::<$array_type>()
318 .ok_or_else(|| {
319 ConnectorError::Internal(format!(
320 "timestamp array does not match declared unit {unit:?}"
321 ))
322 })?;
323 for row in 0..values.len() {
324 if !values.is_null(row) && to_naive_datetime(values.value(row), unit).is_none() {
325 return Err(ConnectorError::SchemaMismatch(format!(
326 "PostgreSQL sink cannot represent {unit:?} timestamp value {} at row {row}",
327 values.value(row)
328 )));
329 }
330 }
331 }};
332 }
333
334 match unit {
335 TimeUnit::Second => validate!(TimestampSecondArray),
336 TimeUnit::Millisecond => validate!(TimestampMillisecondArray),
337 TimeUnit::Microsecond => validate!(TimestampMicrosecondArray),
338 TimeUnit::Nanosecond => unreachable!("nanosecond timestamps fail type admission"),
339 }
340 Ok(())
341}
342
343#[cfg(feature = "postgres-sink")]
345pub(super) fn arrow_column_to_pg_array(
346 array: &dyn arrow_array::Array,
347) -> Result<Box<dyn postgres_types::ToSql + Sync + Send>, ConnectorError> {
348 use arrow_array::{
349 Array as _, BinaryArray, BooleanArray, Date32Array, Decimal128Array, Float32Array,
350 Float64Array, Int16Array, Int32Array, Int64Array, Int8Array, LargeBinaryArray,
351 LargeStringArray, StringArray, TimestampMicrosecondArray, TimestampMillisecondArray,
352 TimestampSecondArray, UInt16Array, UInt32Array, UInt64Array, UInt8Array,
353 };
354
355 postgres_type(array.data_type())?;
356
357 macro_rules! nullable {
359 ($array_type:ty, $value:expr) => {{
360 let values = array
361 .as_any()
362 .downcast_ref::<$array_type>()
363 .ok_or_else(|| {
364 ConnectorError::Internal(format!(
365 "downcast to {} failed",
366 stringify!($array_type)
367 ))
368 })?;
369 Ok(Box::new(
370 (0..values.len())
371 .map(|row| (!values.is_null(row)).then(|| $value(values, row)))
372 .collect::<Vec<Option<_>>>(),
373 ))
374 }};
375 }
376
377 macro_rules! widen {
378 ($array_type:ty, $target:ty) => {{
379 let values = array
380 .as_any()
381 .downcast_ref::<$array_type>()
382 .ok_or_else(|| {
383 ConnectorError::Internal(format!(
384 "downcast to {} failed",
385 stringify!($array_type)
386 ))
387 })?;
388 let converted: Vec<Option<$target>> = (0..values.len())
389 .map(|row| (!values.is_null(row)).then(|| <$target>::from(values.value(row))))
390 .collect();
391 Ok(Box::new(converted))
392 }};
393 }
394
395 match array.data_type() {
396 DataType::Boolean => nullable!(BooleanArray, |v: &BooleanArray, row| v.value(row)),
397 DataType::Int8 => widen!(Int8Array, i16),
398 DataType::UInt8 => widen!(UInt8Array, i16),
399 DataType::Int16 => widen!(Int16Array, i16),
400 DataType::UInt16 => widen!(UInt16Array, i32),
401 DataType::Int32 => widen!(Int32Array, i32),
402 DataType::UInt32 => widen!(UInt32Array, i64),
403 DataType::Int64 => widen!(Int64Array, i64),
404 DataType::UInt64 => {
405 let values = array
406 .as_any()
407 .downcast_ref::<UInt64Array>()
408 .ok_or_else(|| ConnectorError::Internal("downcast to UInt64Array failed".into()))?;
409 let converted = (0..values.len())
410 .map(|row| {
411 if values.is_null(row) {
412 Ok(None)
413 } else {
414 checked_u64(values.value(row), row).map(Some)
415 }
416 })
417 .collect::<Result<Vec<Option<i64>>, ConnectorError>>()?;
418 Ok(Box::new(converted))
419 }
420 DataType::Float32 => widen!(Float32Array, f32),
421 DataType::Float64 => widen!(Float64Array, f64),
422 DataType::Decimal128(_, _) => nullable!(Decimal128Array, |v: &Decimal128Array, row| v
424 .value_as_string(row)),
425 DataType::Utf8 => nullable!(StringArray, |v: &StringArray, row| v.value(row).to_owned()),
426 DataType::LargeUtf8 => nullable!(LargeStringArray, |v: &LargeStringArray, row| v
427 .value(row)
428 .to_owned()),
429 DataType::Binary => nullable!(BinaryArray, |v: &BinaryArray, row| v.value(row).to_vec()),
430 DataType::LargeBinary => nullable!(LargeBinaryArray, |v: &LargeBinaryArray, row| v
431 .value(row)
432 .to_vec()),
433 DataType::Date32 => {
434 let values = array
435 .as_any()
436 .downcast_ref::<Date32Array>()
437 .ok_or_else(|| ConnectorError::Internal("downcast to Date32Array failed".into()))?;
438 let converted = (0..values.len())
439 .map(|row| {
440 if values.is_null(row) {
441 Ok(None)
442 } else {
443 checked_date32(values.value(row), row).map(Some)
444 }
445 })
446 .collect::<Result<Vec<Option<chrono::NaiveDate>>, ConnectorError>>()?;
447 Ok(Box::new(converted))
448 }
449 DataType::Timestamp(unit, timezone) => {
450 macro_rules! timestamps {
451 ($array_type:ty) => {{
452 let values = array
453 .as_any()
454 .downcast_ref::<$array_type>()
455 .ok_or_else(|| {
456 ConnectorError::Internal(format!(
457 "timestamp array does not match declared unit {unit:?}"
458 ))
459 })?;
460 (0..values.len())
461 .map(|row| {
462 if values.is_null(row) {
463 Ok(None)
464 } else {
465 to_naive_datetime(values.value(row), *unit)
466 .map(Some)
467 .ok_or_else(|| {
468 ConnectorError::SchemaMismatch(format!(
469 "PostgreSQL sink cannot represent {unit:?} timestamp \
470 value {} at row {row}",
471 values.value(row)
472 ))
473 })
474 }
475 })
476 .collect::<Result<Vec<Option<chrono::NaiveDateTime>>, ConnectorError>>()?
477 }};
478 }
479
480 let values = match unit {
481 TimeUnit::Second => timestamps!(TimestampSecondArray),
482 TimeUnit::Millisecond => timestamps!(TimestampMillisecondArray),
483 TimeUnit::Microsecond => timestamps!(TimestampMicrosecondArray),
484 TimeUnit::Nanosecond => unreachable!("nanosecond timestamps fail type admission"),
485 };
486 if timezone.is_some() {
487 Ok(Box::new(
488 values
489 .into_iter()
490 .map(|value| value.map(|timestamp| timestamp.and_utc()))
491 .collect::<Vec<Option<chrono::DateTime<chrono::Utc>>>>(),
492 ))
493 } else {
494 Ok(Box::new(values))
495 }
496 }
497 unsupported => Err(ConnectorError::ConfigurationError(format!(
498 "PostgreSQL sink does not support Arrow type {unsupported:?}"
499 ))),
500 }
501}
502
503#[cfg(feature = "postgres-sink")]
506fn to_naive_datetime(value: i64, unit: TimeUnit) -> Option<chrono::NaiveDateTime> {
507 let (units_per_second, nanos_per_unit) = match unit {
508 TimeUnit::Second => (1_i64, 1_000_000_000_u32),
509 TimeUnit::Millisecond => (1_000, 1_000_000),
510 TimeUnit::Microsecond => (1_000_000, 1_000),
511 TimeUnit::Nanosecond => (1_000_000_000, 1),
512 };
513 let seconds = value.div_euclid(units_per_second);
514 let fraction = u32::try_from(value.rem_euclid(units_per_second)).ok()?;
515 let nanos = fraction.checked_mul(nanos_per_unit)?;
516 chrono::DateTime::from_timestamp(seconds, nanos).map(|timestamp| timestamp.naive_utc())
517}
518
519#[cfg(test)]
520mod tests;