use bigdecimal::BigDecimal; use chrono::{DateTime, Utc}; use derive_builder::Builder; use interim_pgtypes::pg_attribute::PgAttribute; use serde::{Deserialize, Serialize}; use sqlx::Acquire as _; use sqlx::{ Decode, Postgres, Row as _, TypeInfo as _, ValueRef as _, postgres::PgRow, query, query_as, types::Json, }; use thiserror::Error; use uuid::Uuid; use crate::client::AppDbClient; use crate::datum::Datum; use crate::presentation::Presentation; /// A materialization of a database column, fit for consumption by an end user. /// /// There may be zero or more fields per column/attribute in a Postgres view. /// There may in some case also be fields with no underlying column, if it has /// been removed or altered. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Field { /// Internal ID for application use. pub id: Uuid, /// Name of the database column. pub name: String, /// Refer to documentation for `Presentation`. pub presentation: sqlx::types::Json, /// Optional human friendly label. pub table_label: Option, /// Width of UI table column in pixels. pub table_width_px: i32, /// Position of the field relative to others. Smaller values appear earlier. pub ordinality: f64, } impl Field { /// Constructs a brand new field to be inserted into the application db. pub fn insert() -> InsertableFieldBuilder { InsertableFieldBuilder::default() } /// Construct an update to an existing field. pub fn update() -> UpdateBuilder { UpdateBuilder::default() } /// Generate a default field config based on an existing column's name and /// type. pub fn default_from_attr(attr: &PgAttribute) -> Option { Presentation::default_from_attr(attr).map(|presentation| Self { id: Uuid::now_v7(), name: attr.attname.clone(), table_label: None, presentation: sqlx::types::Json(presentation), table_width_px: 200, ordinality: 1.0, }) } pub fn get_datum(&self, row: &PgRow) -> Result { let value_ref = row .try_get_raw(self.name.as_str()) .or(Err(ParseError::FieldNotFound))?; let type_info = value_ref.type_info(); let ty = type_info.name(); dbg!(&ty); Ok(match ty { "NUMERIC" => { Datum::Numeric( as Decode>::decode(value_ref).unwrap()) } "TEXT" | "VARCHAR" => { Datum::Text( as Decode>::decode(value_ref).unwrap()) } "UUID" => Datum::Uuid( as Decode>::decode(value_ref).unwrap()), "TIMESTAMPTZ" => Datum::Timestamp( > as Decode>::decode(value_ref).unwrap(), ), _ => return Err(ParseError::UnknownType), }) } pub fn belonging_to_portal(portal_id: Uuid) -> BelongingToPortalQuery { BelongingToPortalQuery { portal_id } } } #[derive(Clone, Debug, PartialEq)] pub struct BelongingToPortalQuery { portal_id: Uuid, } impl BelongingToPortalQuery { pub fn with_id(self, id: Uuid) -> WithIdQuery { WithIdQuery { id, portal_id: self.portal_id, } } pub async fn fetch_all(self, app_db: &mut AppDbClient) -> Result, sqlx::Error> { query_as!( Field, r#" select id, name, table_label, presentation as "presentation: Json", table_width_px, ordinality from fields where portal_id = $1 order by ordinality "#, self.portal_id ) .fetch_all(&mut *app_db.conn) .await } } #[derive(Clone, Debug, PartialEq)] pub struct WithIdQuery { id: Uuid, portal_id: Uuid, } impl WithIdQuery { pub async fn fetch_one(self, app_db: &mut AppDbClient) -> Result { query_as!( Field, r#" select id, name, table_label, presentation as "presentation: Json", table_width_px, ordinality from fields where portal_id = $1 and id = $2 "#, self.portal_id, self.id, ) .fetch_one(&mut *app_db.conn) .await } } #[derive(Builder, Clone, Debug)] pub struct InsertableField { portal_id: Uuid, name: String, #[builder(default)] table_label: Option, presentation: Presentation, #[builder(default = 200)] table_width_px: i32, } impl InsertableField { pub async fn insert(self, app_db: &mut AppDbClient) -> Result { query_as!( Field, r#" insert into fields ( portal_id, name, table_label, presentation, table_width_px, ordinality ) ( select $1 as portal_id, $2 as name, $3 as table_label, $4 as presentation, $5 as table_width_px, coalesce(max(prev.ordinality), 0) + 1 as ordinality from fields as prev where prev.portal_id = $1 ) returning id, name, table_label, presentation as "presentation: Json", table_width_px, ordinality "#, self.portal_id, self.name, self.table_label, Json::<_>(self.presentation) as Json, self.table_width_px, ) .fetch_one(&mut *app_db.conn) .await } } impl InsertableFieldBuilder { pub fn default_from_attr(attr: &PgAttribute) -> Self { Self { name: Some(attr.attname.clone()), presentation: Presentation::default_from_attr(attr), ..Self::default() } } } #[derive(Builder, Clone, Debug)] pub struct Update { id: Uuid, #[builder(default, setter(strip_option))] table_label: Option>, #[builder(default, setter(strip_option))] presentation: Option, #[builder(default, setter(strip_option))] table_width_px: Option, #[builder(default, setter(strip_option))] ordinality: Option, } impl Update { pub async fn execute(self, app_db: &mut AppDbClient) -> Result<(), sqlx::Error> { // TODO: consolidate statements instead of using transaction let mut tx = app_db.get_conn().begin().await?; if let Some(table_label) = self.table_label { query!( "update fields set table_label = $1 where id = $2", table_label, self.id ) .execute(&mut *tx) .await?; } if let Some(presentation) = self.presentation { query!( "update fields set presentation = $1 where id = $2", Json::<_>(presentation) as Json, self.id ) .execute(&mut *tx) .await?; } if let Some(table_width_px) = self.table_width_px { query!( "update fields set table_width_px = $1 where id = $2", table_width_px, self.id ) .execute(&mut *tx) .await?; } if let Some(ordinality) = self.ordinality { query!( "update fields set ordinality = $1 where id = $2", ordinality, self.id ) .execute(&mut *tx) .await?; } tx.commit().await?; Ok(()) } } /// Error when parsing a sqlx value to JSON #[derive(Debug, Error)] pub enum ParseError { // TODO: can this be removed? #[error("incompatible json type")] BadJsonType, #[error("field not found in row")] FieldNotFound, #[error("unknown postgres type")] UnknownType, }