development_postgress #11

Merged
lee merged 11 commits from development_postgress into main 2025-07-31 13:32:49 +00:00
10 changed files with 1508 additions and 9 deletions

View File

@ -123,6 +123,11 @@ pub fn model(_attr: TokenStream, item: TokenStream) -> TokenStream {
}
};
let indexed_field_names = indexed_fields
.iter()
.map(|f| f.0.to_string())
.collect::<Vec<_>>();
let model_impl = quote! {
impl heromodels_core::Model for #struct_name {
fn db_prefix() -> &'static str {
@ -137,6 +142,12 @@ pub fn model(_attr: TokenStream, item: TokenStream) -> TokenStream {
&mut self.base_data
}
fn indexed_fields() -> Vec<&'static str> {
vec![
#(#indexed_field_names),*
]
}
#db_keys_impl
}
};
@ -169,6 +180,10 @@ pub fn model(_attr: TokenStream, item: TokenStream) -> TokenStream {
fn key() -> &'static str {
#index_key
}
fn field_name() -> &'static str {
#name_str
}
}
};

829
heromodels/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,11 +15,20 @@ tst = { path = "../tst" }
heromodels-derive = { path = "../heromodels-derive" }
heromodels_core = { path = "../heromodels_core" }
rhailib_derive = { package = "derive", path = "../../rhailib/src/derive" }
rhai = { version = "1.21.0", features = ["std", "sync", "decimal", "internals"] } # Added "decimal" feature, sync for Arc<Mutex<>>
rhai = { version = "1.21.0", features = [
"std",
"sync",
"decimal",
"internals",
] } # Added "decimal" feature, sync for Arc<Mutex<>>
rhai_client_macros = { path = "../rhai_client_macros" }
strum = "0.26"
strum_macros = "0.26"
uuid = { version = "1.17.0", features = ["v4"] }
postgres = { version = "0.19.10", features = ["with-serde_json-1"] }
jsonb = "0.5.2"
r2d2 = "0.8.10"
r2d2_postgres = "0.18.2"
[features]
default = []
@ -46,4 +55,4 @@ path = "examples/flow_example.rs"
[[example]]
name = "biz_rhai"
path = "examples/biz_rhai/example.rs"
required-features = ["rhai"]
required-features = ["rhai"]

View File

