generated from alecodes/base-template
feat: improve query return types
This commit is contained in:
parent
5d1b4bdfd8
commit
a4fe3808de
3 changed files with 195 additions and 133 deletions
|
|
@ -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(());
|
|
||||||
}
|
|
||||||
|
|
|
||||||
121
src/sql.rs
121
src/sql.rs
|
|
@ -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,
|
let rows = connector.query(query).await?;
|
||||||
value: Option<T>,
|
|
||||||
|
for (idx, row) in rows.iter().enumerate() {
|
||||||
|
println!("Row {}: {:?}", idx, row);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
return Ok(());
|
||||||
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 {
|
|
||||||
pub fn new(row: &PgRow, idx: usize) -> error::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(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))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
183
src/sql/postgres.rs
Normal file
183
src/sql/postgres.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue