feat: improve query return types

This commit is contained in:
Alexander Navarro 2025-01-22 15:56:04 -03:00
parent 5d1b4bdfd8
commit a4fe3808de
3 changed files with 195 additions and 133 deletions

View file

@ -1,13 +1,9 @@
#![allow(unused, dead_code)] #![allow(dead_code, unused)]
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use futures::TryStreamExt;
use simple_crud::error::Error; use simple_crud::error::Error;
use simple_crud::error::Result; use simple_crud::error::Result;
use simple_crud::sql::ColumnType; use simple_crud::sql;
use sqlx::postgres::PgPool;
use sqlx::Column;
use sqlx::Row;
#[derive(Parser)] #[derive(Parser)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
@ -44,7 +40,7 @@ async fn main() -> Result<()> {
// You can check for the existence of subcommands, and if found use their // You can check for the existence of subcommands, and if found use their
// matches just as you would the top level cmd // matches just as you would the top level cmd
let result = match &cli.command { let result = match &cli.command {
Commands::Query { sql } => handle_query(url, sql).await, Commands::Query { sql } => sql::handle_query(url, sql).await,
}; };
if let Err(err) = result { if let Err(err) = result {
@ -53,19 +49,3 @@ async fn main() -> Result<()> {
Ok(()) Ok(())
} }
async fn handle_query(url: String, query: &String) -> Result<()> {
let pool = PgPool::connect(url.as_str()).await?;
let mut rows = sqlx::query(query.as_str()).fetch(&pool);
while let Some(row) = rows.try_next().await? {
for idx in 0..row.len() {
let column = ColumnType::new(&row, idx)?;
// let value = String::from(row.get(col.ordinal()));
println!("Column {:?}", column);
}
}
return Ok(());
}

View file

@ -1,116 +1,15 @@
use chrono::Utc; use crate::error::Result;
use sqlx::postgres::PgRow;
use sqlx::Column as _;
use sqlx::Row as _;
use sqlx::TypeInfo;
use crate::error; mod postgres;
use crate::error::Error;
#[derive(Debug)] pub async fn handle_query(url: String, query: &String) -> Result<()> {
pub struct Column<T> { let connector = postgres::PgConnector::new(url).await?;
name: String,
sql_type: String,
value: Option<T>,
}
#[derive(Debug)] let rows = connector.query(query).await?;
pub enum ColumnType {
I8(Column<i8>),
String(Column<String>),
Bool(Column<bool>),
I16(Column<i16>),
I32(Column<i32>),
I64(Column<i64>),
F32(Column<f32>),
F64(Column<f64>),
DateTime(Column<chrono::DateTime<Utc>>),
}
impl ColumnType { for (idx, row) in rows.iter().enumerate() {
pub fn new(row: &PgRow, idx: usize) -> error::Result<Self> { println!("Row {}: {:?}", idx, row);
let column = row
.columns()
.get(idx)
.ok_or_else(|| Error::ColumnParse(String::from("Could not get column")))?;
let sql_type = column.type_info().name();
let name = String::from(column.name());
match sql_type {
"BOOL" => {
let value: Option<bool> = row.try_get(idx)?;
Ok(ColumnType::Bool(Column {
sql_type: String::from(sql_type),
value,
name,
}))
}
"VARCHAR" | "char(n)" | "TEXT" | "NAME" => {
let value: Option<String> = row.try_get(idx)?;
Ok(ColumnType::String(Column {
sql_type: String::from(sql_type),
value,
name,
}))
}
"CHAR" => {
let value: Option<i8> = row.try_get(idx)?;
Ok(ColumnType::I8(Column {
sql_type: String::from(sql_type),
value,
name,
}))
}
"SMALLSERIAL" | "SMALLINT" => {
let value: Option<i16> = row.try_get(idx)?;
Ok(ColumnType::I16(Column {
sql_type: String::from(sql_type),
value,
name,
}))
}
"INT" | "INT4" | "SERIAL" => {
let value: Option<i32> = row.try_get(idx)?;
Ok(ColumnType::I32(Column {
sql_type: String::from(sql_type),
value,
name,
}))
}
"INT8" | "BIGSERIAL" | "BIGINT" => {
let value: Option<i64> = row.try_get(idx)?;
Ok(ColumnType::I64(Column {
sql_type: String::from(sql_type),
value,
name,
}))
}
"FLOAT4" | "REAL" => {
let value: Option<f32> = row.try_get(idx)?;
Ok(ColumnType::F32(Column {
sql_type: String::from(sql_type),
value,
name,
}))
}
"FLOAT8" | "double precision" => {
let value: Option<f64> = row.try_get(idx)?;
Ok(ColumnType::F64(Column {
sql_type: String::from(sql_type),
value,
name,
}))
}
"TIMESTAMP" | "TIMESTAMPTZ" => {
// with-chrono feature is needed for this
let value: Option<chrono::DateTime<Utc>> = row.try_get(idx)?;
Ok(ColumnType::DateTime(Column {
sql_type: String::from(sql_type),
value,
name,
}))
}
&_ => Err(Error::ColumnParse(format!("{} type not found!", sql_type))),
}
} }
return Ok(());
} }

183
src/sql/postgres.rs Normal file
View file

@ -0,0 +1,183 @@
use std::collections::HashMap;
use std::fmt;
use chrono::Utc;
use sqlx::postgres::PgRow;
use sqlx::Column;
use sqlx::PgPool;
use sqlx::Row;
use sqlx::TypeInfo;
use futures::TryStreamExt;
use crate::error::Error;
use crate::error::Result;
#[derive(Debug)]
pub struct CellData<T: fmt::Display> {
name: String,
sql_type: String,
value: Option<T>,
}
impl<T: fmt::Display + std::default::Default> fmt::Display for CellData<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = match &self.value {
Some(value) => value.to_string(),
None => String::from("NULL"),
};
write!(f, "{}", value)
}
}
#[derive(Debug)]
pub enum Cell {
I8(CellData<i8>),
String(CellData<String>),
Bool(CellData<bool>),
I16(CellData<i16>),
I32(CellData<i32>),
I64(CellData<i64>),
F32(CellData<f32>),
F64(CellData<f64>),
DateTime(CellData<chrono::DateTime<Utc>>),
}
impl Cell {
pub fn new(row: &PgRow, idx: usize) -> Result<Self> {
let column = row
.columns()
.get(idx)
.ok_or_else(|| Error::ColumnParse(String::from("Could not get column")))?;
let sql_type = column.type_info().name();
let name = String::from(column.name());
match sql_type {
"BOOL" => {
let value: Option<bool> = row.try_get(idx)?;
Ok(Cell::Bool(CellData {
sql_type: String::from(sql_type),
value,
name,
}))
}
"VARCHAR" | "char(n)" | "TEXT" | "NAME" => {
let value: Option<String> = row.try_get(idx)?;
Ok(Cell::String(CellData {
sql_type: String::from(sql_type),
value,
name,
}))
}
"CHAR" => {
let value: Option<i8> = row.try_get(idx)?;
Ok(Cell::I8(CellData {
sql_type: String::from(sql_type),
value,
name,
}))
}
"SMALLSERIAL" | "SMALLINT" => {
let value: Option<i16> = row.try_get(idx)?;
Ok(Cell::I16(CellData {
sql_type: String::from(sql_type),
value,
name,
}))
}
"INT" | "INT4" | "SERIAL" => {
let value: Option<i32> = row.try_get(idx)?;
Ok(Cell::I32(CellData {
sql_type: String::from(sql_type),
value,
name,
}))
}
"INT8" | "BIGSERIAL" | "BIGINT" => {
let value: Option<i64> = row.try_get(idx)?;
Ok(Cell::I64(CellData {
sql_type: String::from(sql_type),
value,
name,
}))
}
"FLOAT4" | "REAL" => {
let value: Option<f32> = row.try_get(idx)?;
Ok(Cell::F32(CellData {
sql_type: String::from(sql_type),
value,
name,
}))
}
"FLOAT8" | "double precision" => {
let value: Option<f64> = row.try_get(idx)?;
Ok(Cell::F64(CellData {
sql_type: String::from(sql_type),
value,
name,
}))
}
"TIMESTAMP" | "TIMESTAMPTZ" => {
// With-chrono feature is needed for this
let value: Option<chrono::DateTime<Utc>> = row.try_get(idx)?;
Ok(Cell::DateTime(CellData {
sql_type: String::from(sql_type),
value,
name,
}))
}
&_ => Err(Error::ColumnParse(format!("{} type not found!", sql_type))),
}
}
}
impl fmt::Display for Cell {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = match self {
Cell::I8(cell_data) => cell_data.to_string(),
Cell::String(cell_data) => cell_data.to_string(),
Cell::Bool(cell_data) => cell_data.to_string(),
Cell::I16(cell_data) => cell_data.to_string(),
Cell::I32(cell_data) => cell_data.to_string(),
Cell::I64(cell_data) => cell_data.to_string(),
Cell::F32(cell_data) => cell_data.to_string(),
Cell::F64(cell_data) => cell_data.to_string(),
Cell::DateTime(cell_data) => cell_data.to_string(),
};
write!(f, "{}", value)
}
}
type PgRows = Vec<HashMap<String, Cell>>;
pub(crate) struct PgConnector {
pool: PgPool,
}
impl PgConnector {
pub async fn new(url: String) -> Result<Self> {
let pool = PgPool::connect(url.as_str()).await?;
Ok(Self { pool })
}
pub async fn query(&self, query: &String) -> Result<PgRows> {
let mut query = sqlx::query(query.as_str()).fetch(&self.pool);
let mut rows = vec![];
while let Some(row) = query.try_next().await? {
let mut cells = HashMap::new();
for idx in 0..row.len() {
let col = row.column(idx);
let cell = Cell::new(&row, idx)?;
cells.insert(col.name().into(), cell);
}
rows.push(cells);
}
return Ok(rows);
}
}