@ -0,0 +1,231 @@
use heromodels::db::postgres::Config;
use heromodels::db::{Collection, Db};
use heromodels::models::userexample::user::user_index::{is_active, username};
use heromodels::models::{Comment, User};
use heromodels_core::Model;
// Helper function to print user details
fn print_user_details(user: &User) {
println!("\n--- User Details ---");
println!("ID: {}", user.get_id());
println!("Username: {}", user.username);
println!("Email: {}", user.email);
println!("Full Name: {}", user.full_name);
println!("Active: {}", user.is_active);
println!("Created At: {}", user.base_data.created_at);
println!("Modified At: {}", user.base_data.modified_at);
println!("Comments: {:?}", user.base_data.comments);
}
// Helper function to print comment details
fn print_comment_details(comment: &Comment) {
println!("\n--- Comment Details ---");
println!("ID: {}", comment.get_id());
println!("User ID: {}", comment.user_id);
println!("Content: {}", comment.content);
println!("Created At: {}", comment.base_data.created_at);
println!("Modified At: {}", comment.base_data.modified_at);
}
fn main() {
let db = heromodels::db::postgres::Postgres::new(
Config::new()
.user(Some("postgres".into()))
.password(Some("test123".into()))
.host(Some("localhost".into()))
.port(Some(5432)),
)
.expect("Can connect to postgress");
println!("Hero Models - Basic Usage Example");
println!("================================");
// Create users with auto-generated IDs
// User 1
let user1 = User::new()
.username("johndoe")
.email("john.doe@example.com")
.full_name("John Doe")
.is_active(false)
.build();
// User 2
let user2 = User::new()
.username("janesmith")
.email("jane.smith@example.com")
.full_name("Jane Smith")
.is_active(true)
.build();
// User 3
let user3 = User::new()
.username("willism")
.email("willis.masters@example.com")
.full_name("Willis Masters")
.is_active(true)
.build();
// User 4
let user4 = User::new()
.username("carrols")
.email("carrol.smith@example.com")
.full_name("Carrol Smith")
.is_active(false)
.build();
// Save all users to database and get their assigned IDs and updated models
let (user1_id, db_user1) = db
.collection()
.expect("can open user collection")
.set(&user1)
.expect("can set user");
let (user2_id, db_user2) = db
.collection()
.expect("can open user collection")
.set(&user2)
.expect("can set user");
let (user3_id, db_user3) = db
.collection()
.expect("can open user collection")
.set(&user3)
.expect("can set user");
let (user4_id, db_user4) = db
.collection()
.expect("can open user collection")
.set(&user4)
.expect("can set user");
println!("User 1 assigned ID: {user1_id}");
println!("User 2 assigned ID: {user2_id}");
println!("User 3 assigned ID: {user3_id}");
println!("User 4 assigned ID: {user4_id}");
// We already have the updated models from the set method, so we don't need to retrieve them again
// Print all users retrieved from database
println!("\n--- Users Retrieved from Database ---");
println!("\n1. First user:");
print_user_details(&db_user1);
println!("\n2. Second user:");
print_user_details(&db_user2);
println!("\n3. Third user:");
print_user_details(&db_user3);
println!("\n4. Fourth user:");
print_user_details(&db_user4);
// Demonstrate different ways to retrieve users from the database
// 1. Retrieve by username index
println!("\n--- Retrieving Users by Different Methods ---");
println!("\n1. By Username Index:");
let stored_users = db
.collection::<User>()
.expect("can open user collection")
.get::<username, _>("johndoe")
.expect("can load stored user");
assert_eq!(stored_users.len(), 1);
print_user_details(&stored_users[0]);
// 2. Retrieve by active status
println!("\n2. By Active Status (Active = true):");
let active_users = db
.collection::<User>()
.expect("can open user collection")
.get::<is_active, _>(&true)
.expect("can load stored users");
assert_eq!(active_users.len(), 2);
for active_user in active_users.iter() {
print_user_details(active_user);
}
// 3. Delete a user and show the updated results
println!("\n3. After Deleting a User:");
let user_to_delete_id = active_users[0].get_id();
println!("Deleting user with ID: {user_to_delete_id}");
db.collection::<User>()
.expect("can open user collection")
.delete_by_id(user_to_delete_id)
.expect("can delete existing user");
// Show remaining active users
let active_users = db
.collection::<User>()
.expect("can open user collection")
.get::<is_active, _>(&true)
.expect("can load stored users");
println!(" a. Remaining Active Users:");
assert_eq!(active_users.len(), 1);
for active_user in active_users.iter() {
print_user_details(active_user);
}
// Show inactive users
let inactive_users = db
.collection::<User>()
.expect("can open user collection")
.get::<is_active, _>(&false)
.expect("can load stored users");
println!(" b. Inactive Users:");
assert_eq!(inactive_users.len(), 2);
for inactive_user in inactive_users.iter() {
print_user_details(inactive_user);
}
// Delete a user based on an index for good measure
db.collection::<User>()
.expect("can open user collection")
.delete::<username, _>("janesmith")
.expect("can delete existing user");
println!("\n--- User Model Information ---");
println!("User DB Prefix: {}", User::db_prefix());
// Demonstrate comment creation and association with a user
println!("\n--- Working with Comments ---");
// 1. Create and save a comment
println!("\n1. Creating a Comment:");
let comment = Comment::new()
.user_id(db_user1.get_id()) // commenter's user ID
.content("This is a comment on the user")
.build();
// Save the comment and get its assigned ID and updated model
let (comment_id, db_comment) = db
.collection()
.expect("can open comment collection")
.set(&comment)
.expect("can set comment");
println!("Comment assigned ID: {comment_id}");
println!(" a. Comment Retrieved from Database:");
print_comment_details(&db_comment);
// 3. Associate the comment with a user
println!("\n2. Associating Comment with User:");
let mut updated_user = db_user1.clone();
updated_user.base_data.add_comment(db_comment.get_id());
// Save the updated user and get the new version
let (_, user_with_comment) = db
.collection::<User>()
.expect("can open user collection")
.set(&updated_user)
.expect("can set updated user");
println!(" a. User with Associated Comment:");
print_user_details(&user_with_comment);
println!("\n--- Model Information ---");
println!("User DB Prefix: {}", User::db_prefix());
println!("Comment DB Prefix: {}", Comment::db_prefix());
}

