1use crate::types::arrow_to_pg_type;
2use arrow::datatypes::{Schema, DataType};
3use arrow::record_batch::RecordBatch;
4use arrow::array::{
5 Int16Array, Int32Array, Int64Array, Float32Array, Float64Array, BooleanArray, LargeBinaryArray, LargeStringArray
6};
7use bytes::{BufMut, BytesMut};
8use futures::{Stream, StreamExt};
9use sqlx::{PgConnection, Executor};
10use anyhow::{anyhow, Result};
11
12pub fn generate_create_table_ddl(table: &str, schema: &Schema) -> Result<String> {
14 let mut column_defs = Vec::new();
15 for field in schema.fields() {
16 let pg_type = arrow_to_pg_type(field.data_type())?;
17 let not_null = if field.is_nullable() { "" } else { " NOT NULL" };
18 column_defs.push(format!("\"{}\" {}{}", field.name(), pg_type, not_null));
19 }
20 let ddl = format!("CREATE TABLE \"{}\" ({});", table, column_defs.join(", "));
21 Ok(ddl)
22}
23
24pub async fn arrow_to_table(
26 conn: &mut PgConnection,
27 table: &str,
28 create_table: bool,
29 schema: &Schema,
30 mut stream: impl Stream<Item = Result<RecordBatch>> + Unpin,
31) -> Result<()> {
32 if create_table {
33 let ddl = generate_create_table_ddl(table, schema)?;
34 conn.execute(ddl.as_str()).await?;
35 }
36
37 let copy_query = format!("COPY \"{}\" FROM STDIN WITH (FORMAT binary)", table);
38 let mut copy_in = conn.copy_in_raw(copy_query.as_str()).await?;
39
40 let mut buf = BytesMut::new();
41 buf.extend_from_slice(b"PGCOPY\n\xff\r\n\0");
42 buf.put_i32(0);
43 buf.put_i32(0);
44 copy_in.send(buf.split().freeze()).await?;
45
46 let num_cols = schema.fields().len();
47
48 while let Some(batch) = stream.next().await {
49 let batch = batch?;
50 let rows = batch.num_rows();
51
52 for r in 0..rows {
53 buf.put_i16(num_cols as i16);
54 for c in 0..num_cols {
55 let col = batch.column(c);
56 if col.is_null(r) {
57 buf.put_i32(-1);
58 } else {
59 match schema.field(c).data_type() {
60 DataType::Int16 => {
61 let arr = col.as_any().downcast_ref::<Int16Array>().unwrap();
62 buf.put_i32(2);
63 buf.put_i16(arr.value(r));
64 }
65 DataType::Int32 => {
66 let arr = col.as_any().downcast_ref::<Int32Array>().unwrap();
67 buf.put_i32(4);
68 buf.put_i32(arr.value(r));
69 }
70 DataType::Int64 => {
71 let arr = col.as_any().downcast_ref::<Int64Array>().unwrap();
72 buf.put_i32(8);
73 buf.put_i64(arr.value(r));
74 }
75 DataType::Float32 => {
76 let arr = col.as_any().downcast_ref::<Float32Array>().unwrap();
77 buf.put_i32(4);
78 buf.put_f32(arr.value(r));
79 }
80 DataType::Float64 => {
81 let arr = col.as_any().downcast_ref::<Float64Array>().unwrap();
82 buf.put_i32(8);
83 buf.put_f64(arr.value(r));
84 }
85 DataType::Boolean => {
86 let arr = col.as_any().downcast_ref::<BooleanArray>().unwrap();
87 buf.put_i32(1);
88 buf.put_u8(if arr.value(r) { 1 } else { 0 });
89 }
90 DataType::LargeBinary => {
91 let arr = col.as_any().downcast_ref::<LargeBinaryArray>().unwrap();
92 let val = arr.value(r);
93 buf.put_i32(val.len() as i32);
94 buf.extend_from_slice(val);
95 }
96 DataType::LargeUtf8 => {
97 let arr = col.as_any().downcast_ref::<LargeStringArray>().unwrap();
98 let val = arr.value(r);
99 buf.put_i32(val.len() as i32);
100 buf.extend_from_slice(val.as_bytes());
101 }
102 DataType::Utf8 => {
103 let arr = col.as_any().downcast_ref::<arrow::array::StringArray>().unwrap();
104 let val = arr.value(r);
105 buf.put_i32(val.len() as i32);
106 buf.extend_from_slice(val.as_bytes());
107 }
108 _ => {
109 return Err(anyhow!("Unsupported type in import: {:?}", schema.field(c).data_type()));
110 }
111 }
112 }
113 }
114 if buf.len() > 8192 {
115 copy_in.send(buf.split().freeze()).await?;
116 }
117 }
118 }
119
120 buf.put_i16(-1);
121 copy_in.send(buf.split().freeze()).await?;
122 copy_in.finish().await?;
123
124 Ok(())
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use arrow::datatypes::{DataType, Field, TimeUnit};
131
132 #[test]
133 fn test_generate_create_table_ddl_basic() {
134 let fields = vec![
135 Field::new("id", DataType::Int32, false),
136 Field::new("name", DataType::Utf8, true),
137 Field::new("score", DataType::Float64, true),
138 Field::new("active", DataType::Boolean, false),
139 ];
140 let schema = Schema::new(fields);
141 let ddl = generate_create_table_ddl("users", &schema).unwrap();
142 assert_eq!(
143 ddl,
144 "CREATE TABLE \"users\" (\"id\" INT4 NOT NULL, \"name\" TEXT, \"score\" FLOAT8, \"active\" BOOL NOT NULL);"
145 );
146 }
147
148 #[test]
149 fn test_generate_create_table_ddl_all_types() {
150 let fields = vec![
151 Field::new("col_i16", DataType::Int16, false),
152 Field::new("col_i32", DataType::Int32, false),
153 Field::new("col_i64", DataType::Int64, false),
154 Field::new("col_f32", DataType::Float32, true),
155 Field::new("col_f64", DataType::Float64, true),
156 Field::new("col_bool", DataType::Boolean, false),
157 Field::new("col_utf8", DataType::Utf8, true),
158 Field::new("col_large_utf8", DataType::LargeUtf8, true),
159 Field::new("col_bin", DataType::Binary, true),
160 Field::new("col_large_bin", DataType::LargeBinary, true),
161 Field::new("col_date", DataType::Date32, true),
162 Field::new("col_time", DataType::Time64(TimeUnit::Microsecond), true),
163 Field::new("col_ts", DataType::Timestamp(TimeUnit::Microsecond, None), true),
164 Field::new(
165 "col_tstz",
166 DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
167 true,
168 ),
169 Field::new("col_decimal", DataType::Decimal128(38, 9), true),
170 ];
171 let schema = Schema::new(fields);
172 let ddl = generate_create_table_ddl("all_types", &schema).unwrap();
173 assert_eq!(
174 ddl,
175 "CREATE TABLE \"all_types\" (\
176 \"col_i16\" INT2 NOT NULL, \
177 \"col_i32\" INT4 NOT NULL, \
178 \"col_i64\" INT8 NOT NULL, \
179 \"col_f32\" FLOAT4, \
180 \"col_f64\" FLOAT8, \
181 \"col_bool\" BOOL NOT NULL, \
182 \"col_utf8\" TEXT, \
183 \"col_large_utf8\" TEXT, \
184 \"col_bin\" BYTEA, \
185 \"col_large_bin\" BYTEA, \
186 \"col_date\" DATE, \
187 \"col_time\" TIME, \
188 \"col_ts\" TIMESTAMP, \
189 \"col_tstz\" TIMESTAMPTZ, \
190 \"col_decimal\" NUMERIC);"
191 );
192 }
193
194 #[test]
195 fn test_generate_create_table_ddl_unsupported_type() {
196 let fields = vec![Field::new("invalid_col", DataType::Null, true)];
197 let schema = Schema::new(fields);
198 let err = generate_create_table_ddl("invalid_table", &schema);
199 assert!(err.is_err());
200 }
201
202 #[test]
203 fn test_import_interface_signature() {
204 fn _assert_signature<'a, S>(_stream: S)
206 where
207 S: Stream<Item = Result<RecordBatch>> + Unpin + 'a,
208 {
209 let _ = |conn: &'a mut PgConnection,
210 table: &'a str,
211 create_table: bool,
212 schema: &'a Schema,
213 stream: S| {
214 arrow_to_table(conn, table, create_table, schema, stream)
215 };
216 }
217
218 _assert_signature(futures::stream::empty::<Result<RecordBatch>>());
219 }
220}