indexlake_catalog_postgres/
catalog.rs1use arrow::array::RecordBatch;
2use arrow::datatypes::Schema;
3use bb8::Pool;
4use bb8_postgres::PostgresConnectionManager;
5use bb8_postgres::tokio_postgres::NoTls;
6use bb8_postgres::tokio_postgres::binary_copy::BinaryCopyInWriter;
7use bb8_postgres::tokio_postgres::types::ToSql;
8use futures::StreamExt;
9use indexlake::catalog::{
10 Catalog, CatalogDataType, CatalogDatabase, CatalogSchemaRef, Row, RowStream, Scalar,
11 Transaction,
12};
13use indexlake::expr::{BinaryExpr, Expr};
14use indexlake::{ILError, ILResult};
15use log::{error, trace};
16use uuid::Uuid;
17
18use crate::SqlDisplay;
19use crate::types::{catalog_data_type_to_pg_type, scalar_to_pg_value};
20
21#[derive(Debug, Clone)]
22pub struct PostgresCatalog {
23 pool: Pool<PostgresConnectionManager<NoTls>>,
24}
25
26impl PostgresCatalog {
27 pub fn new(pool: Pool<PostgresConnectionManager<NoTls>>) -> Self {
28 Self { pool }
29 }
30}
31
32#[async_trait::async_trait]
33impl Catalog for PostgresCatalog {
34 fn database(&self) -> CatalogDatabase {
35 CatalogDatabase::Postgres
36 }
37
38 async fn query(&self, sql: &str, schema: CatalogSchemaRef) -> ILResult<RowStream<'static>> {
39 trace!("postgres query: {sql}");
40 let conn = self
41 .pool
42 .get_owned()
43 .await
44 .map_err(|e| ILError::catalog(format!("failed to get postgres connection: {e}")))?;
45 let pg_row_stream = conn
46 .query_raw(sql, Vec::<String>::new())
47 .await
48 .map_err(|e| {
49 ILError::catalog(format!("failed to query postgres: {} {e}", SqlDisplay(sql)))
50 })?;
51
52 let stream = pg_row_stream.map(move |row| {
53 let pg_row =
54 row.map_err(|e| ILError::catalog(format!("failed to get postgres row: {e}")))?;
55 pg_row_to_row(&pg_row, &schema)
56 });
57 Ok(Box::pin(stream))
58 }
59
60 async fn transaction(&self) -> ILResult<Box<dyn Transaction>> {
61 let conn = self
62 .pool
63 .get_owned()
64 .await
65 .map_err(|e| ILError::catalog(format!("failed to get postgres connection: {e}")))?;
66 conn.batch_execute("START TRANSACTION")
67 .await
68 .map_err(|e| ILError::catalog(format!("failed to start postgres txn: {e}")))?;
69 Ok(Box::new(PostgresTransaction {
70 conn: Some(conn),
71 done: false,
72 }))
73 }
74
75 async fn truncate(&self, table_name: &str) -> ILResult<()> {
76 let conn = self
77 .pool
78 .get()
79 .await
80 .map_err(|e| ILError::catalog(format!("failed to get postgres connection: {e}")))?;
81 conn.execute(
82 &format!("TRUNCATE TABLE {}", self.sql_identifier(table_name)),
83 &[],
84 )
85 .await
86 .map(|r| r as usize)
87 .map_err(|e| {
88 ILError::catalog(format!(
89 "failed to truncate table {table_name} on postgres: {e}"
90 ))
91 })?;
92 Ok(())
93 }
94
95 async fn size(&self, table_name: &str) -> ILResult<usize> {
96 let conn = self
97 .pool
98 .get()
99 .await
100 .map_err(|e| ILError::catalog(format!("failed to get postgres connection: {e}")))?;
101 let row = conn
102 .query_one(
103 &format!("SELECT pg_total_relation_size('{table_name}')"),
104 &[],
105 )
106 .await
107 .map_err(|e| {
108 ILError::catalog(format!(
109 "failed to get size of table {table_name} on postgres: {e}"
110 ))
111 })?;
112 let size = row
113 .try_get::<_, Option<i64>>(0)
114 .map_err(|e| ILError::storage(format!("Failed to get size from row: {e}")))?;
115 Ok(size.unwrap_or(0) as usize)
116 }
117
118 fn sql_identifier(&self, ident: &str) -> String {
119 format!("\"{ident}\"")
120 }
121
122 fn sql_binary_literal(&self, value: &[u8]) -> String {
123 format!("E'\\\\x{}'", hex::encode(value))
124 }
125
126 fn sql_uuid_literal(&self, value: &Uuid) -> String {
127 format!("'{value}'")
128 }
129
130 fn sql_string_literal(&self, value: &str) -> String {
131 let value = value.replace("'", "''");
132 format!("'{value}'")
133 }
134
135 fn supports_filter(&self, filter: &Expr, _schema: &Schema) -> ILResult<bool> {
137 match filter {
138 Expr::Function(_) => Ok(false),
139 Expr::BinaryExpr(BinaryExpr { left, right, .. }) => {
140 if let Expr::Column(_) = left.as_ref()
141 && let Expr::Literal(lit) = right.as_ref()
142 && matches!(lit.value, Scalar::List(_))
143 {
144 Ok(false)
145 } else if let Expr::Literal(lit) = left.as_ref()
146 && let Expr::Column(_) = right.as_ref()
147 && matches!(lit.value, Scalar::List(_))
148 {
149 Ok(false)
150 } else {
151 Ok(true)
152 }
153 }
154 _ => Ok(true),
155 }
156 }
157
158 fn unparse_expr(&self, expr: &Expr) -> ILResult<String> {
159 match expr {
160 Expr::Column(name) => Ok(self.sql_identifier(name)),
161 Expr::Literal(literal) => literal.value.to_sql(self),
162 Expr::BinaryExpr(binary_expr) => {
163 let left = self.unparse_expr(&binary_expr.left)?;
164 let right = self.unparse_expr(&binary_expr.right)?;
165 Ok(format!("({} {} {})", left, binary_expr.op, right))
166 }
167 Expr::Not(expr) => Ok(format!("NOT {}", self.unparse_expr(expr)?)),
168 Expr::IsNull(expr) => Ok(format!("{} IS NULL", self.unparse_expr(expr)?)),
169 Expr::IsNotNull(expr) => Ok(format!("{} IS NOT NULL", self.unparse_expr(expr)?)),
170 Expr::InList(in_list) => {
171 let list = in_list
172 .list
173 .iter()
174 .map(|expr| self.unparse_expr(expr))
175 .collect::<ILResult<Vec<_>>>()?
176 .join(", ");
177 Ok(format!(
178 "{} IN ({})",
179 self.unparse_expr(&in_list.expr)?,
180 list
181 ))
182 }
183 Expr::Function(_) => Err(ILError::invalid_input(
184 "Function can only be used for index",
185 )),
186 Expr::Like(like) => {
187 let expr = self.unparse_expr(&like.expr)?;
188 let pattern = self.unparse_expr(&like.pattern)?;
189 match (like.negated, like.case_insensitive) {
190 (true, true) => Ok(format!("{expr} NOT ILIKE {pattern}")),
191 (true, false) => Ok(format!("{expr} NOT LIKE {pattern}")),
192 (false, true) => Ok(format!("{expr} ILIKE {pattern}")),
193 (false, false) => Ok(format!("{expr} LIKE {pattern}")),
194 }
195 }
196 Expr::Cast(cast) => {
197 let catalog_datatype = CatalogDataType::from_arrow(&cast.cast_type)?;
198 let expr_sql = self.unparse_expr(&cast.expr)?;
199 Ok(format!(
200 "CAST({} AS {})",
201 expr_sql,
202 self.unparse_catalog_data_type(catalog_datatype),
203 ))
204 }
205 Expr::TryCast(_) => Err(ILError::invalid_input("TRY_CAST is not supported in SQL")),
206 Expr::Negative(expr) => Ok(format!("-{}", self.unparse_expr(expr)?)),
207 Expr::Case(case) => {
208 let mut sql = String::new();
209 sql.push_str("CASE");
210 for (when, then) in &case.when_then {
211 sql.push_str(&format!(
212 " WHEN {} THEN {}",
213 self.unparse_expr(when)?,
214 self.unparse_expr(then)?,
215 ));
216 }
217 if let Some(else_expr) = &case.else_expr {
218 sql.push_str(&format!(" ELSE {}", self.unparse_expr(else_expr)?));
219 }
220 sql.push_str(" END");
221 Ok(sql)
222 }
223 }
224 }
225
226 fn unparse_catalog_data_type(&self, data_type: CatalogDataType) -> String {
227 match data_type {
228 CatalogDataType::Boolean => "BOOLEAN".to_string(),
229 CatalogDataType::Int8 => "SMALLINT".to_string(),
230 CatalogDataType::Int16 => "SMALLINT".to_string(),
231 CatalogDataType::Int32 => "INTEGER".to_string(),
232 CatalogDataType::Int64 => "BIGINT".to_string(),
233 CatalogDataType::UInt8 => "SMALLINT".to_string(),
234 CatalogDataType::UInt16 => "INTEGER".to_string(),
235 CatalogDataType::UInt32 => "BIGINT".to_string(),
236 CatalogDataType::UInt64 => "FLOAT4".to_string(),
237 CatalogDataType::Float32 => "FLOAT4".to_string(),
238 CatalogDataType::Float64 => "FLOAT8".to_string(),
239 CatalogDataType::Utf8 => "VARCHAR".to_string(),
240 CatalogDataType::Binary => "BYTEA".to_string(),
241 CatalogDataType::Uuid => "UUID".to_string(),
242 }
243 }
244}
245
246#[derive(Debug)]
247pub struct PostgresTransaction {
248 conn: Option<bb8::PooledConnection<'static, PostgresConnectionManager<NoTls>>>,
249 done: bool,
250}
251
252impl PostgresTransaction {
253 fn check_done(&self) -> ILResult<()> {
254 if self.done {
255 return Err(ILError::catalog(
256 "Transaction already committed or rolled back",
257 ));
258 }
259 Ok(())
260 }
261
262 fn conn_mut(
263 &mut self,
264 ) -> ILResult<&mut bb8::PooledConnection<'static, PostgresConnectionManager<NoTls>>> {
265 self.conn
266 .as_mut()
267 .ok_or_else(|| ILError::catalog("connection already taken"))
268 }
269}
270
271#[async_trait::async_trait]
272impl Transaction for PostgresTransaction {
273 async fn query(&mut self, sql: &str, schema: CatalogSchemaRef) -> ILResult<RowStream> {
274 trace!("postgres txn query: {sql}");
275 self.check_done()?;
276
277 let pg_row_stream = self
278 .conn_mut()?
279 .query_raw(sql, Vec::<String>::new())
280 .await
281 .map_err(|e| {
282 ILError::catalog(format!("failed to query postgres: {} {e}", SqlDisplay(sql)))
283 })?;
284
285 let stream = pg_row_stream.map(move |row| {
286 let pg_row =
287 row.map_err(|e| ILError::catalog(format!("failed to get postgres row: {e}")))?;
288 pg_row_to_row(&pg_row, &schema)
289 });
290 Ok(Box::pin(stream))
291 }
292
293 async fn execute(&mut self, sql: &str) -> ILResult<usize> {
294 trace!("postgres txn execute: {sql}");
295 self.check_done()?;
296 self.conn_mut()?
297 .execute(sql, &[])
298 .await
299 .map(|r| r as usize)
300 .map_err(|e| {
301 ILError::catalog(format!(
302 "failed to execute postgres: {} {e}",
303 SqlDisplay(sql)
304 ))
305 })
306 }
307
308 async fn execute_batch(&mut self, sqls: &[String]) -> ILResult<()> {
309 trace!("postgres txn execute batch: {:?}", sqls);
310 self.check_done()?;
311 let sql = sqls.join(";");
312 self.conn_mut()?.batch_execute(&sql).await.map_err(|e| {
313 ILError::catalog(format!(
314 "failed to execute batch postgres: {} {e}",
315 SqlDisplay(&sql)
316 ))
317 })
318 }
319
320 async fn insert_rows(
321 &mut self,
322 table_name: &str,
323 field_names: &[String],
324 batches: &[RecordBatch],
325 ) -> ILResult<()> {
326 trace!("postgres txn insert rows: {table_name}");
327 self.check_done()?;
328
329 let copy_sql = format!(
330 "COPY {} ({}) FROM STDIN BINARY",
331 table_name,
332 field_names.join(", ")
333 );
334 trace!("postgres txn binary copy_in: {copy_sql}");
335
336 let sink = self
337 .conn_mut()?
338 .copy_in(©_sql)
339 .await
340 .map_err(|e| ILError::catalog(format!("failed to start COPY: {e}")))?;
341 let pg_types = batches[0]
342 .schema()
343 .fields()
344 .iter()
345 .map(|f| CatalogDataType::from_arrow(f.data_type()).map(catalog_data_type_to_pg_type))
346 .collect::<ILResult<Vec<_>>>()?;
347 let mut writer = std::pin::pin!(BinaryCopyInWriter::new(sink, &pg_types));
348
349 for batch in batches {
350 for row_idx in 0..batch.num_rows() {
351 let values = batch
352 .columns()
353 .iter()
354 .map(|col| {
355 let scalar = Scalar::try_from_array(col.as_ref(), row_idx)?;
356 scalar_to_pg_value(scalar)
357 })
358 .collect::<ILResult<Vec<_>>>()?;
359 let refs: Vec<&(dyn ToSql + Sync)> =
360 values.iter().map(|v| v as &(dyn ToSql + Sync)).collect();
361 writer
362 .as_mut()
363 .write(&refs)
364 .await
365 .map_err(|e| ILError::catalog(format!("failed to write COPY row: {e}")))?;
366 }
367 }
368
369 let rows = writer
370 .finish()
371 .await
372 .map_err(|e| ILError::catalog(format!("failed to finish COPY: {e}")))?;
373 trace!("postgres COPY inserted {} rows", rows);
374
375 Ok(())
376 }
377
378 async fn commit(&mut self) -> ILResult<()> {
379 trace!("postgres txn commit");
380 self.check_done()?;
381 self.conn_mut()?
382 .batch_execute("COMMIT")
383 .await
384 .map_err(|e| ILError::catalog(format!("failed to commit postgres txn: {e}")))?;
385 self.done = true;
386 Ok(())
387 }
388
389 async fn rollback(&mut self) -> ILResult<()> {
390 trace!("postgres txn rollback");
391 self.check_done()?;
392 self.conn_mut()?
393 .batch_execute("ROLLBACK")
394 .await
395 .map_err(|e| ILError::catalog(format!("failed to rollback postgres txn: {e}")))?;
396 self.done = true;
397 Ok(())
398 }
399}
400
401impl Drop for PostgresTransaction {
402 fn drop(&mut self) {
403 if self.done {
404 return;
405 }
406 if let Some(conn) = self.conn.take() {
407 tokio::spawn(async move {
408 if let Err(e) = conn.batch_execute("ROLLBACK").await {
409 error!("[indexlake] failed to rollback postgres txn: {e}");
410 }
411 });
412 }
413 }
414}
415
416fn pg_row_to_row(
417 pg_row: &bb8_postgres::tokio_postgres::Row,
418 schema: &CatalogSchemaRef,
419) -> ILResult<Row> {
420 let mut values = Vec::new();
421 let err_mapping = |e: bb8_postgres::tokio_postgres::error::Error| {
422 ILError::catalog(format!("failed to get row value: {e}"))
423 };
424 for (idx, field) in schema.columns.iter().enumerate() {
425 let scalar = match field.data_type {
426 CatalogDataType::Boolean => {
427 let v: Option<bool> = pg_row.try_get(idx).map_err(err_mapping)?;
428 Scalar::Boolean(v)
429 }
430 CatalogDataType::Int8 => {
431 let v: Option<i16> = pg_row.try_get(idx).map_err(err_mapping)?;
432 Scalar::Int8(v.map(|v| v as i8))
433 }
434 CatalogDataType::Int16 => {
435 let v: Option<i16> = pg_row.try_get(idx).map_err(err_mapping)?;
436 Scalar::Int16(v)
437 }
438 CatalogDataType::Int32 => {
439 let v: Option<i32> = pg_row.try_get(idx).map_err(err_mapping)?;
440 Scalar::Int32(v)
441 }
442 CatalogDataType::Int64 => {
443 let v: Option<i64> = pg_row.try_get(idx).map_err(err_mapping)?;
444 Scalar::Int64(v)
445 }
446 CatalogDataType::UInt8 => {
447 let v: Option<i16> = pg_row.try_get(idx).map_err(err_mapping)?;
448 Scalar::UInt8(v.map(|v| v as u8))
449 }
450 CatalogDataType::UInt16 => {
451 let v: Option<i32> = pg_row.try_get(idx).map_err(err_mapping)?;
452 Scalar::UInt16(v.map(|v| v as u16))
453 }
454 CatalogDataType::UInt32 => {
455 let v: Option<i64> = pg_row.try_get(idx).map_err(err_mapping)?;
456 Scalar::UInt32(v.map(|v| v as u32))
457 }
458 CatalogDataType::UInt64 => {
459 let v: Option<f32> = pg_row.try_get(idx).map_err(err_mapping)?;
460 Scalar::UInt64(v.map(|v| v as u64))
461 }
462 CatalogDataType::Float32 => {
463 let v: Option<f32> = pg_row.try_get(idx).map_err(err_mapping)?;
464 Scalar::Float32(v)
465 }
466 CatalogDataType::Float64 => {
467 let v: Option<f64> = pg_row.try_get(idx).map_err(err_mapping)?;
468 Scalar::Float64(v)
469 }
470 CatalogDataType::Utf8 => {
471 let v: Option<String> = pg_row.try_get(idx).map_err(err_mapping)?;
472 Scalar::Utf8(v)
473 }
474 CatalogDataType::Binary => {
475 let v: Option<Vec<u8>> = pg_row.try_get(idx).map_err(err_mapping)?;
476 Scalar::Binary(v)
477 }
478 CatalogDataType::Uuid => {
479 let v: Option<uuid::Uuid> = pg_row.try_get(idx).map_err(err_mapping)?;
480 Scalar::Binary(v.map(|v| v.as_bytes().to_vec()))
481 }
482 };
483 if !field.nullable && scalar.is_null() {
484 return Err(ILError::catalog(format!(
485 "column {} is not nullable but got null value",
486 field.name
487 )));
488 }
489 values.push(scalar);
490 }
491 Ok(Row::new(schema.clone(), values))
492}