View File

@ -4,6 +4,7 @@ use heromodels_core::{Index, Model};
use serde::{Deserialize, Serialize};
pub mod hero;
pub mod postgres;
pub trait Db {
/// Error type returned by database operations.
@ -27,7 +28,7 @@ where
where
I: Index<Model = V>,
I::Key: Borrow<Q>,
Q: ToString + ?Sized;
Q: ToString + Serialize + core::fmt::Debug + Sync + ?Sized;
/// Get an object from its ID. This does not use an index lookup
fn get_by_id(&self, id: u32) -> Result<Option<V>, Error<Self::Error>>;
@ -48,7 +49,7 @@ where
where
I: Index<Model = V>,
I::Key: Borrow<Q>,
Q: ToString + ?Sized;
Q: ToString + Serialize + core::fmt::Debug + Sync + ?Sized;
/// Delete an object with a given ID
fn delete_by_id(&self, id: u32) -> Result<(), Error<Self::Error>>;

View File

@ -8,8 +8,8 @@ use std::{
collections::HashSet,
path::PathBuf,
sync::{
Arc, Mutex,
atomic::{AtomicU32, Ordering},
Arc, Mutex,
},
};

View File

@ -0,0 +1,406 @@
use std::time::Duration;
use heromodels_core::Model;
use postgres::types::Json;
use postgres::{Client, NoTls};
use r2d2::Pool;
use r2d2_postgres::PostgresConnectionManager;
use serde::Serialize;
#[derive(Clone)]
pub struct Postgres {
pool: Pool<PostgresConnectionManager<NoTls>>,
}
/// Configuration used to connect to a postgres server. All values are optional, if they are
/// [Option::None], the postgres defaults are used.
#[derive(Default)]
pub struct Config {
pub user: Option<String>,
pub password: Option<String>,
pub dbname: Option<String>,
pub host: Option<String>,
pub port: Option<u16>,
}
impl Config {
/// Create a new empty config
pub fn new() -> Self {
Self::default()
}
/// Set the user value in the config
pub fn user(mut self, user: Option<String>) -> Self {
self.user = user;
self
}
/// Set the password value in the config
pub fn password(mut self, password: Option<String>) -> Self {
self.password = password;
self
}
/// Set the dbname value in the config
pub fn dbname(mut self, dbname: Option<String>) -> Self {
self.dbname = dbname;
self
}
/// Set the host value in the config
pub fn host(mut self, host: Option<String>) -> Self {
self.host = host;
self
}
/// Set the port value in the config
pub fn port(mut self, port: Option<u16>) -> Self {
self.port = port;
self
}
}
#[derive(Debug)]
pub enum Error {
/// An error communicating with the postgres server
Postgres(postgres::error::Error),
/// An error originating from the connection pool
ConnectionPool(r2d2::Error),
/// An error encoding to or decoding from json
Json(serde_json::Error),
/// An error encoding to or decoding from jsonb
JsonB(jsonb::Error),
/// We tried to insert a value but the row was not returned
FailedInsert,
/// We tried to update a value but we didn't get the expected 1 modified row
FailedUpdate(usize),
/// We tried to query the existence of a table but it failed
TableExistenceQuery,
/// Transactions aren't supported
Transaction,
}
impl Postgres {
/// Create a new connection to a postgres instance.
pub fn new(config: Config) -> Result<Self, Error> {
let mut cfg = Client::configure();
cfg.user(config.user.unwrap_or_default().as_ref())
.password(config.password.unwrap_or_default())
.dbname(config.dbname.unwrap_or_default().as_ref())
.host(config.host.unwrap_or_default().as_ref())
.port(config.port.unwrap_or_default());
// No TLS support for now
// let client = cfg.connect(postgres::tls::NoTls)?;
let manager = PostgresConnectionManager::new(cfg, NoTls);
let pool = Pool::builder()
.connection_timeout(Duration::from_secs(1))
.max_size(5)
.build(manager)?;
Ok(Postgres { pool })
}
/// Helper method which generates a table name for a model, to alleviate some keyword
/// conflicts.
fn collection_name<M: Model>() -> String {
format!("model_{}", M::db_prefix())
}
}
impl super::Db for Postgres {
type Error = Error;
fn collection<M: heromodels_core::Model>(
&self,
) -> Result<impl super::Collection<&str, M>, super::Error<Self::Error>> {
// Check if the table exists, if not create it with proper indexes
let mut con = self.pool.get().map_err(Error::from)?;
let row = con
.query_one(
r#"SELECT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_name = $1
) AS table_existence;"#,
&[&Self::collection_name::<M>()],
)
.map_err(Error::from)?;
let exists: bool = row.get("table_existence");
// If table does not exist set it up
if !exists {
// Use a transaction here so if index creation failed the table is also not created.
let mut tx = con.transaction().map_err(Error::from)?;
tx.execute(
&format!(
"CREATE TABLE {} (key BIGSERIAL PRIMARY KEY, value JSONB NOT NULL);",
Self::collection_name::<M>(),
),
&[],
)
.map_err(Error::from)?;
for indexed_field in M::indexed_fields() {
tx.execute(
&format!(
"CREATE INDEX {0}_{1}_idx ON {0} ( (value->'{1}') )",
Self::collection_name::<M>(),
indexed_field,
),
&[],
)
.map_err(Error::from)?;
}
tx.commit().map_err(Error::from)?;
}
Ok(self.clone())
}
}
impl<M> super::Collection<&str, M> for Postgres
where
M: Model,
{
type Error = Error;
fn get<I, Q>(&self, key: &Q) -> Result<Vec<M>, super::Error<Self::Error>>
where
I: heromodels_core::Index<Model = M>,
I::Key: std::borrow::Borrow<Q>,
Q: ToString + Serialize + core::fmt::Debug + Sync + ?Sized,
{
let mut con = self.pool.get().map_err(Error::from)?;
Ok(con
.query(
&format!(
"SELECT (value) FROM {} WHERE value->$1 = $2;",
Self::collection_name::<M>(),
),
&[&I::field_name(), &Json(key)],
)
.map_err(Error::from)?
.into_iter()
.map(|row| {
row.try_get::<_, Json<M>>("value")
.map(|v| v.0)
.map_err(Error::from)
})
.collect::<Result<Vec<M>, _>>()?)
}
fn get_by_id(&self, id: u32) -> Result<Option<M>, super::Error<Self::Error>> {
let mut con = self.pool.get().map_err(Error::from)?;
if let Some(row) = con
.query(
&format!(
"SELECT (value) FROM {} WHERE key = $1;",
Self::collection_name::<M>()
),
&[&(id as i64)],
)
.map_err(Error::from)?
.into_iter()
.next()
{
Ok(Some(
row.try_get::<_, Json<M>>("value").map_err(Error::from)?.0,
))
} else {
Ok(None)
}
}
fn set(&self, value: &M) -> Result<(u32, M), super::Error<Self::Error>> {
let mut con = self.pool.get().map_err(Error::from)?;
if value.get_id() == 0 {
// NOTE: We perform a query here since we want the returned value which has the updated ID
let Some(row) = con
.query(
&format!(
"INSERT INTO {} (value) VALUES ($1) RETURNING key, value;",
Self::collection_name::<M>()
),
&[&Json(value)],
)
.map_err(Error::from)?
.into_iter()
.next()
else {
return Err(Error::FailedInsert.into());
};
// Get the generated ID
let id = row.get::<_, i64>("key") as u32;
let mut value = row.get::<_, Json<M>>("value").0;
// .map_err(Error::from)?;
value.base_data_mut().id = id;
// NOTE: Update the value so the id is set correctly in the value itself
let updated = con
.execute(
&format!(
"UPDATE {} SET value = $1 WHERE key = $2;",
Self::collection_name::<M>()
),
&[&Json(value.clone()), &(value.get_id() as i64)],
)
.map_err(Error::from)?;
if updated != 1 {
return Err(Error::FailedUpdate(updated as usize).into());
}
Ok((id, value))
} else {
let updated = con
.execute(
&format!(
"UPDATE {} SET value = $1 WHERE key = $2;",
Self::collection_name::<M>(),
),
&[&Json(value.clone()), &(value.get_id() as i64)],
)
.map_err(Error::from)?;
if updated != 1 {
return Err(Error::FailedUpdate(updated as usize).into());
}
Ok((value.get_id(), value.clone()))
}
}
fn delete<I, Q>(&self, key: &Q) -> Result<(), super::Error<Self::Error>>
where
I: heromodels_core::Index<Model = M>,
I::Key: std::borrow::Borrow<Q>,
Q: ToString + Serialize + core::fmt::Debug + Sync + ?Sized,
{
let mut con = self.pool.get().map_err(Error::from)?;
con.execute(
&format!(
"DELETE FROM {} WHERE value->$1 = $2;",
Self::collection_name::<M>()
),
&[&I::field_name(), &Json(key)],
)
.map_err(Error::from)?;
Ok(())
}
fn delete_by_id(&self, id: u32) -> Result<(), super::Error<Self::Error>> {
let mut con = self.pool.get().map_err(Error::from)?;
con.execute(
&format!(
"DELETE FROM {} WHERE key = $1;",
Self::collection_name::<M>()
),
&[&(id as i64)],
)
.map_err(Error::from)?;
Ok(())
}
fn get_all(&self) -> Result<Vec<M>, super::Error<Self::Error>> {
let mut con = self.pool.get().map_err(Error::from)?;
Ok(con
.query(
&format!("SELECT (value) FROM {};", Self::collection_name::<M>()),
&[],
)
.map_err(Error::from)?
.into_iter()
.map(|row| {
row.try_get::<_, Json<M>>("value")
.map_err(Error::from)
.map(|v| v.0)
})
.collect::<Result<Vec<M>, _>>()?)
}
fn begin_transaction(
&self,
) -> Result<Box<dyn super::Transaction<Error = Self::Error>>, super::Error<Self::Error>> {
Err(Error::Transaction.into())
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Postgres(e) => f.write_fmt(format_args!("postgres error: {e}")),
Self::ConnectionPool(e) => {
f.write_fmt(format_args!("postgres connection pool error: {e}"))
}
Self::Json(e) => {
f.write_fmt(format_args!("could not decode from or encode to json: {e}"))
}
Self::JsonB(e) => f.write_fmt(format_args!(
"could not decode from or encode to jsonb: {e}"
)),
Self::FailedInsert => f.write_str("insert did not return any result"),
Self::FailedUpdate(amount) => f.write_fmt(format_args!(
"update did not return the expected 1 modified row (got {amount})"
)),
Self::TableExistenceQuery => f.write_str("query to check if table exists failed"),
Self::Transaction => f.write_str("transactions aren't supported"),
}
}
}
impl core::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Postgres(e) => Some(e),
Self::ConnectionPool(e) => Some(e),
Self::Json(e) => Some(e),
Self::JsonB(e) => Some(e),
_ => None,
}
}
}
impl From<postgres::error::Error> for Error {
fn from(value: postgres::error::Error) -> Self {
Self::Postgres(value)
}
}
impl From<r2d2::Error> for Error {
fn from(value: r2d2::Error) -> Self {
Self::ConnectionPool(value)
}
}
impl From<serde_json::Error> for Error {
fn from(value: serde_json::Error) -> Self {
Self::Json(value)
}
}
impl From<jsonb::Error> for Error {
fn from(value: jsonb::Error) -> Self {
Self::JsonB(value)
}
}
impl From<Error> for super::Error<Error> {
fn from(value: Error) -> Self {
super::Error::DB(value)
}
}

View File

@ -84,6 +84,9 @@ impl Index for CompanyNameIndex {
fn key() -> &'static str {
"name"
}
fn field_name() -> &'static str {
"name"
}
}
pub struct CompanyRegistrationNumberIndex;
@ -93,6 +96,9 @@ impl Index for CompanyRegistrationNumberIndex {
fn key() -> &'static str {
"registration_number"
}
fn field_name() -> &'static str {
"registration_number"
}
}
// --- Builder Pattern ---

View File

@ -58,6 +58,11 @@ pub trait Model:
Vec::new()
}
/// Return a list of field names which have an index applied.
fn indexed_fields() -> Vec<&'static str> {
Vec::new()
}
/// Get the unique ID for this model
fn get_id(&self) -> u32;
@ -83,6 +88,11 @@ pub trait Index {
/// The key of this index
fn key() -> &'static str;
/// The original field name. This is the same as [Index::key] by default, unless the user
/// specified a `(name=...)` value to the index attribute, in which case [Index::key] will be
/// set to the specified key.
fn field_name() -> &'static str;
}
/// Base struct that all models should include