From 89f7bc75b0c72bb7e2e58f71f4a64793daab6429 Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Thu, 16 Jan 2025 22:51:53 +0000 Subject: [PATCH] feat(backends): Add initial Postgres support --- Cargo.toml | 1 + geekorm-core/Cargo.toml | 3 + geekorm-core/src/backends/mod.rs | 2 + geekorm-core/src/backends/postgres.rs | 72 +++++++++++ geekorm-core/src/backends/postgres/de.rs | 149 +++++++++++++++++++++++ geekorm-core/src/error.rs | 12 ++ geekorm-derive/Cargo.toml | 1 + 7 files changed, 240 insertions(+) create mode 100644 geekorm-core/src/backends/postgres.rs create mode 100644 geekorm-core/src/backends/postgres/de.rs diff --git a/Cargo.toml b/Cargo.toml index 7d74df1..4c1a3be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,6 +83,7 @@ search = ["geekorm-derive/search", "geekorm-core/search"] libsql = ["backends", "geekorm-derive/libsql", "geekorm-core/libsql"] rusqlite = ["backends", "geekorm-derive/rusqlite", "geekorm-core/rusqlite"] # sqlite = ["backends", "geekorm-derive/sqlite", "geekorm-core/sqlite"] +postgres = ["backends", "geekorm-derive/postgres", "geekorm-core/postgres"] migrations = ["geekorm-core/migrations"] diff --git a/geekorm-core/Cargo.toml b/geekorm-core/Cargo.toml index 62521a7..1e0d308 100644 --- a/geekorm-core/Cargo.toml +++ b/geekorm-core/Cargo.toml @@ -52,6 +52,7 @@ search = [] libsql = ["backends", "dep:libsql", "dep:tokio"] rusqlite = ["backends", "dep:rusqlite", "dep:serde_rusqlite"] # sqlite = ["backends", "dep:sqlite"] +postgres = ["backends", "dep:tokio-postgres", "dep:bytes"] migrations = ["dep:syn", "dep:quote", "dep:proc-macro2"] @@ -84,6 +85,8 @@ sha-crypt = { version = "^0.5", optional = true } libsql = { version = "^0.6", optional = true } rusqlite = { version = "0.32", features = ["bundled"], optional = true } serde_rusqlite = { version = "^0.36", optional = true } +tokio-postgres = { version = "^0.7", optional = true } +bytes = { version = "^1.9", optional = true } # Tokenization quote = { version = "1", optional = true } diff --git a/geekorm-core/src/backends/mod.rs b/geekorm-core/src/backends/mod.rs index ec1441a..c90c4eb 100644 --- a/geekorm-core/src/backends/mod.rs +++ b/geekorm-core/src/backends/mod.rs @@ -67,6 +67,8 @@ use crate::{Query, QueryBuilder, QueryBuilderTrait, TableBuilder, TablePrimaryKe #[cfg(feature = "libsql")] pub mod libsql; +#[cfg(feature = "postgres")] +pub mod postgres; #[cfg(feature = "rusqlite")] pub mod rusqlite; diff --git a/geekorm-core/src/backends/postgres.rs b/geekorm-core/src/backends/postgres.rs new file mode 100644 index 0000000..2812ab0 --- /dev/null +++ b/geekorm-core/src/backends/postgres.rs @@ -0,0 +1,72 @@ +//! # Postgres Backend + +use super::GeekConnection; + +mod de; + +impl GeekConnection for tokio_postgres::Client { + type Connection = tokio_postgres::Client; + + async fn batch(connection: &Self::Connection, query: crate::Query) -> Result<(), crate::Error> { + #[cfg(feature = "log")] + { + log::debug!("Executing query: {}", query.query); + } + + connection.batch_execute(&query.query).await?; + + Ok(()) + } + + async fn query( + connection: &Self::Connection, + query: crate::Query, + ) -> Result, crate::Error> + where + T: serde::de::DeserializeOwned, + { + #[cfg(feature = "log")] + { + log::debug!("Executing query: {}", query.query); + } + + let parameters: &[&(dyn tokio_postgres::types::ToSql + Sync)] = &query + .parameters + .values + .iter() + .map(|(_name, value)| value as &(dyn tokio_postgres::types::ToSql + Sync)) + .collect::>(); + + let rows = connection.query(query.query.as_str(), ¶meters).await?; + + let mut results: Vec = Vec::new(); + for row in rows { + results.push(de::from_row::(&row).map_err(|e| { + #[cfg(feature = "log")] + { + log::error!("Error deserializing row: `{}`", e); + } + crate::Error::SerdeError(e.to_string()) + })?); + } + + Ok(results) + } + + async fn query_first( + connection: &Self::Connection, + query: crate::Query, + ) -> Result + where + T: serde::de::DeserializeOwned, + { + Err(crate::Error::NotImplemented) + } + + async fn execute( + connection: &Self::Connection, + query: crate::Query, + ) -> Result<(), crate::Error> { + Err(crate::Error::NotImplemented) + } +} diff --git a/geekorm-core/src/backends/postgres/de.rs b/geekorm-core/src/backends/postgres/de.rs new file mode 100644 index 0000000..b4db02b --- /dev/null +++ b/geekorm-core/src/backends/postgres/de.rs @@ -0,0 +1,149 @@ +//! # Postgres deserialization +use crate::Value; +use bytes::BytesMut; +use serde::de::{Error, MapAccess, Visitor}; +use serde::Deserialize; +use serde::{de::value::Error as DeError, Deserializer}; +use tokio_postgres::types::ToSql; + +struct PostgresRow<'de> { + row: &'de tokio_postgres::Row, +} + +impl<'de> Deserializer<'de> for PostgresRow<'de> { + type Error = DeError; + + fn deserialize_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + Err(DeError::custom("Expected a struct")) + } + + fn deserialize_struct( + self, + _name: &'static str, + _fields: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + struct RowMap<'a> { + row: &'a tokio_postgres::Row, + index: std::ops::Range, + value: Option<&'a tokio_postgres::types::Type>, + } + + impl<'de> MapAccess<'de> for RowMap<'de> { + type Error = DeError; + + fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> + where + K: serde::de::DeserializeSeed<'de>, + { + match self.index.next() { + Some(index) => { + let column = self + .row + .columns() + .get(index) + .ok_or(DeError::custom("Invalid column index"))?; + + self.value = Some(column.type_()); + + seed.deserialize(&mut *self).map(Some).transpose() + } + None => Ok(None), + } + } + + fn next_value_seed(&mut self, seed: V) -> Result + where + V: serde::de::DeserializeSeed<'de>, + { + let value = self.value.ok_or(DeError::custom("No value"))?; + + seed.deserialize(value.into_deserializer()) + } + } + + visitor.visit_map(RowMap { + row: self.row, + index: 0..self.row.len(), + value: None, + }) + } + + serde::forward_to_deserialize_any! { + bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string + bytes byte_buf option unit unit_struct newtype_struct seq tuple + tuple_struct map enum identifier ignored_any + } +} + +/// Convert a row to a struct +pub(crate) fn from_row<'de, T: Deserialize<'de>>( + row: &'de tokio_postgres::Row, +) -> Result { + let deserializer = PostgresRow { row }; + T::deserialize(deserializer) +} + +impl Value { + pub(super) fn to_postgres_type(&self) -> tokio_postgres::types::Type { + match self { + Value::Boolean(_) => tokio_postgres::types::Type::BOOL, + Value::Text(_) => tokio_postgres::types::Type::TEXT, + Value::Integer(_) => tokio_postgres::types::Type::INT8, + Value::Identifier(_) => tokio_postgres::types::Type::TEXT, + Value::Blob(_) => tokio_postgres::types::Type::BYTEA, + Value::Json(_) => tokio_postgres::types::Type::JSONB, + _ => unimplemented!(), + } + } +} + +impl ToSql for Value { + fn to_sql( + &self, + ty: &tokio_postgres::types::Type, + out: &mut BytesMut, + ) -> Result> + where + Self: Sized, + { + match self { + Value::Null => Ok(tokio_postgres::types::IsNull::Yes), + Value::Boolean(value) => value.to_sql(ty, out), + Value::Text(value) => value.to_sql(ty, out), + Value::Integer(value) => value.to_sql(ty, out), + Value::Identifier(value) => value.to_sql(ty, out), + Value::Blob(value) => value.to_sql(ty, out), + Value::Json(value) => value.to_sql(ty, out), + } + } + + fn accepts(ty: &tokio_postgres::types::Type) -> bool + where + Self: Sized, + { + ty.name() == "jsonb" + } + + fn to_sql_checked( + &self, + ty: &tokio_postgres::types::Type, + out: &mut BytesMut, + ) -> Result> { + match self { + Value::Null => Ok(tokio_postgres::types::IsNull::Yes), + Value::Boolean(value) => value.to_sql_checked(ty, out), + Value::Text(value) => value.to_sql_checked(ty, out), + Value::Integer(value) => value.to_sql_checked(ty, out), + Value::Identifier(value) => value.to_sql_checked(ty, out), + Value::Blob(value) => value.to_sql_checked(ty, out), + Value::Json(value) => value.to_sql_checked(ty, out), + } + } +} diff --git a/geekorm-core/src/error.rs b/geekorm-core/src/error.rs index ac36008..dee1642 100644 --- a/geekorm-core/src/error.rs +++ b/geekorm-core/src/error.rs @@ -73,6 +73,11 @@ pub enum Error { #[error("RuSQLite Error occurred: {0}")] RuSQLiteError(String), + /// Postgres Error + #[cfg(feature = "postgres")] + #[error("Postgres Error occurred: {0}")] + PostgresError(String), + /// Query Syntax Error #[error( "Query Syntax Error: {0}\n -> {1}\nPlease report this error to the GeekORM developers" @@ -128,3 +133,10 @@ pub enum MigrationError { #[error("Missing Migration: {0}")] MissingMigration(String), } + +#[cfg(feature = "postgres")] +impl From for Error { + fn from(e: tokio_postgres::Error) -> Self { + Self::PostgresError(e.to_string()) + } +} diff --git a/geekorm-derive/Cargo.toml b/geekorm-derive/Cargo.toml index fd56858..e95eae3 100644 --- a/geekorm-derive/Cargo.toml +++ b/geekorm-derive/Cargo.toml @@ -47,6 +47,7 @@ search = ["geekorm-core/search"] libsql = ["backends", "geekorm-core/libsql"] rusqlite = ["backends", "geekorm-core/rusqlite"] # sqlite = ["backends", "geekorm-core/sqlite"] +postgres = ["backends", "geekorm-core/postgres"] [lib] proc-macro = true