use crate::test_db::TestFile;
use agdb::CountComparison;
use agdb::Db;
use agdb::DbError;
use agdb::DbId;
use agdb::DbKeyOrder;
use agdb::DbKeyValue;
use agdb::DbType;
use agdb::QueryBuilder;
use agdb::QueryId;
use std::ops::Deref;
use std::ops::DerefMut;
use std::sync::Arc;
use std::sync::RwLock;
#[derive(DbType)]
struct User {
username: String,
email: String,
password: String,
}
#[derive(DbType)]
struct Post {
db_id: Option<DbId>,
title: String,
body: String,
}
#[derive(DbType)]
struct PostLiked {
db_id: Option<DbId>,
title: String,
body: String,
likes: i64,
}
#[derive(DbType)]
struct Comment {
body: String,
}
fn create_db() -> Result<Arc<RwLock<Db>>, DbError> {
let db = Arc::new(RwLock::new(Db::new("social.agdb")?));
db.write()?.transaction_mut(|t| -> Result<(), DbError> {
t.exec_mut(
QueryBuilder::insert()
.nodes()
.aliases(["root", "users", "posts"])
.query(),
)?;
t.exec_mut(
QueryBuilder::insert()
.edges()
.from("root")
.to(["users", "posts"])
.query(),
)?;
Ok(())
})?;
Ok(db)
}
fn register_user(db: &mut Db, user: &User) -> Result<DbId, DbError> {
db.transaction_mut(|t| -> Result<DbId, DbError> {
if t.exec(
QueryBuilder::search()
.from("users")
.where_()
.key("username")
.value(&user.username)
.query(),
)?
.result
!= 0
{
return Err(DbError::from(format!(
"User {} already exists.",
user.username
)));
}
let user = t
.exec_mut(QueryBuilder::insert().element(user).query())?
.elements[0]
.id;
t.exec_mut(
QueryBuilder::insert()
.edges()
.from("users")
.to(user)
.query(),
)?;
Ok(user)
})
}
fn create_post(db: &mut Db, user: DbId, post: &Post) -> Result<DbId, DbError> {
db.transaction_mut(|t| -> Result<DbId, DbError> {
let post = t
.exec_mut(QueryBuilder::insert().element(post).query())?
.elements[0]
.id;
t.exec_mut(
QueryBuilder::insert()
.edges()
.from([QueryId::from("posts"), user.into()])
.to(post)
.values([vec![], vec![("authored", 1_u64).into()]])
.query(),
)?;
Ok(post)
})
}
fn create_comment(
db: &mut Db,
user: DbId,
parent: DbId,
comment: &Comment,
) -> Result<DbId, DbError> {
db.transaction_mut(|t| -> Result<DbId, DbError> {
let comment = t
.exec_mut(QueryBuilder::insert().element(comment).query())?
.elements[0]
.id;
t.exec_mut(
QueryBuilder::insert()
.edges()
.from([parent, user])
.to(comment)
.values([vec![], vec![("commented", 1_u64).into()]])
.query(),
)?;
Ok(comment)
})
}
fn like(db: &mut Db, user: DbId, id: DbId) -> Result<(), DbError> {
db.exec_mut(
QueryBuilder::insert()
.edges()
.from(user)
.to(id)
.values_uniform([("liked", 1).into()])
.query(),
)?;
Ok(())
}
fn remove_like(db: &mut Db, user: DbId, id: DbId) -> Result<(), DbError> {
db.transaction_mut(|t| -> Result<(), DbError> {
t.exec_mut(
QueryBuilder::remove()
.search()
.from(user)
.to(id)
.where_()
.keys("liked")
.query(),
)?;
Ok(())
})
}
fn login(db: &Db, username: &str, password: &str) -> Result<DbId, DbError> {
let result = db
.exec(
QueryBuilder::select()
.values("password")
.search()
.depth_first()
.from("users")
.limit(1)
.where_()
.neighbor()
.and()
.key("username")
.value(username)
.query(),
)?
.elements;
let user = result
.first()
.ok_or(DbError::from(format!("Username '{username}' not found")))?;
let pswd = user.values[0].value.to_string();
if password != pswd {
return Err(DbError::from("Password is incorrect"));
}
Ok(user.id)
}
fn user_posts_ids(db: &Db, user: DbId) -> Result<Vec<DbId>, DbError> {
Ok(db
.exec(
QueryBuilder::search()
.from(user)
.where_()
.neighbor()
.and()
.beyond()
.where_()
.keys("authored")
.or()
.node()
.query(),
)?
.ids())
}
fn post_titles(db: &Db, ids: Vec<DbId>) -> Result<Vec<String>, DbError> {
Ok(db
.exec(QueryBuilder::select().values("title").ids(ids).query())?
.elements
.into_iter()
.map(|post| post.values[0].value.to_string())
.collect())
}
fn posts(db: &Db, offset: u64, limit: u64) -> Result<Vec<Post>, DbError> {
db.exec(
QueryBuilder::select()
.elements::<Post>()
.search()
.from("posts")
.offset(offset)
.limit(limit)
.where_()
.neighbor()
.query(),
)?
.try_into()
}
fn comments(db: &Db, id: DbId) -> Result<Vec<Comment>, DbError> {
db.exec(
QueryBuilder::select()
.elements::<Comment>()
.search()
.depth_first()
.from(id)
.where_()
.node()
.and()
.distance(CountComparison::GreaterThan(1))
.query(),
)?
.try_into()
}
fn add_likes_to_posts(db: &mut Db) -> Result<(), DbError> {
db.transaction_mut(|t| -> Result<(), DbError> {
let posts = t.exec(
QueryBuilder::search()
.from("posts")
.where_()
.neighbor()
.query(),
)?;
let mut likes = Vec::<Vec<DbKeyValue>>::new();
for post in posts.ids() {
let post_likes = t
.exec(
QueryBuilder::search()
.to(post)
.where_()
.distance(1)
.and()
.keys("liked")
.query(),
)?
.result;
likes.push(vec![("likes", post_likes).into()]);
}
t.exec_mut(QueryBuilder::insert().values(likes).ids(posts).query())?;
Ok(())
})
}
fn liked_posts(db: &Db, offset: u64, limit: u64) -> Result<Vec<PostLiked>, DbError> {
db.exec(
QueryBuilder::select()
.elements::<PostLiked>()
.search()
.from("posts")
.order_by([DbKeyOrder::Desc("likes".into())])
.offset(offset)
.limit(limit)
.where_()
.neighbor()
.query(),
)?
.try_into()
}
fn mark_top_level_comments(db: &mut Db) -> Result<(), DbError> {
db.exec_mut(
QueryBuilder::insert()
.values_uniform([("level", 1).into()])
.search()
.from("posts")
.where_()
.distance(4)
.query(),
)?;
Ok(())
}
#[test]
fn efficient_agdb() -> Result<(), DbError> {
let _test_file = TestFile::from("social.agdb");
let db = create_db()?;
let user = User {
username: "john_doe".to_string(),
email: "john@doe.com".to_string(),
password: "password123".to_string(),
};
register_user(db.write()?.deref_mut(), &user)?;
let user_id = login(db.read()?.deref(), "john_doe", "password123")?;
let post = Post {
db_id: None,
title: "Awesome".to_string(),
body: "http://pictures.com/awesome.png".to_string(),
};
let post_id = create_post(db.write()?.deref_mut(), user_id, &post)?;
let comment = Comment {
body: "This is truly awesome".to_string(),
};
let comment_id = create_comment(db.write()?.deref_mut(), user_id, post_id, &comment)?;
let reply_comment = Comment {
body: "Indeed it is".to_string(),
};
let reply_comment_id =
create_comment(db.write()?.deref_mut(), user_id, comment_id, &reply_comment)?;
like(db.write()?.deref_mut(), user_id, post_id)?;
like(db.write()?.deref_mut(), user_id, comment_id)?;
like(db.write()?.deref_mut(), user_id, reply_comment_id)?;
remove_like(db.write()?.deref_mut(), user_id, comment_id)?;
let posts = posts(db.read()?.deref(), 0, 10)?;
assert_eq!(posts.len(), 1);
let user_posts = user_posts_ids(db.read()?.deref(), user_id)?;
assert_eq!(user_posts.len(), 1);
let user_post_titles = post_titles(db.read()?.deref(), user_posts)?;
assert_eq!(user_post_titles, vec!["Awesome"]);
let comments = comments(db.read()?.deref(), post_id)?;
assert_eq!(comments.len(), 2);
add_likes_to_posts(db.write()?.deref_mut())?;
mark_top_level_comments(db.write()?.deref_mut())?;
let posts = liked_posts(db.read()?.deref(), 0, 10)?;
assert_eq!(posts.len(), 1);
Ok(())
}