bestool_postgres/
text_cast.rs

1use std::error::Error;
2
3use tokio_postgres::types::{FromSql, Type};
4use tracing::debug;
5
6use crate::pool::PgPool;
7
8/// Raw bytes wrapper that can extract any PostgreSQL value
9#[derive(Debug, Clone, Hash, Eq, PartialEq)]
10struct RawValue {
11	pgtype: Type,
12	bytes: Vec<u8>,
13	null: bool,
14}
15
16impl<'a> FromSql<'a> for RawValue {
17	fn from_sql(ty: &Type, val: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
18		Ok(RawValue {
19			pgtype: ty.clone(),
20			bytes: val.to_vec(),
21			null: false,
22		})
23	}
24
25	fn from_sql_null(ty: &Type) -> Result<Self, Box<dyn Error + Sync + Send>> {
26		Ok(RawValue {
27			pgtype: ty.clone(),
28			bytes: Vec::new(),
29			null: true,
30		})
31	}
32
33	fn accepts(_ty: &Type) -> bool {
34		// Accept any type
35		true
36	}
37}
38
39impl tokio_postgres::types::ToSql for RawValue {
40	fn to_sql(
41		&self,
42		_ty: &Type,
43		out: &mut tokio_postgres::types::private::BytesMut,
44	) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
45		out.extend_from_slice(&self.bytes);
46		Ok(tokio_postgres::types::IsNull::No)
47	}
48
49	fn accepts(_ty: &Type) -> bool {
50		true
51	}
52
53	fn to_sql_checked(
54		&self,
55		_ty: &Type,
56		out: &mut tokio_postgres::types::private::BytesMut,
57	) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
58		self.to_sql(_ty, out)
59	}
60}
61
62/// Represents a cell to be cast (row index, column index)
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub struct CellRef {
65	pub row_idx: usize,
66	pub col_idx: usize,
67}
68
69/// On-demand text caster that converts values to text without re-querying
70#[derive(Debug, Clone)]
71pub struct TextCaster {
72	pool: PgPool,
73}
74
75impl TextCaster {
76	/// Create a new text caster
77	pub fn new(pool: PgPool) -> Self {
78		Self { pool }
79	}
80
81	/// Cast multiple values in batches of up to 100 at a time
82	/// Returns a vector of results in the same order as the input cells
83	pub async fn cast_batch(
84		&self,
85		rows: &[tokio_postgres::Row],
86		cells: &[CellRef],
87	) -> Vec<Result<String, Box<dyn std::error::Error + Send + Sync>>> {
88		const BATCH_SIZE: usize = 100;
89
90		let mut results = Vec::with_capacity(cells.len());
91		let num_chunks = cells.len().div_ceil(BATCH_SIZE);
92
93		debug!(
94			"Batch casting {} cells in {} chunk(s) of up to {} cells each",
95			cells.len(),
96			num_chunks,
97			BATCH_SIZE
98		);
99
100		for (chunk_idx, chunk) in cells.chunks(BATCH_SIZE).enumerate() {
101			debug!(
102				"Processing chunk {}/{} with {} cells",
103				chunk_idx + 1,
104				num_chunks,
105				chunk.len()
106			);
107			let chunk_results = self.cast_chunk(rows, chunk).await;
108			results.extend(chunk_results);
109		}
110
111		results
112	}
113
114	async fn cast_chunk(
115		&self,
116		rows: &[tokio_postgres::Row],
117		cells: &[CellRef],
118	) -> Vec<Result<String, Box<dyn std::error::Error + Send + Sync>>> {
119		if cells.is_empty() {
120			return Vec::new();
121		}
122
123		// Extract raw values and track which ones need casting
124		let mut raw_values = Vec::with_capacity(cells.len());
125		let mut needs_cast = Vec::new(); // indices of values that need actual casting
126
127		for (idx, cell) in cells.iter().enumerate() {
128			let result: Result<RawValue, _> = rows[cell.row_idx].try_get(cell.col_idx);
129			match result {
130				Ok(raw) => {
131					if raw.null {
132						raw_values.push(Ok(raw));
133					} else {
134						needs_cast.push(idx);
135						raw_values.push(Ok(raw));
136					}
137				}
138				Err(e) => raw_values.push(Err(e)),
139			}
140		}
141
142		// If nothing needs casting, return early
143		if needs_cast.is_empty() {
144			return raw_values
145				.into_iter()
146				.map(|r| match r {
147					Ok(raw) => {
148						if raw.null {
149							Ok("NULL".to_string())
150						} else {
151							Ok("(error)".to_string())
152						}
153					}
154					Err(e) => Err(Box::new(e) as Box<dyn Error + Send + Sync>),
155				})
156				.collect();
157		}
158
159		// Build a single query that casts all values at once
160		let client = match self.pool.get().await {
161			Ok(c) => c,
162			Err(_e) => {
163				return (0..cells.len())
164					.map(|_| {
165						Err(
166							Box::new(std::io::Error::other("Failed to get database connection"))
167								as Box<dyn Error + Send + Sync>,
168						)
169					})
170					.collect();
171			}
172		};
173
174		// First, get all type names in one query
175		let oids: Vec<u32> = needs_cast
176			.iter()
177			.filter_map(|&idx| {
178				if let Ok(ref raw) = raw_values[idx] {
179					Some(raw.pgtype.oid())
180				} else {
181					None
182				}
183			})
184			.collect();
185
186		let mut type_names = Vec::with_capacity(oids.len());
187		for oid in oids {
188			match client
189				.query_one("SELECT typname FROM pg_type WHERE oid = $1", &[&oid])
190				.await
191			{
192				Ok(row) => type_names.push(row.get::<_, String>(0)),
193				Err(e) => {
194					// If we can't get type name, return all errors
195					return (0..cells.len())
196						.map(|_| {
197							Err(Box::new(std::io::Error::other(format!(
198								"Failed to get type name: {}",
199								e
200							))) as Box<dyn Error + Send + Sync>)
201						})
202						.collect();
203				}
204			}
205		}
206
207		// Build the combined SELECT query
208		let mut query = String::from("SELECT ");
209		let mut params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = Vec::new();
210		let mut cast_positions = Vec::new(); // Maps result column index to raw_values index
211
212		for (param_idx, &raw_idx) in needs_cast.iter().enumerate() {
213			if param_idx > 0 {
214				query.push_str(", ");
215			}
216
217			if let Ok(ref raw) = raw_values[raw_idx] {
218				let typename = &type_names[param_idx];
219				query.push_str(&format!("${}::{}::text", param_idx + 1, typename));
220				params.push(raw);
221				cast_positions.push(raw_idx);
222			}
223		}
224
225		debug!(
226			"Executing batch cast query with {} parameters: {}",
227			params.len(),
228			query
229		);
230
231		// Execute the combined query
232		let cast_results = match client.query_one(&query, &params).await {
233			Ok(row) => {
234				let mut results = Vec::new();
235				for col_idx in 0..row.len() {
236					match row.try_get::<_, String>(col_idx) {
237						Ok(text) => results.push(Ok(text)),
238						Err(e) => results.push(Err(Box::new(e) as Box<dyn Error + Send + Sync>)),
239					}
240				}
241				results
242			}
243			Err(e) => {
244				// If the batch query fails, return error for all cells that needed casting
245				let error_msg = format!("Batch cast query failed: {}", e);
246				(0..needs_cast.len())
247					.map(|_| {
248						Err(Box::new(std::io::Error::other(error_msg.clone()))
249							as Box<dyn Error + Send + Sync>)
250					})
251					.collect()
252			}
253		};
254
255		// Now build the final results vector in the original order
256		let mut results = Vec::with_capacity(cells.len());
257		let mut cast_iter = cast_results.into_iter();
258
259		for (idx, raw_result) in raw_values.into_iter().enumerate() {
260			if needs_cast.contains(&idx) {
261				// This cell needed casting, get the next result
262				results.push(cast_iter.next().unwrap_or_else(|| {
263					Err(Box::new(std::io::Error::other("Missing cast result"))
264						as Box<dyn Error + Send + Sync>)
265				}));
266			} else {
267				// This cell was NULL or had an error
268				match raw_result {
269					Ok(raw) => {
270						if raw.null {
271							results.push(Ok("NULL".to_string()));
272						} else {
273							results.push(Ok("(unexpected)".to_string()));
274						}
275					}
276					Err(e) => results.push(Err(Box::new(e) as Box<dyn Error + Send + Sync>)),
277				}
278			}
279		}
280
281		results
282	}
283}
284
285#[cfg(test)]
286mod tests {
287	use super::*;
288
289	#[tokio::test]
290	async fn basic_int() {
291		let connection_string = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
292		let pool = crate::pool::create_pool(&connection_string, "test")
293			.await
294			.expect("Failed to create pool");
295
296		let caster = TextCaster::new(pool.clone());
297
298		let client = pool.get().await.unwrap();
299		let rows = client
300			.query("SELECT 123::int4 as record_col", &[])
301			.await
302			.unwrap();
303
304		let cell = CellRef {
305			row_idx: 0,
306			col_idx: 0,
307		};
308		let results = caster.cast_batch(&rows, &[cell]).await;
309		assert_eq!(results.len(), 1);
310		let result = &results[0];
311		if let Err(e) = result {
312			eprintln!("Error casting to text: {:?}", e);
313		}
314		assert!(
315			result.is_ok(),
316			"Failed to cast to text: {:?}",
317			result.as_ref().err()
318		);
319		let text = result.as_ref().unwrap();
320		eprintln!("Got text: {}", text);
321		assert_eq!(text, "123", "Text doesn't match expected value: {}", text);
322	}
323
324	#[tokio::test]
325	async fn batch_multiple_values() {
326		let connection_string = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
327		let pool = crate::pool::create_pool(&connection_string, "test")
328			.await
329			.expect("Failed to create pool");
330
331		let caster = TextCaster::new(pool.clone());
332
333		let client = pool.get().await.unwrap();
334		let rows = client
335			.query(
336				"SELECT '$100.50'::money, '$200.75'::money UNION ALL SELECT '$300.25'::money, '$400.00'::money",
337				&[],
338			)
339			.await
340			.unwrap();
341
342		let cells = vec![
343			CellRef {
344				row_idx: 0,
345				col_idx: 0,
346			},
347			CellRef {
348				row_idx: 0,
349				col_idx: 1,
350			},
351			CellRef {
352				row_idx: 1,
353				col_idx: 0,
354			},
355			CellRef {
356				row_idx: 1,
357				col_idx: 1,
358			},
359		];
360
361		let results = caster.cast_batch(&rows, &cells).await;
362
363		assert_eq!(results.len(), 4);
364		for result in &results {
365			assert!(result.is_ok());
366		}
367
368		let values: Vec<String> = results.into_iter().map(|r| r.unwrap()).collect();
369		assert!(values[0].contains("100"));
370		assert!(values[1].contains("200"));
371		assert!(values[2].contains("300"));
372		assert!(values[3].contains("400"));
373	}
374
375	#[tokio::test]
376	async fn batch_large_number() {
377		let connection_string = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
378		let pool = crate::pool::create_pool(&connection_string, "test")
379			.await
380			.expect("Failed to create pool");
381
382		let caster = TextCaster::new(pool.clone());
383
384		let client = pool.get().await.unwrap();
385		// Generate 150 rows to test chunking (should be split into 100 + 50)
386		let rows = client
387			.query(
388				"SELECT generate_series(1, 150)::text::money as money_col",
389				&[],
390			)
391			.await
392			.unwrap();
393
394		let cells: Vec<CellRef> = (0..150)
395			.map(|i| CellRef {
396				row_idx: i,
397				col_idx: 0,
398			})
399			.collect();
400
401		let results = caster.cast_batch(&rows, &cells).await;
402
403		assert_eq!(results.len(), 150);
404		for (i, result) in results.iter().enumerate() {
405			assert!(
406				result.is_ok(),
407				"Failed to cast row {}: {:?}",
408				i,
409				result.as_ref().err()
410			);
411		}
412	}
413
414	#[tokio::test]
415	async fn batch_query_verification() {
416		let connection_string = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
417		let pool = crate::pool::create_pool(&connection_string, "test")
418			.await
419			.expect("Failed to create pool");
420
421		let caster = TextCaster::new(pool.clone());
422
423		let client = pool.get().await.unwrap();
424		// Create a query with multiple different types
425		let rows = client
426			.query(
427				"SELECT
428					'$100.50'::money as col1,
429					'$200.75'::money as col2,
430					'$300.25'::money as col3
431				UNION ALL SELECT
432					'$400.00'::money,
433					'$500.00'::money,
434					'$600.00'::money",
435				&[],
436			)
437			.await
438			.unwrap();
439
440		// Collect all 6 cells (2 rows × 3 columns)
441		let cells: Vec<CellRef> = vec![
442			CellRef {
443				row_idx: 0,
444				col_idx: 0,
445			},
446			CellRef {
447				row_idx: 0,
448				col_idx: 1,
449			},
450			CellRef {
451				row_idx: 0,
452				col_idx: 2,
453			},
454			CellRef {
455				row_idx: 1,
456				col_idx: 0,
457			},
458			CellRef {
459				row_idx: 1,
460				col_idx: 1,
461			},
462			CellRef {
463				row_idx: 1,
464				col_idx: 2,
465			},
466		];
467
468		let results = caster.cast_batch(&rows, &cells).await;
469
470		// All 6 values should be cast successfully
471		assert_eq!(results.len(), 6);
472		for result in &results {
473			assert!(result.is_ok(), "Cast failed: {:?}", result);
474		}
475
476		// Verify the values contain the expected amounts
477		let values: Vec<String> = results.into_iter().map(|r| r.unwrap()).collect();
478		assert!(values[0].contains("100"));
479		assert!(values[1].contains("200"));
480		assert!(values[2].contains("300"));
481		assert!(values[3].contains("400"));
482		assert!(values[4].contains("500"));
483		assert!(values[5].contains("600"));
484
485		// The key point: this should have executed only 1 SELECT with 6 parameters,
486		// not 6 separate SELECT queries. We verify this by the debug log output.
487	}
488
489	#[tokio::test]
490	async fn batch_mixed_types() {
491		let connection_string = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
492		let pool = crate::pool::create_pool(&connection_string, "test")
493			.await
494			.expect("Failed to create pool");
495
496		let caster = TextCaster::new(pool.clone());
497
498		let client = pool.get().await.unwrap();
499		// Create a query with multiple different types in the same row
500		let rows = client
501			.query(
502				"SELECT
503					'$99.99'::money as money_col,
504					'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid as uuid_col,
505					point(3.14, 2.71) as point_col,
506					'192.168.1.100'::inet as inet_col",
507				&[],
508			)
509			.await
510			.unwrap();
511
512		// Collect all 4 cells (different types)
513		let cells: Vec<CellRef> = vec![
514			CellRef {
515				row_idx: 0,
516				col_idx: 0,
517			}, // money
518			CellRef {
519				row_idx: 0,
520				col_idx: 1,
521			}, // uuid
522			CellRef {
523				row_idx: 0,
524				col_idx: 2,
525			}, // point
526			CellRef {
527				row_idx: 0,
528				col_idx: 3,
529			}, // inet
530		];
531
532		let results = caster.cast_batch(&rows, &cells).await;
533
534		// All 4 values should be cast successfully
535		assert_eq!(results.len(), 4);
536		for (idx, result) in results.iter().enumerate() {
537			assert!(result.is_ok(), "Cast {} failed: {:?}", idx, result);
538		}
539
540		// Verify the values
541		let values: Vec<String> = results.into_iter().map(|r| r.unwrap()).collect();
542		assert!(values[0].contains("99")); // money
543		assert_eq!(values[1], "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); // uuid
544		assert!(values[2].contains("3.14") && values[2].contains("2.71")); // point
545		assert!(values[3].contains("192.168.1.100")); // inet
546
547		// The key point: this executes as a single query:
548		// SELECT $1::money::text, $2::uuid::text, $3::point::text, $4::inet::text
549	}
550
551	#[tokio::test]
552	#[ignore = "FIXME: figure out a workaround for (anonymous?) composite types"]
553	async fn composite() {
554		let connection_string = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
555		let pool = crate::pool::create_pool(&connection_string, "test")
556			.await
557			.expect("Failed to create pool");
558
559		let caster = TextCaster::new(pool.clone());
560
561		let client = pool.get().await.unwrap();
562		let rows = client
563			.query("SELECT row(1, 'test', true) as record_col", &[])
564			.await
565			.unwrap();
566
567		let cell = CellRef {
568			row_idx: 0,
569			col_idx: 0,
570		};
571		let results = caster.cast_batch(&rows, &[cell]).await;
572		assert_eq!(results.len(), 1);
573		let result = &results[0];
574		if let Err(e) = result {
575			eprintln!("Error casting to text: {:?}", e);
576		}
577		assert!(
578			result.is_ok(),
579			"Failed to cast to text: {:?}",
580			result.as_ref().err()
581		);
582		let text = result.as_ref().unwrap();
583		eprintln!("Got text: {}", text);
584		assert!(
585			text.contains("1") && text.contains("test"),
586			"Text doesn't contain expected values: {}",
587			text
588		);
589	}
590
591	#[tokio::test]
592	async fn money_type() {
593		let connection_string = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
594		let pool = crate::pool::create_pool(&connection_string, "test")
595			.await
596			.expect("Failed to create pool");
597
598		let caster = TextCaster::new(pool.clone());
599
600		let client = pool.get().await.unwrap();
601		let rows = client
602			.query("SELECT '$1,234.56'::money as money_col", &[])
603			.await
604			.unwrap();
605
606		let cell = CellRef {
607			row_idx: 0,
608			col_idx: 0,
609		};
610		let results = caster.cast_batch(&rows, &[cell]).await;
611		assert_eq!(results.len(), 1);
612		assert!(
613			results[0].is_ok(),
614			"Failed to cast money: {:?}",
615			results[0].as_ref().err()
616		);
617		let text = results[0].as_ref().unwrap();
618		eprintln!("Money as text: {}", text);
619		assert!(text.contains("1") && text.contains("234"));
620	}
621
622	#[tokio::test]
623	async fn uuid_type() {
624		let connection_string = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
625		let pool = crate::pool::create_pool(&connection_string, "test")
626			.await
627			.expect("Failed to create pool");
628
629		let caster = TextCaster::new(pool.clone());
630
631		let client = pool.get().await.unwrap();
632		let rows = client
633			.query(
634				"SELECT 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid as uuid_col",
635				&[],
636			)
637			.await
638			.unwrap();
639
640		let cell = CellRef {
641			row_idx: 0,
642			col_idx: 0,
643		};
644		let results = caster.cast_batch(&rows, &[cell]).await;
645		assert_eq!(results.len(), 1);
646		assert!(
647			results[0].is_ok(),
648			"Failed to cast uuid: {:?}",
649			results[0].as_ref().err()
650		);
651		let text = results[0].as_ref().unwrap();
652		eprintln!("UUID as text: {}", text);
653		assert_eq!(text, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11");
654	}
655
656	#[tokio::test]
657	async fn json_type() {
658		let connection_string = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
659		let pool = crate::pool::create_pool(&connection_string, "test")
660			.await
661			.expect("Failed to create pool");
662
663		let caster = TextCaster::new(pool.clone());
664
665		let client = pool.get().await.unwrap();
666		let rows = client
667			.query("SELECT '{\"key\": \"value\"}'::json as json_col", &[])
668			.await
669			.unwrap();
670
671		let cell = CellRef {
672			row_idx: 0,
673			col_idx: 0,
674		};
675		let results = caster.cast_batch(&rows, &[cell]).await;
676		assert_eq!(results.len(), 1);
677		assert!(
678			results[0].is_ok(),
679			"Failed to cast json: {:?}",
680			results[0].as_ref().err()
681		);
682		let text = results[0].as_ref().unwrap();
683		eprintln!("JSON as text: {}", text);
684		assert!(text.contains("key") && text.contains("value"));
685	}
686
687	#[tokio::test]
688	async fn jsonb_type() {
689		let connection_string = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
690		let pool = crate::pool::create_pool(&connection_string, "test")
691			.await
692			.expect("Failed to create pool");
693
694		let caster = TextCaster::new(pool.clone());
695
696		let client = pool.get().await.unwrap();
697		let rows = client
698			.query("SELECT '{\"foo\": \"bar\"}'::jsonb as jsonb_col", &[])
699			.await
700			.unwrap();
701
702		let cell = CellRef {
703			row_idx: 0,
704			col_idx: 0,
705		};
706		let results = caster.cast_batch(&rows, &[cell]).await;
707		assert_eq!(results.len(), 1);
708		assert!(
709			results[0].is_ok(),
710			"Failed to cast jsonb: {:?}",
711			results[0].as_ref().err()
712		);
713		let text = results[0].as_ref().unwrap();
714		eprintln!("JSONB as text: {}", text);
715		assert!(text.contains("foo") && text.contains("bar"));
716	}
717
718	#[tokio::test]
719	async fn array_type() {
720		let connection_string = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
721		let pool = crate::pool::create_pool(&connection_string, "test")
722			.await
723			.expect("Failed to create pool");
724
725		let caster = TextCaster::new(pool.clone());
726
727		let client = pool.get().await.unwrap();
728		let rows = client
729			.query("SELECT ARRAY[1, 2, 3, 4]::int[] as array_col", &[])
730			.await
731			.unwrap();
732
733		let cell = CellRef {
734			row_idx: 0,
735			col_idx: 0,
736		};
737		let results = caster.cast_batch(&rows, &[cell]).await;
738		assert_eq!(results.len(), 1);
739		assert!(
740			results[0].is_ok(),
741			"Failed to cast array: {:?}",
742			results[0].as_ref().err()
743		);
744		let text = results[0].as_ref().unwrap();
745		eprintln!("Array as text: {}", text);
746		assert!(text.contains("1") && text.contains("2") && text.contains("3"));
747	}
748
749	#[tokio::test]
750	async fn bytea_type() {
751		let connection_string = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
752		let pool = crate::pool::create_pool(&connection_string, "test")
753			.await
754			.expect("Failed to create pool");
755
756		let caster = TextCaster::new(pool.clone());
757
758		let client = pool.get().await.unwrap();
759		let rows = client
760			.query("SELECT '\\xDEADBEEF'::bytea as bytea_col", &[])
761			.await
762			.unwrap();
763
764		let cell = CellRef {
765			row_idx: 0,
766			col_idx: 0,
767		};
768		let results = caster.cast_batch(&rows, &[cell]).await;
769		assert_eq!(results.len(), 1);
770		assert!(
771			results[0].is_ok(),
772			"Failed to cast bytea: {:?}",
773			results[0].as_ref().err()
774		);
775		let text = results[0].as_ref().unwrap();
776		eprintln!("Bytea as text: {}", text);
777		assert!(text.contains("\\x") || text.contains("de") || text.contains("ad"));
778	}
779
780	#[tokio::test]
781	async fn inet_type() {
782		let connection_string = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
783		let pool = crate::pool::create_pool(&connection_string, "test")
784			.await
785			.expect("Failed to create pool");
786
787		let caster = TextCaster::new(pool.clone());
788
789		let client = pool.get().await.unwrap();
790		let rows = client
791			.query("SELECT '192.168.1.1/24'::inet as inet_col", &[])
792			.await
793			.unwrap();
794
795		let cell = CellRef {
796			row_idx: 0,
797			col_idx: 0,
798		};
799		let results = caster.cast_batch(&rows, &[cell]).await;
800		assert_eq!(results.len(), 1);
801		assert!(
802			results[0].is_ok(),
803			"Failed to cast inet: {:?}",
804			results[0].as_ref().err()
805		);
806		let text = results[0].as_ref().unwrap();
807		eprintln!("Inet as text: {}", text);
808		assert!(text.contains("192.168.1.1"));
809	}
810
811	#[tokio::test]
812	async fn interval_type() {
813		let connection_string = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
814		let pool = crate::pool::create_pool(&connection_string, "test")
815			.await
816			.expect("Failed to create pool");
817
818		let caster = TextCaster::new(pool.clone());
819
820		let client = pool.get().await.unwrap();
821		let rows = client
822			.query("SELECT '2 days 3 hours'::interval as interval_col", &[])
823			.await
824			.unwrap();
825
826		let cell = CellRef {
827			row_idx: 0,
828			col_idx: 0,
829		};
830		let results = caster.cast_batch(&rows, &[cell]).await;
831		assert_eq!(results.len(), 1);
832		assert!(
833			results[0].is_ok(),
834			"Failed to cast interval: {:?}",
835			results[0].as_ref().err()
836		);
837		let text = results[0].as_ref().unwrap();
838		eprintln!("Interval as text: {}", text);
839		assert!(text.contains("day") || text.contains("hour") || text.contains("2"));
840	}
841
842	#[tokio::test]
843	async fn null_value() {
844		let connection_string = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
845		let pool = crate::pool::create_pool(&connection_string, "test")
846			.await
847			.expect("Failed to create pool");
848
849		let caster = TextCaster::new(pool.clone());
850
851		let client = pool.get().await.unwrap();
852		let rows = client
853			.query("SELECT NULL::int as null_col", &[])
854			.await
855			.unwrap();
856
857		let cell = CellRef {
858			row_idx: 0,
859			col_idx: 0,
860		};
861		let results = caster.cast_batch(&rows, &[cell]).await;
862		assert_eq!(results.len(), 1);
863		assert!(
864			results[0].is_ok(),
865			"Failed to cast null: {:?}",
866			results[0].as_ref().err()
867		);
868		let text = results[0].as_ref().unwrap();
869		eprintln!("NULL as text: {}", text);
870		assert_eq!(text, "NULL");
871	}
872
873	#[tokio::test]
874	async fn point_type() {
875		let connection_string = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
876		let pool = crate::pool::create_pool(&connection_string, "test")
877			.await
878			.expect("Failed to create pool");
879
880		let caster = TextCaster::new(pool.clone());
881
882		let client = pool.get().await.unwrap();
883		let rows = client
884			.query("SELECT point(1.5, 2.5) as point_col", &[])
885			.await
886			.unwrap();
887
888		let cell = CellRef {
889			row_idx: 0,
890			col_idx: 0,
891		};
892		let results = caster.cast_batch(&rows, &[cell]).await;
893		assert_eq!(results.len(), 1);
894		assert!(
895			results[0].is_ok(),
896			"Failed to cast point: {:?}",
897			results[0].as_ref().err()
898		);
899		let text = results[0].as_ref().unwrap();
900		eprintln!("Point as text: {}", text);
901		assert!(text.contains("1.5") && text.contains("2.5"));
902	}
903
904	#[tokio::test]
905	async fn box_type() {
906		let connection_string = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
907		let pool = crate::pool::create_pool(&connection_string, "test")
908			.await
909			.expect("Failed to create pool");
910
911		let caster = TextCaster::new(pool.clone());
912
913		let client = pool.get().await.unwrap();
914		let rows = client
915			.query("SELECT box(point(0,0), point(1,1)) as box_col", &[])
916			.await
917			.unwrap();
918
919		let cell = CellRef {
920			row_idx: 0,
921			col_idx: 0,
922		};
923		let results = caster.cast_batch(&rows, &[cell]).await;
924		assert_eq!(results.len(), 1);
925		assert!(
926			results[0].is_ok(),
927			"Failed to cast box: {:?}",
928			results[0].as_ref().err()
929		);
930		let text = results[0].as_ref().unwrap();
931		eprintln!("Box as text: {}", text);
932		assert!(text.contains("0") && text.contains("1"));
933	}
934}