Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial Postgres support #173

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down
3 changes: 3 additions & 0 deletions geekorm-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down Expand Up @@ -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 }
Expand Down
2 changes: 2 additions & 0 deletions geekorm-core/src/backends/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
72 changes: 72 additions & 0 deletions geekorm-core/src/backends/postgres.rs
Original file line number Diff line number Diff line change
@@ -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<T>(
connection: &Self::Connection,
query: crate::Query,
) -> Result<Vec<T>, 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::<Vec<_>>();

let rows = connection.query(query.query.as_str(), &parameters).await?;

let mut results: Vec<T> = Vec::new();
for row in rows {
results.push(de::from_row::<T>(&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<T>(
connection: &Self::Connection,
query: crate::Query,
) -> Result<T, crate::Error>
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)
}
}
149 changes: 149 additions & 0 deletions geekorm-core/src/backends/postgres/de.rs
Original file line number Diff line number Diff line change
@@ -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<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
Err(DeError::custom("Expected a struct"))
}

fn deserialize_struct<V>(
self,
_name: &'static str,
_fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
struct RowMap<'a> {
row: &'a tokio_postgres::Row,
index: std::ops::Range<usize>,
value: Option<&'a tokio_postgres::types::Type>,
}

impl<'de> MapAccess<'de> for RowMap<'de> {
type Error = DeError;

fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, 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<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
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<T, DeError> {
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<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>>
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<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
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),
}
}
}
12 changes: 12 additions & 0 deletions geekorm-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -128,3 +133,10 @@ pub enum MigrationError {
#[error("Missing Migration: {0}")]
MissingMigration(String),
}

#[cfg(feature = "postgres")]
impl From<tokio_postgres::Error> for Error {
fn from(e: tokio_postgres::Error) -> Self {
Self::PostgresError(e.to_string())
}
}
1 change: 1 addition & 0 deletions geekorm-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading