mod inspect;
use candid::Principal;
use ic_dbms_api::prelude::{
CandidColumnDef, ColumnDef, Database as _, DeleteBehavior, Filter, IcDbmsResult, InsertRecord,
Query, TableSchema, TransactionId, UpdateRecord, Value,
};
use wasm_dbms::prelude::{DatabaseSchema, WasmDbmsDatabase};
pub use self::inspect::inspect;
use crate::memory::{DBMS_CONTEXT, IcAccessControlList, IcMemoryProvider};
use crate::trap;
pub fn acl_add_principal(principal: Principal) -> IcDbmsResult<()> {
assert_caller_is_allowed();
DBMS_CONTEXT.with(|ctx| ctx.acl_add(principal))
}
pub fn acl_remove_principal(principal: Principal) -> IcDbmsResult<()> {
assert_caller_is_allowed();
DBMS_CONTEXT.with(|ctx| ctx.acl_remove(&principal))
}
pub fn acl_allowed_principals() -> Vec<Principal> {
assert_caller_is_allowed();
DBMS_CONTEXT.with(|ctx| ctx.acl_allowed())
}
pub fn begin_transaction() -> TransactionId {
assert_caller_is_allowed();
let owner = crate::utils::caller();
DBMS_CONTEXT.with(|ctx| ctx.begin_transaction(owner.as_slice().to_vec()))
}
pub fn commit<S>(transaction_id: TransactionId, database_schema: S) -> IcDbmsResult<()>
where
S: DatabaseSchema<IcMemoryProvider, IcAccessControlList> + 'static,
{
assert_caller_is_allowed();
assert_caller_owns_transaction(Some(&transaction_id));
DBMS_CONTEXT.with(|ctx| {
let mut db = WasmDbmsDatabase::from_transaction(ctx, database_schema, transaction_id);
db.commit()
})
}
pub fn rollback<S>(transaction_id: TransactionId, database_schema: S) -> IcDbmsResult<()>
where
S: DatabaseSchema<IcMemoryProvider, IcAccessControlList> + 'static,
{
assert_caller_is_allowed();
assert_caller_owns_transaction(Some(&transaction_id));
DBMS_CONTEXT.with(|ctx| {
let mut db = WasmDbmsDatabase::from_transaction(ctx, database_schema, transaction_id);
db.rollback()
})
}
pub fn select<T, S>(
query: Query,
transaction_id: Option<TransactionId>,
database_schema: S,
) -> IcDbmsResult<Vec<T::Record>>
where
T: TableSchema,
S: DatabaseSchema<IcMemoryProvider, IcAccessControlList> + 'static,
{
assert_caller_is_allowed();
assert_caller_owns_transaction(transaction_id.as_ref());
with_database(transaction_id, database_schema, |db| db.select::<T>(query))
}
pub fn select_raw<S>(
table: &str,
query: Query,
transaction_id: Option<TransactionId>,
database_schema: S,
) -> IcDbmsResult<Vec<Vec<(ColumnDef, Value)>>>
where
S: DatabaseSchema<IcMemoryProvider, IcAccessControlList> + 'static,
{
assert_caller_is_allowed();
assert_caller_owns_transaction(transaction_id.as_ref());
with_database(transaction_id, database_schema, |db| {
db.select_raw(table, query)
})
}
pub fn select_join<S>(
table: &str,
query: Query,
transaction_id: Option<TransactionId>,
database_schema: S,
) -> IcDbmsResult<Vec<Vec<(CandidColumnDef, Value)>>>
where
S: DatabaseSchema<IcMemoryProvider, IcAccessControlList> + 'static,
{
assert_caller_is_allowed();
assert_caller_owns_transaction(transaction_id.as_ref());
with_database(transaction_id, database_schema, |db| {
db.select_join(table, query)
})
}
pub fn insert<T, S>(
record: T::Insert,
transaction_id: Option<TransactionId>,
database_schema: S,
) -> IcDbmsResult<()>
where
T: TableSchema,
T::Insert: InsertRecord<Schema = T>,
S: DatabaseSchema<IcMemoryProvider, IcAccessControlList> + 'static,
{
assert_caller_is_allowed();
assert_caller_owns_transaction(transaction_id.as_ref());
with_database(transaction_id, database_schema, |db| db.insert::<T>(record))
}
pub fn update<T, S>(
patch: T::Update,
transaction_id: Option<TransactionId>,
database_schema: S,
) -> IcDbmsResult<u64>
where
T: TableSchema,
T::Update: UpdateRecord<Schema = T>,
S: DatabaseSchema<IcMemoryProvider, IcAccessControlList> + 'static,
{
assert_caller_is_allowed();
assert_caller_owns_transaction(transaction_id.as_ref());
with_database(transaction_id, database_schema, |db| db.update::<T>(patch))
}
pub fn delete<T, S>(
behaviour: DeleteBehavior,
filter: Option<Filter>,
transaction_id: Option<TransactionId>,
database_schema: S,
) -> IcDbmsResult<u64>
where
T: TableSchema,
S: DatabaseSchema<IcMemoryProvider, IcAccessControlList> + 'static,
{
assert_caller_is_allowed();
assert_caller_owns_transaction(transaction_id.as_ref());
with_database(transaction_id, database_schema, |db| {
db.delete::<T>(behaviour, filter)
})
}
fn with_database<S, F, R>(transaction_id: Option<TransactionId>, database_schema: S, f: F) -> R
where
S: DatabaseSchema<IcMemoryProvider, IcAccessControlList> + 'static,
F: for<'a> FnOnce(&WasmDbmsDatabase<'a, IcMemoryProvider, IcAccessControlList>) -> R,
{
DBMS_CONTEXT.with(|ctx| {
let db = match transaction_id {
Some(tx_id) => WasmDbmsDatabase::from_transaction(ctx, database_schema, tx_id),
None => WasmDbmsDatabase::oneshot(ctx, database_schema),
};
f(&db)
})
}
fn assert_caller_is_allowed() {
let caller = crate::utils::caller();
if !DBMS_CONTEXT.with(|ctx| ctx.acl_is_allowed(&caller)) {
trap!("Caller {caller} is not allowed to perform this operation");
}
}
fn assert_caller_owns_transaction(transaction_id: Option<&TransactionId>) {
let Some(tx_id) = transaction_id else {
return;
};
let caller = crate::utils::caller();
if !DBMS_CONTEXT.with(|ctx| ctx.has_transaction(tx_id, caller.as_slice())) {
trap!("Caller {caller} does not own transaction {tx_id}");
}
}
#[cfg(test)]
mod tests {
use ic_dbms_api::prelude::Uint32;
use super::*;
use crate::tests::{UserInsertRequest, load_fixtures};
#[test]
fn test_should_insert_into_acl() {
init_acl();
let bob = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap();
assert!(acl_add_principal(bob).is_ok());
let allowed = acl_allowed_principals();
assert!(allowed.contains(&bob));
assert!(allowed.contains(&alice()));
}
#[test]
fn test_should_remove_from_acl() {
init_acl();
let bob = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap();
assert!(acl_add_principal(bob).is_ok());
assert!(acl_remove_principal(bob).is_ok());
let allowed = acl_allowed_principals();
assert!(!allowed.contains(&bob));
assert!(allowed.contains(&alice()));
}
#[test]
fn test_should_list_acl_principals() {
init_acl();
let allowed = acl_allowed_principals();
assert!(allowed.contains(&alice()));
}
#[test]
fn test_should_begin_transaction() {
init_acl();
let _tx_id = begin_transaction();
}
#[test]
fn test_should_commit_transaction() {
init_acl();
let tx_id = begin_transaction();
let res = commit(tx_id, crate::tests::TestDatabaseSchema);
assert!(res.is_ok());
}
#[test]
fn test_should_rollback_transaction() {
init_acl();
let tx_id = begin_transaction();
let res = rollback(tx_id, crate::tests::TestDatabaseSchema);
assert!(res.is_ok());
}
#[test]
fn test_should_insert_record() {
load_fixtures();
init_acl();
let record = UserInsertRequest {
id: 100u32.into(),
name: "Alice".to_string().into(),
email: "alice@example.com".into(),
age: 25u32.into(),
};
let res = insert::<crate::tests::User, _>(record, None, crate::tests::TestDatabaseSchema);
assert!(res.is_ok());
}
#[test]
fn test_should_select_record() {
init_acl();
load_fixtures();
let query = Query::builder().all().limit(10).build();
let res = select::<crate::tests::User, _>(query, None, crate::tests::TestDatabaseSchema);
assert!(res.is_ok());
let records = res.unwrap();
assert!(!records.is_empty());
}
#[test]
fn test_should_update_record() {
init_acl();
load_fixtures();
let patch = crate::tests::UserUpdateRequest {
id: None,
name: Some("Robert".into()),
email: Some("robert@example.com".into()),
age: None,
where_clause: Some(Filter::Eq("id".to_string(), Uint32::from(1u32).into())),
};
let res = update::<crate::tests::User, _>(patch, None, crate::tests::TestDatabaseSchema);
assert!(res.is_ok());
}
#[test]
fn test_should_delete_record() {
init_acl();
load_fixtures();
let filter = Some(Filter::Eq("id".to_string(), Uint32::from(2u32).into()));
let res = delete::<crate::tests::User, _>(
DeleteBehavior::Cascade,
filter,
None,
crate::tests::TestDatabaseSchema,
);
assert!(res.is_ok());
}
#[test]
fn test_should_select_raw_record() {
init_acl();
load_fixtures();
let query = Query::builder().all().limit(10).build();
let res = select_raw("users", query, None, crate::tests::TestDatabaseSchema);
assert!(res.is_ok());
let rows = res.unwrap();
assert!(!rows.is_empty());
for row in &rows {
assert!(row.iter().any(|(col, _)| col.name == "id"));
assert!(row.iter().any(|(col, _)| col.name == "name"));
}
}
#[test]
fn test_should_fail_select_raw_unknown_table() {
init_acl();
load_fixtures();
let query = Query::builder().all().build();
let res = select_raw("nonexistent", query, None, crate::tests::TestDatabaseSchema);
assert!(res.is_err());
}
#[test]
#[should_panic = "Caller ghsi2-tqaaa-aaaan-aaaca-cai does not own transaction 0"]
fn test_should_not_allow_operating_wrong_tx() {
init_acl();
load_fixtures();
let bob = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap();
let tx_id = DBMS_CONTEXT.with(|ctx| ctx.begin_transaction(bob.as_slice().to_vec()));
let _ = commit(tx_id, crate::tests::TestDatabaseSchema);
}
fn alice() -> Principal {
crate::utils::caller()
}
fn init_acl() {
DBMS_CONTEXT.with(|ctx| {
ctx.acl_add(alice()).unwrap();
});
}
}