use std::path::PathBuf;
use std::sync::{LazyLock, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use appdb::connection::{DbRuntime, InitDbOptions, get_db, reinit_db, reinit_db_with_options};
use appdb::crypto::{
CryptoContext, SensitiveFieldTag, SensitiveModelTag, clear_crypto_context_registry,
register_crypto_context_for, reset_default_crypto_config, set_default_crypto_config,
};
use appdb::error::classify_db_error_text;
use appdb::graph::{GraphCrud, GraphRepo};
use appdb::model::meta::{HasId, ModelMeta, ResolveRecordId, UniqueLookupMeta, register_table};
use appdb::model::relation::relation_name;
use appdb::query::{RawSqlStmt, query_bound_checked, query_bound_return};
use appdb::repository::Repo;
use appdb::tx::{TxStmt, run_tx};
use appdb::{
AutoFill, Bridge, Crud, DBError, DBErrorKind, Id, Order, Relation, Sensitive, SensitiveShape,
SensitiveValueOf, Store, StoredModel,
};
use serde::{Deserialize, Serialize};
use surrealdb::types::{RecordId, SurrealValue, Table};
use tokio::runtime::Runtime;
#[derive(Clone, Default)]
struct RecordingForeignPersistence {
events: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
}
impl RecordingForeignPersistence {
fn events(&self) -> Vec<String> {
self.events.lock().expect("events lock poisoned").clone()
}
}
impl appdb::ForeignPersistence for RecordingForeignPersistence {
async fn exists_record(&self, record: RecordId) -> anyhow::Result<bool> {
self.events
.lock()
.expect("events lock poisoned")
.push(format!("exists:{record:?}"));
Repo::<ItAtomicSaveChild>::exists_record(record).await
}
async fn ensure_at<T>(&self, id: RecordId, data: T) -> anyhow::Result<T>
where
T: ModelMeta + Crud + StoredModel + appdb::ForeignModel + Send,
{
self.events
.lock()
.expect("events lock poisoned")
.push(format!("ensure_at:{id:?}"));
Repo::<T>::upsert_at(id, data).await
}
async fn create<T>(&self, data: T) -> anyhow::Result<T>
where
T: ModelMeta + Crud + StoredModel + appdb::ForeignModel + Send,
{
self.events
.lock()
.expect("events lock poisoned")
.push(format!("create:{}", T::table_name()));
Repo::<T>::create(data).await
}
}
static TEST_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
static TEST_RT: LazyLock<Runtime> =
LazyLock::new(|| Runtime::new().expect("integration runtime should be created"));
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItStringUser {
id: Id,
payload: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue, Store)]
struct ItNumberUser {
id: Id,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItNumberForeignChild {
id: Id,
name: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItNumberForeignParent {
id: Id,
#[foreign]
child: ItNumberForeignChild,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue)]
struct ItRecordUser {
#[serde(deserialize_with = "appdb::serde_utils::id::deserialize_record_id_or_compat_string")]
id: RecordId,
name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue, Store)]
struct ItNoId {
name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue, Store)]
struct ItProfile {
id: Id,
#[unique]
name: String,
note: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItAliasedPost {
id: Id,
#[unique]
slug: String,
title: String,
body: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItPagedEntry {
id: Id,
#[pagin]
created_at: i64,
title: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItIdPagedEntry {
#[pagin]
id: Id,
title: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItAutoFilledEntry {
id: Id,
#[pagin]
#[fill(now)]
created_at: AutoFill,
title: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
#[table_as(ItAliasedPost)]
struct ItAliasedPostBase {
id: Id,
#[unique]
slug: String,
title: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItAliasedForeignParent {
id: Id,
#[foreign]
featured: Option<ItAliasedPostBase>,
#[foreign]
nested: Option<Vec<Vec<ItAliasedPostBase>>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct StoredAliasedForeignParentRow {
id: Id,
featured: Option<RecordId>,
nested: Option<Vec<Vec<RecordId>>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItAliasedNestedAuthor {
id: Id,
#[unique]
handle: String,
display_name: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItAliasedNestedPost {
id: Id,
#[unique]
slug: String,
headline: String,
#[foreign]
author: ItAliasedNestedAuthor,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
#[table_as(ItAliasedNestedPost)]
struct ItAliasedNestedPostAlias {
id: Id,
#[unique]
slug: String,
headline: String,
#[foreign]
author: ItAliasedNestedAuthor,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItAliasedNestedHolder {
id: Id,
#[foreign]
featured: ItAliasedNestedPostAlias,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct StoredAliasedNestedPostRow {
id: Id,
slug: String,
headline: String,
#[serde(deserialize_with = "appdb::serde_utils::id::deserialize_record_id_or_compat_string")]
author: RecordId,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct StoredAliasedNestedHolderRow {
id: Id,
#[serde(deserialize_with = "appdb::serde_utils::id::deserialize_record_id_or_compat_string")]
featured: RecordId,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItNestedForeignLeaf {
id: Id,
#[unique]
code: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItNestedForeignBranch {
id: Id,
#[foreign]
leaf: ItNestedForeignLeaf,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItNestedForeignRoot {
id: Id,
#[foreign]
branch: ItNestedForeignBranch,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct StoredNestedForeignRootRow {
id: Id,
#[serde(deserialize_with = "appdb::serde_utils::id::deserialize_record_id_or_compat_string")]
branch: RecordId,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct StoredNestedForeignBranchRow {
id: Id,
#[serde(deserialize_with = "appdb::serde_utils::id::deserialize_record_id_or_compat_string")]
leaf: RecordId,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItInlineChild {
id: Id,
name: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItInlineParent {
id: Id,
child: ItInlineChild,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct StoredInlineParentRow {
id: RecordId,
child: ItInlineChild,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItNestedIdChild {
id: Id,
#[unique]
name: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItNestedLookupChild {
#[unique]
code: String,
note: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItNestedParent {
id: Id,
#[foreign]
child: ItNestedIdChild,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct ItMismatchedForeignChild {
id: Id,
resolved_id: Id,
name: String,
}
impl StoredModel for ItMismatchedForeignChild {
type Stored = Self;
fn into_stored(self) -> anyhow::Result<Self::Stored> {
Ok(self)
}
fn from_stored(stored: Self::Stored) -> anyhow::Result<Self> {
Ok(stored)
}
}
impl ModelMeta for ItMismatchedForeignChild {
fn table_name() -> &'static str {
static TABLE_NAME: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new();
TABLE_NAME.get_or_init(|| {
register_table(
stringify!(ItMismatchedForeignChild),
"it_mismatched_foreign_child",
)
})
}
}
impl Crud for ItMismatchedForeignChild {}
impl appdb::ForeignModel for ItMismatchedForeignChild {
async fn persist_foreign(value: Self) -> anyhow::Result<Self::Stored> {
Ok(value)
}
async fn hydrate_foreign(stored: Self::Stored) -> anyhow::Result<Self> {
Ok(stored)
}
}
#[async_trait::async_trait]
impl ResolveRecordId for ItMismatchedForeignChild {
async fn resolve_record_id(&self) -> anyhow::Result<RecordId> {
Ok(RecordId::new(
Self::table_name(),
self.resolved_id.to_string(),
))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItSchemalessForeignChild {
id: Id,
name: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItSchemalessForeignParent {
id: Id,
#[foreign]
child: ItSchemalessForeignChild,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItFreshSaveParent {
id: Id,
name: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItNestedOptionalParent {
id: Id,
#[foreign]
child: Option<ItNestedLookupChild>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct StoredNestedParentRow {
id: Id,
child: RecordId,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct StoredSchemalessForeignParentRow {
id: Id,
child: RecordId,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct StoredNestedOptionalParentRow {
id: Id,
child: Option<RecordId>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItNestedVecParent {
id: Id,
#[foreign]
children: Vec<ItNestedLookupChild>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct StoredNestedVecParentRow {
id: Id,
children: Vec<RecordId>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItRecursiveForeignParent {
id: Id,
#[foreign]
children: Option<Vec<Vec<ItNestedLookupChild>>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct StoredRecursiveForeignParentRow {
id: Id,
children: Option<Vec<Vec<RecordId>>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItManualForeignAlphaChild {
id: Id,
#[unique]
name: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItManualForeignBetaChild {
#[unique]
code: String,
note: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Bridge)]
enum ItManualForeignChild {
Alpha(ItManualForeignAlphaChild),
Beta(ItManualForeignBetaChild),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItManualForeignParent {
id: Id,
#[foreign]
child: ItManualForeignChild,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct StoredManualForeignParentRow {
id: Id,
child: RecordId,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue, Store)]
struct ItCompositeUnique {
id: Id,
#[unique]
name: String,
#[unique]
locale: String,
note: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItFallbackLookup {
name: String,
note: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue, Store)]
struct ItLookupSource {
#[unique]
name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue, Store)]
struct ItLookupTarget {
#[unique]
code: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue, Store)]
struct ItForeignOnlyLookup {
#[foreign]
child: ItLookupTarget,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue, Store)]
struct ItForeignUniqueLookup {
id: Id,
#[foreign]
#[unique]
child: ItLookupTarget,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store, Sensitive)]
struct ItSensitiveProfile {
id: Id,
alias: String,
#[secure]
secret: String,
#[secure]
note: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store, Sensitive)]
#[crypto(
service = "integration-type-override",
account = "integration-type-master"
)]
struct ItSensitiveOverrideProfile {
id: Id,
alias: String,
#[secure]
secret: String,
#[secure]
#[crypto(field_account = "integration-field-note")]
note: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store, Sensitive)]
struct ItSensitiveFallbackLookup {
alias: String,
#[secure]
secret: String,
note: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store, Sensitive)]
struct ItSensitiveLookupSource {
#[unique]
alias: String,
#[secure]
secret: String,
note: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store, Sensitive)]
struct ItSensitiveLookupTarget {
#[unique]
code: String,
#[secure]
secret: String,
note: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct StoredSensitiveProfileRow {
id: RecordId,
alias: String,
secret: Vec<u8>,
note: Option<Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct StoredSensitiveOverrideProfileRow {
id: RecordId,
alias: String,
secret: Vec<u8>,
note: Option<Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Sensitive)]
struct ItNestedSensitiveChild {
label: String,
#[secure]
secret: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store, Sensitive)]
struct ItNestedSensitiveParent {
id: Id,
alias: String,
#[secure]
child: ItNestedSensitiveChild,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store, Sensitive)]
struct ItOptionalNestedSensitiveParent {
id: Id,
alias: String,
#[secure]
child: Option<ItNestedSensitiveChild>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store, Sensitive)]
struct ItVecNestedSensitiveParent {
id: Id,
alias: String,
#[secure]
children: Vec<ItNestedSensitiveChild>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Sensitive)]
struct ItNestedOverrideLeaf {
label: String,
#[secure]
secret: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store, Sensitive)]
#[crypto(service = "integration-nested", account = "integration-nested-master")]
struct ItNestedOverrideParent {
id: Id,
alias: String,
#[secure]
#[crypto(field_account = "integration-nested-left")]
left: ItNestedOverrideLeaf,
#[secure]
#[crypto(field_account = "integration-nested-right")]
right: ItNestedOverrideLeaf,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue)]
struct StoredNestedSensitiveParentRow {
id: RecordId,
alias: String,
child: EncryptedItNestedSensitiveChild,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue)]
struct StoredOptionalNestedSensitiveParentRow {
id: RecordId,
alias: String,
child: Option<EncryptedItNestedSensitiveChild>,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue)]
struct StoredNestedOverrideParentRow {
id: RecordId,
alias: String,
left: EncryptedItNestedOverrideLeaf,
right: EncryptedItNestedOverrideLeaf,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue)]
struct StoredVecNestedSensitiveParentRow {
id: RecordId,
alias: String,
children: Vec<EncryptedItNestedSensitiveChild>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
enum ItPlainStatus {
Draft { kind: String },
Published { kind: String },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
enum ItPayloadState {
Draft {
kind: String,
note: String,
},
Published {
kind: String,
version: u32,
tags: Vec<String>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
enum ItSensitiveRuntimeSeamStatus {
Draft,
Published,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
enum ItSensitiveRuntimeSeamState {
Draft { note: String },
Published { version: u32, tags: Vec<String> },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct ItSensitiveRuntimeSeamPayload {
alias: String,
status: ItSensitiveRuntimeSeamStatus,
optional_status: Option<ItSensitiveRuntimeSeamStatus>,
state: ItSensitiveRuntimeSeamState,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store, Sensitive)]
struct ItSensitiveRuntimeSeamParent {
id: Id,
alias: String,
#[secure]
payload: SensitiveValueOf<ItSensitiveRuntimeSeamPayload>,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue)]
struct StoredSensitiveRuntimeSeamParentRow {
id: RecordId,
alias: String,
payload: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue)]
struct StoredPlainEnumProfileRow {
id: RecordId,
name: String,
status: serde_json::Value,
optional_status: Option<serde_json::Value>,
state: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItPlainEnumProfile {
id: Id,
name: String,
status: ItPlainStatus,
optional_status: Option<ItPlainStatus>,
state: ItPayloadState,
}
impl ModelMeta for ItRecordUser {
fn table_name() -> &'static str {
static TABLE_NAME: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new();
TABLE_NAME.get_or_init(|| register_table(stringify!(ItRecordUser), "it_record_user"))
}
}
impl Crud for ItRecordUser {}
impl StoredModel for ItRecordUser {
type Stored = Self;
fn into_stored(self) -> anyhow::Result<Self::Stored> {
Ok(self)
}
fn from_stored(stored: Self::Stored) -> anyhow::Result<Self> {
Ok(stored)
}
}
impl appdb::ForeignModel for ItRecordUser {
async fn persist_foreign(value: Self) -> anyhow::Result<Self::Stored> {
Ok(value)
}
async fn hydrate_foreign(stored: Self::Stored) -> anyhow::Result<Self> {
Ok(stored)
}
}
impl HasId for ItRecordUser {
fn id(&self) -> RecordId {
self.id.clone()
}
}
#[async_trait::async_trait]
impl appdb::model::meta::ResolveRecordId for ItRecordUser {
async fn resolve_record_id(&self) -> anyhow::Result<RecordId> {
Ok(self.id())
}
}
#[derive(Debug, Clone, Copy, Relation)]
#[relation(name = "it_follows_rel")]
struct ItFollowsRel;
#[derive(Debug, Clone, Copy, Relation)]
struct AutoNamedTestRelation;
#[derive(Debug, Clone, Relation)]
#[allow(dead_code)]
struct NamedFieldTestRelation {
created_at: i64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItRelateLeaf {
id: Id,
label: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItRelateRoot {
id: Id,
title: String,
#[relate("it_relate_primary")]
primary: ItRelateLeaf,
#[relate("it_relate_optional")]
optional: Option<ItRelateLeaf>,
#[relate("it_relate_many")]
items: Vec<ItRelateLeaf>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItBackRelateLeaf {
id: Id,
label: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItBackRelateRoot {
id: Id,
title: String,
#[back_relate("it_back_relate_primary")]
primary: ItBackRelateLeaf,
#[back_relate("it_back_relate_optional")]
optional: Option<ItBackRelateLeaf>,
#[back_relate("it_back_relate_many")]
items: Vec<ItBackRelateLeaf>,
#[back_relate("it_back_relate_optional_many")]
maybe_items: Option<Vec<ItBackRelateLeaf>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct StoredRelateEdgeRow {
source: RecordId,
#[serde(deserialize_with = "appdb::serde_utils::id::deserialize_record_id_or_compat_string")]
out: RecordId,
position: i64,
}
fn test_db_path() -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock before epoch")
.as_nanos();
std::env::temp_dir().join(format!("appdb_it_{}_{}", std::process::id(), nanos))
}
fn run_async<T>(fut: impl std::future::Future<Output = T>) -> T {
TEST_RT.block_on(fut)
}
fn acquire_test_lock() -> std::sync::MutexGuard<'static, ()> {
TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
async fn ensure_db() {
reinit_db(test_db_path())
.await
.expect("database should initialize");
}
async fn load_sensitive_profile_raw(id: &str) -> StoredSensitiveProfileRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItSensitiveProfile::table_name())
.bind("id", id.to_owned());
query_bound_return::<StoredSensitiveProfileRow>(stmt)
.await
.expect("raw row query should succeed")
.expect("raw row should exist")
}
async fn load_sensitive_override_profile_raw(id: &str) -> StoredSensitiveOverrideProfileRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::table($table) WHERE id = $id LIMIT 1;")
.bind("table", ItSensitiveOverrideProfile::table_name())
.bind(
"id",
RecordId::new(ItSensitiveOverrideProfile::table_name(), id),
);
query_bound_return::<StoredSensitiveOverrideProfileRow>(stmt)
.await
.expect("sensitive override profile row should load")
.expect("sensitive override profile row should exist")
}
async fn load_sensitive_runtime_seam_parent_raw(id: &str) -> StoredSensitiveRuntimeSeamParentRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItSensitiveRuntimeSeamParent::table_name())
.bind("id", id.to_owned());
query_bound_return::<StoredSensitiveRuntimeSeamParentRow>(stmt)
.await
.expect("sensitive runtime seam parent raw row should load")
.expect("sensitive runtime seam parent raw row should exist")
}
async fn load_plain_enum_profile_raw(id: &str) -> StoredPlainEnumProfileRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItPlainEnumProfile::table_name())
.bind("id", id.to_owned());
query_bound_return::<StoredPlainEnumProfileRow>(stmt)
.await
.expect("plain enum profile raw row should load")
.expect("plain enum profile raw row should exist")
}
async fn load_nested_sensitive_parent_raw(id: &str) -> StoredNestedSensitiveParentRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItNestedSensitiveParent::table_name())
.bind("id", id.to_owned());
query_bound_return::<StoredNestedSensitiveParentRow>(stmt)
.await
.expect("nested sensitive parent raw row should load")
.expect("nested sensitive parent raw row should exist")
}
async fn load_optional_nested_sensitive_parent_raw(
id: &str,
) -> StoredOptionalNestedSensitiveParentRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItOptionalNestedSensitiveParent::table_name())
.bind("id", id.to_owned());
query_bound_return::<StoredOptionalNestedSensitiveParentRow>(stmt)
.await
.expect("optional nested sensitive parent raw row should load")
.expect("optional nested sensitive parent raw row should exist")
}
async fn load_vec_nested_sensitive_parent_raw(id: &str) -> StoredVecNestedSensitiveParentRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItVecNestedSensitiveParent::table_name())
.bind("id", id.to_owned());
query_bound_return::<StoredVecNestedSensitiveParentRow>(stmt)
.await
.expect("vec nested sensitive parent raw row should load")
.expect("vec nested sensitive parent raw row should exist")
}
async fn load_nested_override_parent_raw(id: &str) -> StoredNestedOverrideParentRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItNestedOverrideParent::table_name())
.bind("id", id.to_owned());
query_bound_return::<StoredNestedOverrideParentRow>(stmt)
.await
.expect("nested override parent raw row should load")
.expect("nested override parent raw row should exist")
}
async fn load_aliased_foreign_parent_raw(id: &str) -> StoredAliasedForeignParentRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItAliasedForeignParent::table_name())
.bind("id", id.to_owned());
let value = query_bound_return::<serde_json::Value>(stmt)
.await
.expect("aliased foreign raw parent query should succeed")
.expect("aliased foreign raw parent should exist");
let mut map = value
.as_object()
.cloned()
.expect("aliased foreign raw row should be an object");
let featured = map
.remove("featured")
.map(serde_json::from_value::<Option<RecordId>>)
.transpose()
.expect("aliased foreign featured should decode as optional record id")
.flatten();
let nested = map
.remove("nested")
.map(serde_json::from_value::<Option<Vec<Vec<RecordId>>>>)
.transpose()
.expect("aliased foreign nested should decode as nested optional record ids")
.flatten();
StoredAliasedForeignParentRow {
id: Id::from(id),
featured,
nested,
}
}
async fn load_aliased_nested_post_raw(id: &str) -> StoredAliasedNestedPostRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItAliasedNestedPost::table_name())
.bind("id", id.to_owned());
let mut value = query_bound_return::<serde_json::Value>(stmt)
.await
.expect("aliased nested post raw query should succeed")
.expect("aliased nested post raw row should exist");
appdb::decode_record_link_value(&mut value);
serde_json::from_value(value).expect("aliased nested post raw row should decode")
}
async fn load_aliased_nested_holder_raw(id: &str) -> StoredAliasedNestedHolderRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItAliasedNestedHolder::table_name())
.bind("id", id.to_owned());
let value = query_bound_return::<serde_json::Value>(stmt)
.await
.expect("aliased nested holder raw query should succeed")
.expect("aliased nested holder raw row should exist");
let mut value = value;
appdb::decode_record_link_value(&mut value);
serde_json::from_value(value).expect("aliased nested holder raw row should decode")
}
async fn load_inline_parent_raw(id: &str) -> StoredInlineParentRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItInlineParent::table_name())
.bind("id", id.to_owned());
query_bound_return::<StoredInlineParentRow>(stmt)
.await
.expect("inline parent raw row query should succeed")
.expect("inline parent raw row should exist")
}
async fn load_nested_parent_raw(id: &str) -> StoredNestedParentRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItNestedParent::table_name())
.bind("id", id.to_owned());
let value = query_bound_return::<serde_json::Value>(stmt)
.await
.expect("nested parent raw row query should succeed")
.expect("nested parent raw row should exist");
let id = value
.get("id")
.and_then(serde_json::Value::as_str)
.and_then(|raw| {
raw.split_once(':')
.map(|(_, key)| key.trim_matches('`').to_owned())
})
.expect("nested parent raw row should contain normalized string id");
let child = value
.get("child")
.cloned()
.map(|value| parse_record_id_value(value, "nested parent child"))
.expect("nested parent raw row should contain child");
StoredNestedParentRow {
id: Id::from(id),
child,
}
}
async fn load_schemaless_foreign_parent_raw(id: &str) -> StoredSchemalessForeignParentRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItSchemalessForeignParent::table_name())
.bind("id", id.to_owned());
let value = query_bound_return::<serde_json::Value>(stmt)
.await
.expect("schemaless foreign parent raw row query should succeed")
.expect("schemaless foreign parent raw row should exist");
let id = value
.get("id")
.and_then(serde_json::Value::as_str)
.and_then(|raw| {
raw.split_once(':')
.map(|(_, key)| key.trim_matches('`').to_owned())
})
.expect("schemaless foreign parent raw row should contain normalized string id");
let child = value
.get("child")
.cloned()
.map(|value| parse_record_id_value(value, "schemaless foreign parent child"))
.expect("schemaless foreign parent raw row should contain child");
StoredSchemalessForeignParentRow {
id: Id::from(id),
child,
}
}
async fn load_nested_optional_parent_raw(id: &str) -> StoredNestedOptionalParentRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItNestedOptionalParent::table_name())
.bind("id", id.to_owned());
let value = query_bound_return::<serde_json::Value>(stmt)
.await
.expect("nested optional parent raw row query should succeed")
.expect("nested optional parent raw row should exist");
let id = value
.get("id")
.and_then(serde_json::Value::as_str)
.and_then(|raw| {
raw.split_once(':')
.map(|(_, key)| key.trim_matches('`').to_owned())
})
.expect("nested optional parent raw row should contain normalized string id");
let child = match value.get("child").cloned() {
Some(serde_json::Value::Null) | None => None,
Some(other) => Some(parse_record_id_value(other, "nested optional parent child")),
};
StoredNestedOptionalParentRow {
id: Id::from(id),
child,
}
}
async fn load_nested_vec_parent_raw(id: &str) -> StoredNestedVecParentRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItNestedVecParent::table_name())
.bind("id", id.to_owned());
let value = query_bound_return::<serde_json::Value>(stmt)
.await
.expect("nested vec parent raw row query should succeed")
.expect("nested vec parent raw row should exist");
let id = value
.get("id")
.and_then(serde_json::Value::as_str)
.and_then(|raw| {
raw.split_once(':')
.map(|(_, key)| key.trim_matches('`').to_owned())
})
.expect("nested vec parent raw row should contain normalized string id");
let children = value
.get("children")
.and_then(serde_json::Value::as_array)
.expect("nested vec parent raw row should contain children")
.iter()
.cloned()
.map(|value| parse_record_id_value(value, "nested vec child"))
.collect::<Vec<_>>();
StoredNestedVecParentRow {
id: Id::from(id),
children,
}
}
async fn load_recursive_foreign_parent_raw(id: &str) -> StoredRecursiveForeignParentRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItRecursiveForeignParent::table_name())
.bind("id", id.to_owned());
let value = query_bound_return::<serde_json::Value>(stmt)
.await
.expect("recursive foreign parent raw row query should succeed")
.expect("recursive foreign parent raw row should exist");
let id = value
.get("id")
.and_then(serde_json::Value::as_str)
.and_then(|raw| {
raw.split_once(':')
.map(|(_, key)| key.trim_matches('`').to_owned())
})
.expect("recursive foreign parent raw row should contain normalized string id");
let children = value.get("children").and_then(|value| match value {
serde_json::Value::Null => None,
serde_json::Value::Array(rows) => Some(
rows.iter()
.map(|row| {
row.as_array()
.expect("recursive foreign child row should be an array")
.iter()
.cloned()
.map(|value| parse_record_id_value(value, "recursive foreign child"))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>(),
),
_ => panic!("recursive foreign children should be null or nested arrays"),
});
StoredRecursiveForeignParentRow {
id: Id::from(id),
children,
}
}
async fn load_manual_foreign_parent_raw(id: &str) -> StoredManualForeignParentRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItManualForeignParent::table_name())
.bind("id", id.to_owned());
let value = query_bound_return::<serde_json::Value>(stmt)
.await
.expect("manual foreign parent raw row query should succeed")
.expect("manual foreign parent raw row should exist");
let id = value
.get("id")
.and_then(serde_json::Value::as_str)
.and_then(|raw| {
raw.split_once(':')
.map(|(_, key)| key.trim_matches('`').to_owned())
})
.expect("manual foreign parent raw row should contain normalized string id");
let child = value
.get("child")
.cloned()
.map(|value| parse_record_id_value(value, "manual foreign child"))
.expect("manual foreign parent raw row should contain child");
StoredManualForeignParentRow {
id: Id::from(id),
child,
}
}
async fn load_nested_foreign_root_raw(id: &str) -> StoredNestedForeignRootRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItNestedForeignRoot::table_name())
.bind("id", id.to_owned());
let value = query_bound_return::<serde_json::Value>(stmt)
.await
.expect("nested foreign root raw row query should succeed")
.expect("nested foreign root raw row should exist");
let id = value
.get("id")
.and_then(serde_json::Value::as_str)
.and_then(|raw| {
raw.split_once(':')
.map(|(_, key)| key.trim_matches('`').to_owned())
})
.expect("nested foreign root raw row should contain normalized string id");
let branch = value
.get("branch")
.cloned()
.map(|value| parse_record_id_value(value, "nested foreign root branch"))
.expect("nested foreign root raw row should contain branch");
StoredNestedForeignRootRow {
id: Id::from(id),
branch,
}
}
async fn load_nested_foreign_branch_raw(id: &str) -> StoredNestedForeignBranchRow {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItNestedForeignBranch::table_name())
.bind("id", id.to_owned());
let value = query_bound_return::<serde_json::Value>(stmt)
.await
.expect("nested foreign branch raw row query should succeed")
.expect("nested foreign branch raw row should exist");
let id = value
.get("id")
.and_then(serde_json::Value::as_str)
.and_then(|raw| {
raw.split_once(':')
.map(|(_, key)| key.trim_matches('`').to_owned())
})
.expect("nested foreign branch raw row should contain normalized string id");
let leaf = value
.get("leaf")
.cloned()
.map(|value| parse_record_id_value(value, "nested foreign branch leaf"))
.expect("nested foreign branch raw row should contain leaf");
StoredNestedForeignBranchRow {
id: Id::from(id),
leaf,
}
}
async fn seed_nested_foreign_root_pair(
other: ItNestedForeignRoot,
target: ItNestedForeignRoot,
) -> ItNestedForeignRoot {
ItNestedForeignRoot::save(other)
.await
.expect("seed save should succeed");
ItNestedForeignRoot::save(target.clone())
.await
.expect("target save should succeed")
}
async fn ensure_tables_exist(table_names: &[&str]) {
let db = get_db().expect("database should be initialized");
for table in table_names {
db.query(format!("DEFINE TABLE IF NOT EXISTS {table} SCHEMALESS;"))
.await
.expect("table define should succeed")
.check()
.expect("table define response should be valid");
}
}
fn assert_sensitive_row_encrypted(
raw: &StoredSensitiveProfileRow,
expected_alias: &str,
expected_secret: &str,
expected_note: Option<&str>,
) {
assert_eq!(raw.alias, expected_alias);
assert_ne!(raw.secret, expected_secret.as_bytes());
assert!(raw.secret.len() > expected_secret.len());
match (&raw.note, expected_note) {
(Some(ciphertext), Some(plaintext)) => {
assert_ne!(ciphertext, plaintext.as_bytes());
assert!(ciphertext.len() > plaintext.len());
}
(None, None) => {}
other => panic!("unexpected note state: {other:?}"),
}
}
fn assert_sensitive_override_row_encrypted(
raw: &StoredSensitiveOverrideProfileRow,
alias: &str,
secret: &str,
note: Option<&str>,
) {
assert_eq!(raw.alias, alias);
assert_ne!(raw.secret, secret.as_bytes());
match (&raw.note, note) {
(Some(ciphertext), Some(expected)) => assert_ne!(ciphertext, expected.as_bytes()),
(None, None) => {}
other => panic!("unexpected sensitive override note shape: {other:?}"),
}
}
fn assert_nested_sensitive_child_encrypted(
raw: &EncryptedItNestedSensitiveChild,
expected_label: &str,
expected_secret: &str,
) {
assert_eq!(raw.label, expected_label);
assert_ne!(raw.secret, expected_secret.as_bytes());
assert!(raw.secret.len() > expected_secret.len());
}
fn parse_record_id_value(value: serde_json::Value, context: &str) -> RecordId {
match serde_json::from_value::<RecordId>(value.clone()) {
Ok(record) => record,
Err(_) => {
let text = value
.as_str()
.unwrap_or_else(|| panic!("{context} should be a record-id-compatible string"));
let (table, key) = text
.split_once(':')
.unwrap_or_else(|| panic!("{context} should be a table:key record link"));
RecordId::new(table, key.trim_matches('`'))
}
}
}
async fn load_relate_root_raw(id: &str) -> serde_json::Value {
let stmt = RawSqlStmt::new("SELECT * FROM ONLY type::record($table, $id);")
.bind("table", ItRelateRoot::table_name())
.bind("id", id.to_owned());
query_bound_return::<serde_json::Value>(stmt)
.await
.expect("relate root raw query should succeed")
.expect("relate root raw row should exist")
}
async fn load_back_relate_root_raw(id: &str) -> serde_json::Value {
let stmt = RawSqlStmt::new("SELECT * FROM ONLY type::record($table, $id);")
.bind("table", ItBackRelateRoot::table_name())
.bind("id", id.to_owned());
query_bound_return::<serde_json::Value>(stmt)
.await
.expect("back relate root raw query should succeed")
.expect("back relate root raw row should exist")
}
async fn load_relate_edges(rel: &str, id: &str) -> Vec<StoredRelateEdgeRow> {
let stmt = RawSqlStmt::new(
"SELECT `in` AS source, out, position FROM $rel WHERE in = $record ORDER BY position ASC;",
)
.bind("rel", Table::from(rel))
.bind("record", RecordId::new(ItRelateRoot::table_name(), id));
let mut result = query_bound_checked(stmt)
.await
.expect("relate edge query should succeed");
result.take(0).expect("relate edge rows should decode")
}
async fn load_back_relate_edges(rel: &str, id: &str) -> Vec<StoredRelateEdgeRow> {
let stmt = RawSqlStmt::new(
"SELECT `in` AS source, out, position FROM $rel WHERE out = $record ORDER BY position ASC;",
)
.bind("rel", Table::from(rel))
.bind("record", RecordId::new(ItBackRelateRoot::table_name(), id));
let mut result = query_bound_checked(stmt)
.await
.expect("back relate edge query should succeed");
result.take(0).expect("back relate edge rows should decode")
}
#[test]
fn id_repo_roundtrip_passes() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItStringUser>::delete_all()
.await
.expect("delete_all should succeed");
let inserted = Repo::<ItStringUser>::save(ItStringUser {
id: Id::from("alice"),
payload: String::new(),
})
.await
.expect("save should succeed");
assert_eq!(inserted.id, Id::from("alice"));
let selected = Repo::<ItStringUser>::get("alice")
.await
.expect("get should succeed");
assert_eq!(selected.id, Id::from("alice"));
});
}
#[test]
fn inherent_model_api_roundtrip_passes() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
ItStringUser::delete_all()
.await
.expect("delete_all should succeed");
let inserted = Repo::<ItStringUser>::save(ItStringUser {
id: Id::from("alice"),
payload: String::new(),
})
.await
.expect("save should succeed");
let loaded = ItStringUser::get("alice")
.await
.expect("get should succeed");
assert_eq!(loaded.id, Id::from("alice"));
let listed = ItStringUser::list().await.expect("list should succeed");
assert_eq!(listed.len(), 1);
let ids = ItStringUser::list_record_ids()
.await
.expect("list_record_ids should succeed");
assert_eq!(ids.len(), 1);
let record = ItStringUser::list_record_ids()
.await
.expect("list_record_ids should succeed")
.into_iter()
.next()
.expect("one record id should exist");
assert_eq!(record.table, Table::from("it_string_user"));
ItStringUser::delete("alice")
.await
.expect("delete should succeed");
let err = ItStringUser::get("alice")
.await
.expect_err("deleted record should not load");
let typed = DBError::from(err);
assert_eq!(typed.kind(), DBErrorKind::NotFound);
drop(inserted);
});
}
#[test]
fn model_exists_reports_whether_table_has_any_rows() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
assert!(
!ItStringUser::exists()
.await
.expect("exists on an empty table should succeed"),
"empty table should report false"
);
Repo::<ItStringUser>::save(ItStringUser {
id: Id::from("seed"),
payload: "value".to_owned(),
})
.await
.expect("seed save should succeed");
assert!(
ItStringUser::exists()
.await
.expect("exists on a populated table should succeed"),
"non-empty table should report true"
);
ItStringUser::delete_all()
.await
.expect("delete_all should succeed");
assert!(
!ItStringUser::exists()
.await
.expect("exists after delete_all should succeed"),
"table cleared by delete_all should report false"
);
});
}
#[test]
fn select_missing_record_fails() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
let _ = Repo::<ItStringUser>::save(ItStringUser {
id: Id::from("seed"),
payload: String::new(),
})
.await
.expect("seed save should succeed");
let err = Repo::<ItStringUser>::get("missing")
.await
.expect_err("missing record should fail");
let typed = DBError::from(err);
assert_eq!(typed.kind(), DBErrorKind::NotFound, "{typed}");
});
}
#[test]
fn exists_record_missing_table_uses_typed_classification() {
let _guard = acquire_test_lock();
run_async(async {
reinit_db_with_options(test_db_path(), InitDbOptions::default())
.await
.expect("schemaless database should initialize");
let exists = Repo::<ItStringUser>::exists_record(RecordId::new(
ItStringUser::table_name(),
"typed-missing-table",
))
.await
.expect("missing table should classify as typed missing-table and return false");
assert!(!exists);
});
}
#[test]
fn create_at_duplicate_id_returns_typed_conflict() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItStringUser>::delete_all()
.await
.expect("delete_all should succeed");
let original = Repo::<ItStringUser>::create_at(
RecordId::new(ItStringUser::table_name(), "typed-conflict"),
ItStringUser {
id: Id::from("typed-conflict"),
payload: "first".to_owned(),
},
)
.await
.expect("initial create_at should succeed");
let err = Repo::<ItStringUser>::create_at(
RecordId::new(ItStringUser::table_name(), "typed-conflict"),
ItStringUser {
id: Id::from("typed-conflict"),
payload: "second".to_owned(),
},
)
.await
.expect_err("duplicate create_at should return a typed conflict");
let typed = DBError::from(err);
assert_eq!(typed.kind(), DBErrorKind::Conflict);
let loaded = Repo::<ItStringUser>::get("typed-conflict")
.await
.expect("original row should remain unchanged after duplicate create_at");
assert_eq!(original.payload, "first");
assert_eq!(loaded.payload, "first");
});
}
#[test]
fn save_upsert_at_create_at_explicit_id_semantics_are_intentional() {
let _guard = acquire_test_lock();
run_async(async {
reinit_db_with_options(test_db_path(), InitDbOptions::default())
.await
.expect("schemaless database should initialize");
let created = Repo::<ItStringUser>::create_at(
RecordId::new(ItStringUser::table_name(), "explicit-id-semantics"),
ItStringUser {
id: Id::from("explicit-id-semantics"),
payload: "created".to_owned(),
},
)
.await
.expect("create_at should create the addressed row");
assert_eq!(created.payload, "created");
let saved = Repo::<ItStringUser>::save(ItStringUser {
id: Id::from("explicit-id-semantics"),
payload: "saved".to_owned(),
})
.await
.expect("save should ensure/update the same addressed row");
assert_eq!(saved.payload, "saved");
let upserted = Repo::<ItStringUser>::upsert_at(
RecordId::new(ItStringUser::table_name(), "explicit-id-semantics"),
ItStringUser {
id: Id::from("explicit-id-semantics"),
payload: "upserted".to_owned(),
},
)
.await
.expect("upsert_at should ensure/update the same addressed row");
assert_eq!(upserted.payload, "upserted");
let loaded = Repo::<ItStringUser>::get("explicit-id-semantics")
.await
.expect("final explicit-id row should be loadable");
let ids = Repo::<ItStringUser>::list_record_ids()
.await
.expect("list_record_ids should succeed");
assert_eq!(loaded.payload, "upserted");
assert_eq!(
ids,
vec![RecordId::new(
ItStringUser::table_name(),
"explicit-id-semantics"
)]
);
});
}
#[test]
fn create_at_preserves_record_id_table_identity_for_record_id_models() {
let _guard = acquire_test_lock();
run_async(async {
reinit_db_with_options(test_db_path(), InitDbOptions::default())
.await
.expect("schemaless database should initialize");
let created = Repo::<ItRecordUser>::create_at(
RecordId::new("it_record_user", "create-return-record-id"),
ItRecordUser {
id: RecordId::new("it_record_user", "create-return-record-id"),
name: "created".to_owned(),
},
)
.await
.expect("create_at should preserve the full record id for RecordId-backed models");
assert_eq!(
created.id,
RecordId::new("it_record_user", "create-return-record-id")
);
assert_eq!(created.name, "created");
});
}
#[test]
fn upsert_at_preserves_record_id_table_identity_for_record_id_models() {
let _guard = acquire_test_lock();
run_async(async {
reinit_db_with_options(test_db_path(), InitDbOptions::default())
.await
.expect("schemaless database should initialize");
let upserted = Repo::<ItRecordUser>::upsert_at(
RecordId::new("it_record_user", "upsert-return-record-id"),
ItRecordUser {
id: RecordId::new("it_record_user", "upsert-return-record-id"),
name: "upserted".to_owned(),
},
)
.await
.expect("upsert_at should preserve the full record id for RecordId-backed models");
assert_eq!(
upserted.id,
RecordId::new("it_record_user", "upsert-return-record-id")
);
assert_eq!(upserted.name, "upserted");
});
}
#[test]
fn decode_raw_row_invalid_shape_returns_typed_decode_error() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
let stmt = RawSqlStmt::new("RETURN { id: 'typed-decode', payload: 99 }; ");
let err = query_bound_return::<ItStringUser>(stmt)
.await
.expect_err("invalid raw row shape should fail to decode");
let typed = DBError::from(err);
assert_eq!(typed.kind(), DBErrorKind::Decode);
});
}
#[test]
fn typed_db_error_classifier_distinguishes_all_validated_categories() {
let missing_table = classify_db_error_text("The table it_missing does not exist".to_owned());
assert_eq!(missing_table.kind, DBErrorKind::MissingTable);
let not_found = classify_db_error_text("Record not found".to_owned());
assert_eq!(not_found.kind, DBErrorKind::NotFound);
let conflict = classify_db_error_text("Database conflict: record already exists".to_owned());
assert_eq!(conflict.kind, DBErrorKind::Conflict);
let decode =
classify_db_error_text("failed to decode stored row: missing field `payload`".to_owned());
assert_eq!(decode.kind, DBErrorKind::Decode);
let transport = classify_db_error_text("transport connection timed out".to_owned());
assert_eq!(transport.kind, DBErrorKind::Transport);
let engine = classify_db_error_text("surreal engine exploded".to_owned());
assert_eq!(engine.kind, DBErrorKind::Engine);
}
#[test]
fn number_id_repo_roundtrip_passes() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItNumberUser>::delete_all()
.await
.expect("delete_all should succeed");
let inserted = Repo::<ItNumberUser>::save(ItNumberUser {
id: Id::from(42i64),
})
.await
.expect("save should succeed");
assert_eq!(inserted.id, Id::from(42i64));
let selected = Repo::<ItNumberUser>::list()
.await
.expect("list should succeed");
assert_eq!(selected.len(), 1);
assert_eq!(selected[0].id, Id::from(42i64));
});
}
#[test]
fn number_id_foreign_roundtrip_preserves_numeric_public_ids() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItNumberForeignParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNumberForeignChild>::delete_all()
.await
.expect("delete_all should succeed");
let saved = Repo::<ItNumberForeignParent>::save_many(vec![ItNumberForeignParent {
id: Id::from(42i64),
child: ItNumberForeignChild {
id: Id::from("child-42"),
name: "child".to_owned(),
},
}])
.await
.expect("save_many should succeed");
assert_eq!(saved.len(), 1);
assert_eq!(saved[0].id, Id::from(42i64));
let loaded = Repo::<ItNumberForeignParent>::get(42i64)
.await
.expect("get should succeed");
assert_eq!(loaded.id, Id::from(42i64));
let listed = Repo::<ItNumberForeignParent>::list()
.await
.expect("list should succeed");
assert_eq!(listed.len(), 1);
assert_eq!(listed[0].id, Id::from(42i64));
});
}
#[test]
fn db_runtime_opens_without_global_registration() {
let _guard = acquire_test_lock();
run_async(async {
let runtime = DbRuntime::open(test_db_path())
.await
.expect("runtime should open");
let db = runtime.handle();
let mut result = db
.query("RETURN 1;")
.await
.expect("query should succeed")
.check()
.expect("response should be valid");
let value: Option<i64> = result.take(0).expect("result should decode");
assert_eq!(value, Some(1));
});
}
#[test]
fn save_preserves_payload_fields() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItStringUser>::delete_all()
.await
.expect("delete_all should succeed");
let inserted = Repo::<ItStringUser>::save(ItStringUser {
id: Id::from("s1"),
payload: "hello".to_owned(),
})
.await
.expect("save should succeed");
assert_eq!(inserted.id, Id::from("s1"));
assert_eq!(inserted.payload, "hello");
let selected = Repo::<ItStringUser>::get("s1")
.await
.expect("get should succeed");
assert_eq!(selected.id, Id::from("s1"));
assert_eq!(selected.payload, "hello");
});
}
#[test]
fn ordinary_colon_strings_are_not_record_links() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
ensure_tables_exist(&[ItStringUser::table_name()]).await;
Repo::<ItStringUser>::delete_all()
.await
.expect("delete_all should succeed");
let input = ItStringUser {
id: Id::from("colon-string-user"),
payload: "alpha:beta".to_owned(),
};
let saved = ItStringUser::save(input.clone())
.await
.expect("save should preserve ordinary colon strings");
let loaded = ItStringUser::get("colon-string-user")
.await
.expect("get should preserve ordinary colon strings");
let loaded_record = Repo::<ItStringUser>::get_record(RecordId::new(
ItStringUser::table_name(),
"colon-string-user",
))
.await
.expect("get_record should preserve ordinary colon strings");
let raw: serde_json::Value = query_bound_return(RawSqlStmt::new(format!(
"SELECT payload FROM {}:`colon-string-user`;",
ItStringUser::table_name()
)))
.await
.expect("raw payload query should succeed")
.expect("raw payload row should exist");
assert_eq!(saved, input);
assert_eq!(loaded, input);
assert_eq!(loaded_record, input);
assert_eq!(raw, serde_json::json!({ "payload": "alpha:beta" }));
});
}
#[test]
fn plain_root_strings_are_not_coerced_into_record_ids_on_reload() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
ensure_tables_exist(&[ItStringUser::table_name()]).await;
Repo::<ItStringUser>::delete_all()
.await
.expect("delete_all should succeed");
let db = get_db().expect("db should be initialized");
db.query("CREATE type::record($table, $id) CONTENT { payload: $payload };")
.bind(("table", ItStringUser::table_name()))
.bind(("id", "plain-root"))
.bind(("payload", "alpha"))
.await
.expect("setup row should succeed");
let raw: serde_json::Value = query_bound_return(RawSqlStmt::new(format!(
"SELECT VALUE {{ id: payload, payload: payload }} FROM {}:`plain-root`;",
ItStringUser::table_name()
)))
.await
.expect("raw projection query should succeed")
.expect("raw projection row should exist");
let loaded: ItStringUser = serde_json::from_value(raw)
.expect("plain root strings should still decode as plain ids");
assert_eq!(loaded.id, Id::from("alpha"));
assert_eq!(loaded.payload, "alpha");
});
}
#[test]
fn decode_record_link_value_accepts_table_agnostic_unquoted_record_ids() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItRecordUser>::delete_all()
.await
.expect("delete_all should succeed");
let db = get_db().expect("db should be initialized");
db.query("CREATE type::record($table, $id) CONTENT { name: $name };")
.bind(("table", "custom_table"))
.bind(("id", 1001))
.bind(("name", "custom"))
.await
.expect("seed custom table row should succeed")
.check()
.expect("seed custom table row should be valid");
let raw: serde_json::Value = query_bound_return(
RawSqlStmt::new(
"RETURN { id: type::string(type::record($first_table, $first_id)), name: $plain }",
)
.bind("first_table", "custom_table")
.bind("first_id", 1001)
.bind("plain", "alpha:beta gamma"),
)
.await
.expect("raw record-id payload query should succeed")
.expect("raw record-id payload row should exist");
let selected = serde_json::to_string(&raw).expect("raw payload should serialize");
assert!(selected.contains("custom_table:1001"));
assert!(selected.contains("alpha:beta gamma"));
});
}
#[test]
fn decode_record_link_value_accepts_table_agnostic_unquoted_string_keys() {
let _guard = acquire_test_lock();
run_async(async {
let mut value = serde_json::json!({
"id": "custom_table:compat-read",
"name": "compat"
});
appdb::decode_record_link_value(&mut value);
let loaded: ItRecordUser = serde_json::from_value(value)
.expect("decode_record_link_value should preserve table-agnostic string keys");
assert_eq!(loaded.id, RecordId::new("custom_table", "compat-read"));
assert_eq!(loaded.name, "compat");
});
}
#[test]
fn nested_ref_opt_in_keeps_unannotated_fields_inline() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItInlineParent>::delete_all()
.await
.expect("delete_all should succeed");
let input = ItInlineParent {
id: Id::from("inline-parent"),
child: ItInlineChild {
id: Id::from("inline-child"),
name: "alpha".to_owned(),
},
};
let saved = ItInlineParent::save(input.clone())
.await
.expect("save should succeed");
let loaded = ItInlineParent::get("inline-parent")
.await
.expect("get should succeed");
let raw = load_inline_parent_raw("inline-parent").await;
assert_eq!(saved, input);
assert_eq!(loaded, input);
assert_eq!(raw.child, input.child);
});
}
#[test]
fn nested_ref_single_and_option_paths() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItNestedParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedIdChild>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedOptionalParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedIdChild>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedLookupChild>::delete_all()
.await
.expect("delete_all should succeed");
let direct_existing_input = ItNestedParent {
id: Id::from("nested-parent-id"),
child: ItNestedIdChild {
id: Id::from("nested-child-id"),
name: "alpha".to_owned(),
},
};
let direct_child_created =
Repo::<ItNestedIdChild>::create(direct_existing_input.child.clone())
.await
.expect("direct child create should succeed");
assert_eq!(
direct_child_created
.resolve_record_id()
.await
.expect("created child should resolve"),
RecordId::new(ItNestedIdChild::table_name(), "nested-child-id")
);
let direct_existing_saved = ItNestedParent::save(direct_existing_input.clone())
.await
.expect("direct nested save should succeed");
let direct_existing_loaded = ItNestedParent::get("nested-parent-id")
.await
.expect("direct nested get should succeed");
let direct_existing_raw = load_nested_parent_raw("nested-parent-id").await;
let direct_existing_child = Repo::<ItNestedIdChild>::get("nested-child-id")
.await
.expect("nested child row should exist");
assert_eq!(direct_existing_saved, direct_existing_input);
assert_eq!(direct_existing_loaded, direct_existing_input);
assert_eq!(direct_existing_child, direct_existing_input.child);
assert_eq!(
direct_existing_raw.child,
RecordId::new(ItNestedIdChild::table_name(), "nested-child-id")
);
let _seeded_lookup_child = Repo::<ItNestedLookupChild>::create(ItNestedLookupChild {
code: "lookup-existing".to_owned(),
note: Some("seeded".to_owned()),
})
.await
.expect("seeded lookup child create should succeed");
let reuse_parent = ItNestedOptionalParent {
id: Id::from("nested-option-some-existing"),
child: Some(ItNestedLookupChild {
code: "lookup-existing".to_owned(),
note: Some("seeded".to_owned()),
}),
};
let reused_saved = ItNestedOptionalParent::save(reuse_parent.clone())
.await
.expect("lookup-backed nested save should succeed");
let reused_loaded = ItNestedOptionalParent::get("nested-option-some-existing")
.await
.expect("lookup-backed nested get should succeed");
let reused_raw = load_nested_optional_parent_raw("nested-option-some-existing").await;
let lookup_ids = Repo::<ItNestedLookupChild>::list_record_ids()
.await
.expect("list_record_ids should succeed");
assert_eq!(reused_saved, reuse_parent);
assert_eq!(reused_loaded, reuse_parent);
assert_eq!(lookup_ids.len(), 1);
assert_eq!(reused_raw.child, Some(lookup_ids[0].clone()));
let create_parent = ItNestedOptionalParent {
id: Id::from("nested-option-some-create"),
child: Some(ItNestedLookupChild {
code: "lookup-create".to_owned(),
note: Some("created".to_owned()),
}),
};
let created_saved = ItNestedOptionalParent::save(create_parent.clone())
.await
.expect("missing lookup-backed nested save should create child");
let created_loaded = ItNestedOptionalParent::get("nested-option-some-create")
.await
.expect("created lookup-backed nested get should succeed");
let created_raw = load_nested_optional_parent_raw("nested-option-some-create").await;
let created_child_id = Repo::<ItNestedLookupChild>::find_unique_id_for(
create_parent
.child
.as_ref()
.expect("child should be present"),
)
.await
.expect("created lookup child should resolve");
let explicit_create_parent = ItNestedOptionalParent {
id: Id::from("nested-direct-create-through-option"),
child: Some(ItNestedLookupChild {
code: "lookup-create-explicit".to_owned(),
note: Some("created-explicit".to_owned()),
}),
};
let explicit_create_saved = ItNestedOptionalParent::save(explicit_create_parent.clone())
.await
.expect("missing optional nested child should be created");
let explicit_create_loaded =
ItNestedOptionalParent::get("nested-direct-create-through-option")
.await
.expect("created optional nested get should succeed");
let explicit_create_raw =
load_nested_optional_parent_raw("nested-direct-create-through-option").await;
let explicit_create_child_id = Repo::<ItNestedLookupChild>::find_unique_id_for(
explicit_create_parent
.child
.as_ref()
.expect("child should be present"),
)
.await
.expect("created optional nested child should resolve");
let explicit_create_child =
Repo::<ItNestedLookupChild>::get_record(explicit_create_child_id.clone())
.await
.expect("created optional nested child row should exist");
assert_eq!(explicit_create_saved, explicit_create_parent);
assert_eq!(explicit_create_loaded, explicit_create_parent);
assert_eq!(
explicit_create_child,
explicit_create_parent
.child
.clone()
.expect("child should be present")
);
assert_eq!(explicit_create_raw.child, Some(explicit_create_child_id));
assert_eq!(created_saved, create_parent);
assert_eq!(created_loaded, create_parent);
let created_child = Repo::<ItNestedLookupChild>::get_record(created_child_id.clone())
.await
.expect("created lookup child should load");
assert_eq!(
created_child,
create_parent
.child
.clone()
.expect("child should be present")
);
assert_eq!(created_raw.child, Some(created_child_id.clone()));
let none_parent = ItNestedOptionalParent {
id: Id::from("nested-option-none"),
child: None,
};
let none_saved = ItNestedOptionalParent::save(none_parent.clone())
.await
.expect("optional none nested save should succeed");
let none_loaded = ItNestedOptionalParent::get("nested-option-none")
.await
.expect("optional none nested get should succeed");
let none_raw = load_nested_optional_parent_raw("nested-option-none").await;
let child_rows_after_none = Repo::<ItNestedLookupChild>::list()
.await
.expect("lookup child list should succeed");
assert_eq!(none_saved, none_parent);
assert_eq!(none_loaded, none_parent);
assert_eq!(none_raw.child, None);
assert_eq!(child_rows_after_none.len(), 3);
});
}
#[test]
fn fresh_db_save_requires_no_manual_define_table() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
let input = ItFreshSaveParent {
id: Id::from("fresh-parent"),
name: "fresh-name".to_owned(),
};
let saved = ItFreshSaveParent::save(input.clone())
.await
.expect("fresh-db save should succeed without manual define table");
let loaded = ItFreshSaveParent::get("fresh-parent")
.await
.expect("fresh-db get should succeed after first save");
assert_eq!(saved, input);
assert_eq!(loaded, input);
});
}
#[test]
fn foreign_child_with_explicit_id_is_auto_persisted_when_missing() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItNestedParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedIdChild>::delete_all()
.await
.expect("delete_all should succeed");
let parent = ItNestedParent {
id: Id::from("explicit-id-parent"),
child: ItNestedIdChild {
id: Id::from("explicit-id-child"),
name: "created-on-save".to_owned(),
},
};
let saved = ItNestedParent::save(parent.clone())
.await
.expect("save should auto-persist missing explicit-id child");
let loaded = ItNestedParent::get("explicit-id-parent")
.await
.expect("get should return hydrated parent");
let raw = load_nested_parent_raw("explicit-id-parent").await;
let child = Repo::<ItNestedIdChild>::get("explicit-id-child")
.await
.expect("missing explicit-id child should now exist");
assert_eq!(saved, parent);
assert_eq!(loaded, parent);
assert_eq!(child, parent.child);
assert_eq!(
raw.child,
RecordId::new(ItNestedIdChild::table_name(), "explicit-id-child")
);
});
}
#[test]
fn foreign_existing_child_is_reused_without_duplication() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItNestedParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedIdChild>::delete_all()
.await
.expect("delete_all should succeed");
let child = ItNestedIdChild {
id: Id::from("reused-child"),
name: "reused-name".to_owned(),
};
let seeded = Repo::<ItNestedIdChild>::create(child.clone())
.await
.expect("seed child should succeed");
let seeded_id = seeded
.resolve_record_id()
.await
.expect("seeded child should resolve");
let parent = ItNestedParent {
id: Id::from("reused-parent"),
child: child.clone(),
};
let before_ids = Repo::<ItNestedIdChild>::list_record_ids()
.await
.expect("child ids before save should load");
let saved = ItNestedParent::save(parent.clone())
.await
.expect("save should reuse existing child");
let loaded = ItNestedParent::get("reused-parent")
.await
.expect("get should return hydrated reused parent");
let raw = load_nested_parent_raw("reused-parent").await;
let after_ids = Repo::<ItNestedIdChild>::list_record_ids()
.await
.expect("child ids after save should load");
assert_eq!(saved, parent);
assert_eq!(loaded, parent);
assert_eq!(before_ids, vec![seeded_id.clone()]);
assert_eq!(after_ids, vec![seeded_id.clone()]);
assert_eq!(raw.child, seeded_id);
});
}
#[test]
fn foreign_explicit_id_and_resolved_record_id_mismatch_fails_deterministically() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItMismatchedForeignChild>::delete_all()
.await
.expect("delete_all should succeed");
let err = appdb::resolve_foreign_record_id(ItMismatchedForeignChild {
id: Id::from("serialized-child"),
resolved_id: Id::from("resolved-child"),
name: "mismatch".to_owned(),
})
.await
.expect_err("mismatched explicit and resolved foreign ids should fail");
let db_err = appdb::error::classify_db_error(&err);
match db_err {
DBError::InvalidModel(message) => {
assert!(
message.contains("foreign explicit id mismatch"),
"unexpected invalid-model message: {message}"
);
assert!(
message.contains("serialized explicit id"),
"message should mention serialized explicit id: {message}"
);
assert!(
message.contains("resolved record id"),
"message should mention resolved record id: {message}"
);
}
other => panic!("expected invalid-model mismatch error, got {other:?}"),
}
let serialized = Repo::<ItMismatchedForeignChild>::get("serialized-child").await;
assert!(
matches!(
appdb::error::classify_db_error(
&serialized.expect_err("serialized id must remain absent")
),
DBError::NotFound | DBError::MissingTable(_)
),
"serialized id should not be persisted"
);
let resolved = Repo::<ItMismatchedForeignChild>::get("resolved-child").await;
assert!(
matches!(
appdb::error::classify_db_error(
&resolved.expect_err("resolved id must remain absent")
),
DBError::NotFound | DBError::MissingTable(_)
),
"resolved id should not be persisted"
);
});
}
#[test]
fn schemaless_foreign_save_bootstraps_store_child_tables() {
let _guard = acquire_test_lock();
run_async(async {
reinit_db_with_options(test_db_path(), InitDbOptions::default())
.await
.expect("schemaless database should initialize");
let parent = ItNestedParent {
id: Id::from("schemaless-parent"),
child: ItNestedIdChild {
id: Id::from("schemaless-child"),
name: "schemaless-created-on-save".to_owned(),
},
};
let saved = ItNestedParent::save(parent.clone())
.await
.expect("schemaless save should bootstrap parent and child store tables");
let loaded = ItNestedParent::get("schemaless-parent")
.await
.expect("schemaless get should hydrate saved parent");
let raw = load_nested_parent_raw("schemaless-parent").await;
let child = Repo::<ItNestedIdChild>::get("schemaless-child")
.await
.expect("schemaless foreign child should exist after save");
assert_eq!(saved, parent);
assert_eq!(loaded, parent);
assert_eq!(child, parent.child);
assert_eq!(
raw.child,
RecordId::new(ItNestedIdChild::table_name(), "schemaless-child")
);
});
}
#[test]
fn schemaless_foreign_save_bootstraps_child_tables_without_schema_side_effects() {
let _guard = acquire_test_lock();
run_async(async {
reinit_db_with_options(test_db_path(), InitDbOptions::default())
.await
.expect("schemaless database should initialize");
let parent = ItSchemalessForeignParent {
id: Id::from("schemaless-side-effect-free-parent"),
child: ItSchemalessForeignChild {
id: Id::from("schemaless-side-effect-free-child"),
name: "schemaless-without-unique-index".to_owned(),
},
};
let saved = ItSchemalessForeignParent::save(parent.clone())
.await
.expect("schemaless save should bootstrap foreign child tables even without schema inventory side effects");
let loaded = ItSchemalessForeignParent::get("schemaless-side-effect-free-parent")
.await
.expect("schemaless get should hydrate saved parent");
let raw = load_schemaless_foreign_parent_raw("schemaless-side-effect-free-parent").await;
let child = Repo::<ItSchemalessForeignChild>::get("schemaless-side-effect-free-child")
.await
.expect("schemaless foreign child should exist after save");
assert_eq!(saved, parent);
assert_eq!(loaded, parent);
assert_eq!(child, parent.child);
assert_eq!(
raw.child,
RecordId::new(
ItSchemalessForeignChild::table_name(),
"schemaless-side-effect-free-child"
)
);
});
}
#[test]
fn schema_managed_runtime_applies_registered_inventory() {
let _guard = acquire_test_lock();
run_async(async {
let runtime = DbRuntime::open(test_db_path())
.await
.expect("managed runtime should initialize and apply schema inventory");
runtime.reinstall_global_for_tests();
let stmt = RawSqlStmt::new(format!("INFO FOR TABLE {};", ItProfile::table_name()));
let info = query_bound_return::<serde_json::Value>(stmt)
.await
.expect("managed runtime table info query should succeed")
.expect("managed runtime table info should exist after schema inventory apply");
let indexes = info
.get("indexes")
.and_then(serde_json::Value::as_object)
.expect(
"managed runtime table info should expose indexes after schema inventory apply",
);
let unique_index = indexes
.get("it_profile_name_unique")
.and_then(serde_json::Value::as_str)
.expect("managed runtime should apply the registered unique index inventory");
assert!(
unique_index
.contains("DEFINE INDEX it_profile_name_unique ON it_profile FIELDS name UNIQUE"),
"managed runtime should report the registered unique index DDL, got: {unique_index}"
);
});
}
#[test]
fn schemaless_upsert_at_bootstraps_plain_explicit_id_table() {
let _guard = acquire_test_lock();
run_async(async {
reinit_db_with_options(test_db_path(), InitDbOptions::default())
.await
.expect("schemaless database should initialize");
let child = ItSchemalessForeignChild {
id: Id::from("schemaless-upsert-at-child"),
name: "plain-explicit-id-upsert".to_owned(),
};
let saved = Repo::<ItSchemalessForeignChild>::upsert_at(
RecordId::new(
ItSchemalessForeignChild::table_name(),
"schemaless-upsert-at-child",
),
child.clone(),
)
.await
.expect("schemaless upsert_at should bootstrap the missing table");
let loaded = Repo::<ItSchemalessForeignChild>::get("schemaless-upsert-at-child")
.await
.expect("schemaless upsert_at row should be loadable");
assert_eq!(saved, child);
assert_eq!(loaded, child);
});
}
#[test]
fn schemaless_resolve_foreign_record_id_bootstraps_plain_explicit_id_child() {
let _guard = acquire_test_lock();
run_async(async {
reinit_db_with_options(test_db_path(), InitDbOptions::default())
.await
.expect("schemaless database should initialize");
let child = ItSchemalessForeignChild {
id: Id::from("schemaless-resolve-foreign-child"),
name: "resolve-foreign-explicit-id".to_owned(),
};
let record = appdb::resolve_foreign_record_id(child.clone())
.await
.expect("resolve_foreign_record_id should bootstrap the missing child table");
let loaded = Repo::<ItSchemalessForeignChild>::get("schemaless-resolve-foreign-child")
.await
.expect("resolved foreign child should be persisted");
assert_eq!(
record,
RecordId::new(
ItSchemalessForeignChild::table_name(),
"schemaless-resolve-foreign-child"
)
);
assert_eq!(loaded, child);
});
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store)]
struct ItAtomicSaveChild {
id: Id,
#[unique]
code: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct ItAtomicSaveParentStored {
id: Id,
child: RecordId,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct ItAtomicSaveParent {
id: Id,
child: ItAtomicSaveChild,
}
static mut ATOMIC_SAVE_HYDRATE_FAIL: bool = true;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue)]
struct StoredAtomicSaveParentRow {
id: Id,
child: RecordId,
}
impl StoredModel for ItAtomicSaveParent {
type Stored = ItAtomicSaveParentStored;
fn into_stored(self) -> anyhow::Result<Self::Stored> {
anyhow::bail!("into_stored should not be used for atomic save parent tests")
}
fn from_stored(stored: Self::Stored) -> anyhow::Result<Self> {
let child_json = serde_json::to_value(&stored.child)?;
let child_id = child_json
.get("id")
.and_then(serde_json::Value::as_str)
.and_then(|raw| {
raw.split_once(':')
.map(|(_, key)| key.trim_matches('`').to_owned())
})
.unwrap_or_else(|| child_json.to_string());
Ok(Self {
id: stored.id,
child: ItAtomicSaveChild {
id: Id::from(child_id),
code: String::new(),
},
})
}
}
impl ModelMeta for ItAtomicSaveParent {
fn table_name() -> &'static str {
static TABLE_NAME: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new();
TABLE_NAME
.get_or_init(|| register_table(stringify!(ItAtomicSaveParent), "it_atomic_save_parent"))
}
}
impl Crud for ItAtomicSaveParent {}
#[allow(clippy::manual_async_fn)]
impl appdb::ForeignModel for ItAtomicSaveParent {
fn persist_foreign(
value: Self,
) -> impl std::future::Future<Output = anyhow::Result<Self::Stored>> + Send {
async move {
let child_record = appdb::resolve_foreign_record_id(value.child).await?;
Ok(ItAtomicSaveParentStored {
id: value.id,
child: child_record,
})
}
}
fn hydrate_foreign(
stored: Self::Stored,
) -> impl std::future::Future<Output = anyhow::Result<Self>> + Send {
async move {
if unsafe { ATOMIC_SAVE_HYDRATE_FAIL } {
anyhow::bail!(
"intentional atomic save hydration failure for {}",
stored.id
)
}
Ok(Self {
id: stored.id,
child: Repo::<ItAtomicSaveChild>::get_record(stored.child).await?,
})
}
}
fn has_foreign_fields() -> bool {
true
}
}
async fn load_atomic_save_parent_raw(id: &str) -> Option<StoredAtomicSaveParentRow> {
let stmt = RawSqlStmt::new("SELECT * FROM type::record($table, $id);")
.bind("table", ItAtomicSaveParent::table_name())
.bind("id", id.to_owned());
let value = query_bound_return::<serde_json::Value>(stmt)
.await
.expect("atomic save raw parent query should succeed");
value.map(|value| {
let id = value
.get("id")
.and_then(serde_json::Value::as_str)
.and_then(|raw| {
raw.split_once(':')
.map(|(_, key)| key.trim_matches('`').to_owned())
})
.expect("atomic save raw row should contain normalized string id");
let child = value
.get("child")
.cloned()
.map(serde_json::from_value::<RecordId>)
.expect("atomic save raw row should contain child")
.expect("atomic save raw child should decode as record id");
StoredAtomicSaveParentRow {
id: Id::from(id),
child,
}
})
}
async fn atomic_child_exists(id: &str) -> bool {
Repo::<ItAtomicSaveChild>::get(id).await.is_ok()
}
async fn atomic_parent_exists(id: &str) -> bool {
load_atomic_save_parent_raw(id).await.is_some()
}
fn set_atomic_save_hydrate_fail(value: bool) {
unsafe {
ATOMIC_SAVE_HYDRATE_FAIL = value;
}
}
#[test]
fn single_save_failure_leaves_no_foreign_residue() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItAtomicSaveParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItAtomicSaveChild>::delete_all()
.await
.expect("delete_all should succeed");
set_atomic_save_hydrate_fail(true);
let parent = ItAtomicSaveParent {
id: Id::from("atomic-parent-single"),
child: ItAtomicSaveChild {
id: Id::from("atomic-child-single"),
code: "single-code".to_owned(),
},
};
let err = Repo::<ItAtomicSaveParent>::save(ItAtomicSaveParent {
id: parent.id.clone(),
child: parent.child.clone(),
})
.await
.expect_err("single save should fail after write when hydration fails");
assert!(
err.to_string()
.contains("intentional atomic save hydration failure"),
"unexpected save failure: {err}"
);
assert!(
!atomic_parent_exists("atomic-parent-single").await,
"parent row should not persist after failed save"
);
assert!(
!atomic_child_exists("atomic-child-single").await,
"child row should not persist after failed save"
);
});
}
#[test]
fn foreign_resolution_can_use_injected_persistence_context() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItAtomicSaveChild>::delete_all()
.await
.expect("delete_all should succeed");
let persistence = RecordingForeignPersistence::default();
let record = appdb::resolve_foreign_record_id_with(
&persistence,
ItAtomicSaveChild {
id: Id::from("atomic-child-context"),
code: "context-code".to_owned(),
},
)
.await
.expect("foreign resolution with injected persistence should succeed");
assert_eq!(
record,
RecordId::new("it_atomic_save_child", "atomic-child-context")
);
assert!(atomic_child_exists("atomic-child-context").await);
assert_eq!(
persistence.events(),
vec![
"exists:RecordId { table: Table(\"it_atomic_save_child\"), key: String(\"atomic-child-context\") }".to_owned(),
"ensure_at:RecordId { table: Table(\"it_atomic_save_child\"), key: String(\"atomic-child-context\") }".to_owned(),
]
);
});
}
#[test]
fn save_many_failure_leaves_no_batch_orphans() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItAtomicSaveParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItAtomicSaveChild>::delete_all()
.await
.expect("delete_all should succeed");
set_atomic_save_hydrate_fail(true);
let rows = vec![
ItAtomicSaveParent {
id: Id::from("atomic-parent-batch-a"),
child: ItAtomicSaveChild {
id: Id::from("atomic-child-batch-a"),
code: "batch-code-a".to_owned(),
},
},
ItAtomicSaveParent {
id: Id::from("atomic-parent-batch-b"),
child: ItAtomicSaveChild {
id: Id::from("atomic-child-batch-b"),
code: "batch-code-b".to_owned(),
},
},
];
let err = Repo::<ItAtomicSaveParent>::save_many(rows)
.await
.expect_err("save_many should fail after writes when hydration fails");
assert!(
err.to_string()
.contains("intentional atomic save hydration failure"),
"unexpected batch save failure: {err}"
);
for id in ["atomic-parent-batch-a", "atomic-parent-batch-b"] {
assert!(
!atomic_parent_exists(id).await,
"batch parent {id} should not persist after failed save_many"
);
}
for id in ["atomic-child-batch-a", "atomic-child-batch-b"] {
assert!(
!atomic_child_exists(id).await,
"batch child {id} should not persist after failed save_many"
);
}
});
}
#[test]
fn save_many_reuses_existing_recursive_foreign_graphs_without_duplication() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItRecursiveForeignParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedLookupChild>::delete_all()
.await
.expect("delete_all should succeed");
let seeded = ItRecursiveForeignParent::save(ItRecursiveForeignParent {
id: Id::from("recursive-save-many-shared"),
children: Some(vec![
vec![ItNestedLookupChild {
code: "recursive-save-many-a".to_owned(),
note: Some("seed-a".to_owned()),
}],
vec![ItNestedLookupChild {
code: "recursive-save-many-b".to_owned(),
note: Some("seed-b".to_owned()),
}],
]),
})
.await
.expect("seed recursive foreign save should succeed");
let updated = ItRecursiveForeignParent {
id: Id::from("recursive-save-many-shared"),
children: Some(vec![
vec![
ItNestedLookupChild {
code: "recursive-save-many-a".to_owned(),
note: Some("seed-a".to_owned()),
},
ItNestedLookupChild {
code: "recursive-save-many-c".to_owned(),
note: Some("created-c".to_owned()),
},
],
vec![ItNestedLookupChild {
code: "recursive-save-many-b".to_owned(),
note: Some("seed-b".to_owned()),
}],
]),
};
let created = ItRecursiveForeignParent {
id: Id::from("recursive-save-many-created"),
children: Some(vec![vec![ItNestedLookupChild {
code: "recursive-save-many-d".to_owned(),
note: Some("created-d".to_owned()),
}]]),
};
let saved = ItRecursiveForeignParent::save_many(vec![updated.clone(), created.clone()])
.await
.expect("save_many should recursively upsert recursive foreign graphs");
let loaded_updated = ItRecursiveForeignParent::get("recursive-save-many-shared")
.await
.expect("updated parent should load");
let loaded_created = ItRecursiveForeignParent::get("recursive-save-many-created")
.await
.expect("created parent should load");
let raw_updated = load_recursive_foreign_parent_raw("recursive-save-many-shared").await;
let raw_created = load_recursive_foreign_parent_raw("recursive-save-many-created").await;
let all_children = Repo::<ItNestedLookupChild>::list()
.await
.expect("lookup child list should succeed");
assert_eq!(saved, vec![updated.clone(), created.clone()]);
assert_eq!(loaded_updated, updated);
assert_eq!(loaded_created, created);
assert_ne!(
seeded, loaded_updated,
"seeded parent should be updated in place"
);
assert_eq!(
all_children.len(),
4,
"existing recursive children should be reused, not duplicated"
);
let expected_updated_ids = vec![
vec![
Repo::<ItNestedLookupChild>::find_unique_id_for(
&updated
.children
.as_ref()
.expect("updated children should exist")[0][0],
)
.await
.expect("updated child a should resolve"),
Repo::<ItNestedLookupChild>::find_unique_id_for(
&updated
.children
.as_ref()
.expect("updated children should exist")[0][1],
)
.await
.expect("updated child c should resolve"),
],
vec![
Repo::<ItNestedLookupChild>::find_unique_id_for(
&updated
.children
.as_ref()
.expect("updated children should exist")[1][0],
)
.await
.expect("updated child b should resolve"),
],
];
let expected_created_ids = vec![vec![
Repo::<ItNestedLookupChild>::find_unique_id_for(
&created
.children
.as_ref()
.expect("created children should exist")[0][0],
)
.await
.expect("created child d should resolve"),
]];
assert_eq!(raw_updated.children, Some(expected_updated_ids));
assert_eq!(raw_created.children, Some(expected_created_ids));
});
}
#[test]
fn save_many_duplicate_parent_ids_fail_before_writes_or_foreign_side_effects() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItRecursiveForeignParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedLookupChild>::delete_all()
.await
.expect("delete_all should succeed");
let rows = vec![
ItRecursiveForeignParent {
id: Id::from("duplicate-batch-parent"),
children: Some(vec![vec![ItNestedLookupChild {
code: "duplicate-batch-a".to_owned(),
note: Some("first".to_owned()),
}]]),
},
ItRecursiveForeignParent {
id: Id::from("duplicate-batch-parent"),
children: Some(vec![vec![ItNestedLookupChild {
code: "duplicate-batch-b".to_owned(),
note: Some("second".to_owned()),
}]]),
},
];
let err = ItRecursiveForeignParent::save_many(rows)
.await
.expect_err("duplicate parent ids should fail before any writes");
let typed = DBError::from(err);
assert_eq!(typed.kind(), DBErrorKind::Conflict);
assert!(
typed
.to_string()
.contains("duplicate record id in one batch"),
"unexpected duplicate batch error: {typed}"
);
assert!(
!Repo::<ItRecursiveForeignParent>::exists_record(RecordId::new(
ItRecursiveForeignParent::table_name(),
"duplicate-batch-parent"
))
.await
.expect("parent existence check should succeed"),
"duplicate batch should not persist any parent rows"
);
assert!(
!Repo::<ItNestedLookupChild>::exists_record(RecordId::new(
ItNestedLookupChild::table_name(),
"duplicate-batch-a"
))
.await
.expect("first child existence check should succeed"),
"duplicate batch should not leave the first foreign side effect"
);
assert!(
!Repo::<ItNestedLookupChild>::exists_record(RecordId::new(
ItNestedLookupChild::table_name(),
"duplicate-batch-b"
))
.await
.expect("second child existence check should succeed"),
"duplicate batch should not leave the second foreign side effect"
);
});
}
#[test]
fn failed_save_can_retry_cleanly_with_same_identifiers() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItAtomicSaveParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItAtomicSaveChild>::delete_all()
.await
.expect("delete_all should succeed");
set_atomic_save_hydrate_fail(true);
let failed = ItAtomicSaveParent {
id: Id::from("atomic-parent-retry"),
child: ItAtomicSaveChild {
id: Id::from("atomic-child-retry"),
code: "retry-initial".to_owned(),
},
};
let err = Repo::<ItAtomicSaveParent>::save(failed)
.await
.expect_err("initial save should fail after write when hydration fails");
assert!(
err.to_string()
.contains("intentional atomic save hydration failure"),
"unexpected retry setup failure: {err}"
);
assert!(
!atomic_parent_exists("atomic-parent-retry").await,
"failed save should leave no parent residue before retry"
);
assert!(
!atomic_child_exists("atomic-child-retry").await,
"failed save should leave no child residue before retry"
);
let corrected = ItAtomicSaveParent {
id: Id::from("atomic-parent-retry"),
child: ItAtomicSaveChild {
id: Id::from("atomic-child-retry"),
code: "retry-corrected".to_owned(),
},
};
set_atomic_save_hydrate_fail(false);
let saved = Repo::<ItAtomicSaveParent>::save(corrected.clone())
.await
.expect("retry save should succeed once cleanup is correct");
let loaded = Repo::<ItAtomicSaveParent>::get("atomic-parent-retry")
.await
.expect("retry get should return hydrated parent");
let raw = load_atomic_save_parent_raw("atomic-parent-retry")
.await
.expect("retry parent row should exist");
let child = Repo::<ItAtomicSaveChild>::get("atomic-child-retry")
.await
.expect("retry child row should exist");
assert_eq!(saved, corrected);
assert_eq!(loaded, corrected);
assert_eq!(child, corrected.child);
assert!(
atomic_parent_exists("atomic-parent-retry").await,
"successful retry should persist the parent once the failure flag is cleared"
);
assert!(
atomic_child_exists("atomic-child-retry").await,
"successful retry should persist the child once the failure flag is cleared"
);
assert_eq!(
raw.child,
RecordId::new(ItAtomicSaveChild::table_name(), "atomic-child-retry")
);
});
}
#[test]
fn nested_ref_vec_and_collection_hydration() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItNestedVecParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedLookupChild>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedOptionalParent>::delete_all()
.await
.expect("delete_all should succeed");
let existing_child = Repo::<ItNestedLookupChild>::create(ItNestedLookupChild {
code: "vec-existing".to_owned(),
note: Some("reused".to_owned()),
})
.await
.expect("seeded vec child create should succeed");
let existing_child_id = existing_child
.resolve_record_id()
.await
.expect("existing vec child should resolve");
let first_parent = ItNestedVecParent {
id: Id::from("nested-vec-parent-1"),
children: vec![
existing_child.clone(),
ItNestedLookupChild {
code: "vec-created-a".to_owned(),
note: Some("created-a".to_owned()),
},
ItNestedLookupChild {
code: "vec-created-b".to_owned(),
note: None,
},
],
};
let first_saved = ItNestedVecParent::save(first_parent.clone())
.await
.expect("vec nested save should succeed");
let first_loaded = ItNestedVecParent::get("nested-vec-parent-1")
.await
.expect("vec nested get should succeed");
let first_raw = load_nested_vec_parent_raw("nested-vec-parent-1").await;
let first_expected_ids = vec![
existing_child_id.clone(),
Repo::<ItNestedLookupChild>::find_unique_id_for(&first_parent.children[1])
.await
.expect("created vec child A should resolve"),
Repo::<ItNestedLookupChild>::find_unique_id_for(&first_parent.children[2])
.await
.expect("created vec child B should resolve"),
];
assert_eq!(first_saved, first_parent);
assert_eq!(first_loaded, first_parent);
assert_eq!(first_raw.children, first_expected_ids);
let direct_parent = ItNestedParent {
id: Id::from("nested-vec-direct-parent"),
child: ItNestedIdChild {
id: Id::from("nested-vec-direct-child"),
name: "direct-alpha".to_owned(),
},
};
let optional_parent = ItNestedOptionalParent {
id: Id::from("nested-vec-optional-parent"),
child: Some(ItNestedLookupChild {
code: "vec-optional".to_owned(),
note: Some("optional".to_owned()),
}),
};
let second_parent = ItNestedVecParent {
id: Id::from("nested-vec-parent-2"),
children: vec![
ItNestedLookupChild {
code: "vec-second-a".to_owned(),
note: Some("second-a".to_owned()),
},
existing_child.clone(),
],
};
let _direct_child_created = Repo::<ItNestedIdChild>::create(direct_parent.child.clone())
.await
.expect("direct child create should succeed");
let direct_saved = ItNestedParent::save(direct_parent.clone())
.await
.expect("direct nested save should succeed");
let optional_saved = ItNestedOptionalParent::save(optional_parent.clone())
.await
.expect("optional nested save should succeed");
let second_saved = ItNestedVecParent::save(second_parent.clone())
.await
.expect("second vec nested save should succeed");
let second_raw = load_nested_vec_parent_raw("nested-vec-parent-2").await;
let second_expected_ids = vec![
Repo::<ItNestedLookupChild>::find_unique_id_for(&second_parent.children[0])
.await
.expect("second vec child A should resolve"),
existing_child_id.clone(),
];
assert_eq!(direct_saved, direct_parent);
assert_eq!(optional_saved, optional_parent);
assert_eq!(second_saved, second_parent);
assert_eq!(second_raw.children, second_expected_ids);
let all_children = Repo::<ItNestedLookupChild>::list()
.await
.expect("lookup child list should succeed");
assert_eq!(all_children.len(), 5);
let vec_list = ItNestedVecParent::list()
.await
.expect("vec nested list should succeed");
assert_eq!(vec_list.len(), 2);
assert!(vec_list.contains(&first_parent));
assert!(vec_list.contains(&second_parent));
let direct_list = ItNestedParent::list()
.await
.expect("direct nested list should succeed");
assert_eq!(direct_list, vec![direct_parent.clone()]);
let optional_list = ItNestedOptionalParent::list()
.await
.expect("optional nested list should succeed");
assert_eq!(optional_list, vec![optional_parent.clone()]);
let vec_limited = ItNestedVecParent::list_limit(1)
.await
.expect("vec nested list_limit should succeed");
assert_eq!(vec_limited.len(), 1);
assert!(vec_limited[0] == first_parent || vec_limited[0] == second_parent);
let direct_limited = ItNestedParent::list_limit(1)
.await
.expect("direct nested list_limit should succeed");
assert_eq!(direct_limited, vec![direct_parent]);
let optional_limited = ItNestedOptionalParent::list_limit(1)
.await
.expect("optional nested list_limit should succeed");
assert_eq!(optional_limited, vec![optional_parent]);
});
}
#[test]
fn nested_ref_recursive_option_vec_shapes_roundtrip() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItRecursiveForeignParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedLookupChild>::delete_all()
.await
.expect("delete_all should succeed");
let parent = ItRecursiveForeignParent {
id: Id::from("recursive-foreign-parent"),
children: Some(vec![
vec![
ItNestedLookupChild {
code: "recursive-a".to_owned(),
note: Some("note-a".to_owned()),
},
ItNestedLookupChild {
code: "recursive-b".to_owned(),
note: None,
},
],
vec![ItNestedLookupChild {
code: "recursive-c".to_owned(),
note: Some("note-c".to_owned()),
}],
]),
};
let saved = ItRecursiveForeignParent::save(parent.clone())
.await
.expect("recursive foreign save should succeed");
let loaded = ItRecursiveForeignParent::get("recursive-foreign-parent")
.await
.expect("recursive foreign get should succeed");
let listed = ItRecursiveForeignParent::list()
.await
.expect("recursive foreign list should succeed");
let raw = load_recursive_foreign_parent_raw("recursive-foreign-parent").await;
let expected_ids = parent
.children
.as_ref()
.expect("children should be present")
.iter()
.map(|row| async move {
let mut ids = Vec::with_capacity(row.len());
for child in row {
ids.push(
Repo::<ItNestedLookupChild>::find_unique_id_for(child)
.await
.expect("recursive child should resolve"),
);
}
ids
});
let mut resolved_nested_ids = Vec::new();
for future in expected_ids {
resolved_nested_ids.push(future.await);
}
assert_eq!(saved, parent);
assert_eq!(loaded, parent);
assert_eq!(listed, vec![parent.clone()]);
assert_eq!(raw.children, Some(resolved_nested_ids));
});
}
#[test]
fn manual_foreign_enum_dispatcher_roundtrip_passes() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItManualForeignParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItManualForeignAlphaChild>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItManualForeignBetaChild>::delete_all()
.await
.expect("delete_all should succeed");
let alpha_parent = ItManualForeignParent {
id: Id::from("manual-foreign-alpha-parent"),
child: ItManualForeignChild::Alpha(ItManualForeignAlphaChild {
id: Id::from("manual-foreign-alpha-child"),
name: "alpha".to_owned(),
}),
};
let alpha_child = match &alpha_parent.child {
ItManualForeignChild::Alpha(child) => child.clone(),
ItManualForeignChild::Beta(_) => {
unreachable!("alpha parent should contain alpha child")
}
};
Repo::<ItManualForeignAlphaChild>::create(alpha_child)
.await
.expect("alpha child seed should succeed");
let saved_alpha = ItManualForeignParent::save(alpha_parent.clone())
.await
.expect("alpha foreign save should succeed");
let loaded_alpha = ItManualForeignParent::get("manual-foreign-alpha-parent")
.await
.expect("alpha foreign get should succeed");
let raw_alpha = load_manual_foreign_parent_raw("manual-foreign-alpha-parent").await;
assert_eq!(saved_alpha, alpha_parent);
assert_eq!(loaded_alpha, alpha_parent);
assert_eq!(
raw_alpha.child,
RecordId::new(
ItManualForeignAlphaChild::table_name(),
"manual-foreign-alpha-child"
)
);
let beta_parent = ItManualForeignParent {
id: Id::from("manual-foreign-beta-parent"),
child: ItManualForeignChild::Beta(ItManualForeignBetaChild {
code: "manual-foreign-beta-child".to_owned(),
note: Some("beta-note".to_owned()),
}),
};
let saved_beta = ItManualForeignParent::save(beta_parent.clone())
.await
.expect("beta foreign save should succeed");
let loaded_beta = ItManualForeignParent::get("manual-foreign-beta-parent")
.await
.expect("beta foreign get should succeed");
let raw_beta = load_manual_foreign_parent_raw("manual-foreign-beta-parent").await;
let beta_child_id =
Repo::<ItManualForeignBetaChild>::find_unique_id_for(match &beta_parent.child {
ItManualForeignChild::Beta(child) => child,
ItManualForeignChild::Alpha(_) => {
unreachable!("beta parent should contain beta child")
}
})
.await
.expect("beta child id should resolve");
assert_eq!(saved_beta, beta_parent);
assert_eq!(loaded_beta, beta_parent);
assert_eq!(raw_beta.child, beta_child_id);
});
}
#[test]
fn store_unique_field_registers_schema_index() {
let ddls = inventory::iter::<appdb::model::schema::SchemaItem>
.into_iter()
.map(|item| item.ddl)
.collect::<Vec<_>>();
assert!(ddls.iter().any(|ddl| {
ddl.contains("DEFINE INDEX IF NOT EXISTS it_profile_name_unique")
&& ddl.contains("ON it_profile")
&& ddl.contains("FIELDS name UNIQUE")
}));
assert!(ddls.iter().any(|ddl| {
ddl.contains("DEFINE INDEX IF NOT EXISTS it_aliased_post_slug_unique")
&& ddl.contains("ON it_aliased_post")
&& ddl.contains("FIELDS slug UNIQUE")
}));
assert!(ddls.iter().any(|ddl| {
ddl.contains("DEFINE INDEX IF NOT EXISTS it_paged_entry_created_at_id_pagin")
&& ddl.contains("ON it_paged_entry")
&& ddl.contains("FIELDS created_at,id")
&& !ddl.contains("UNIQUE")
}));
assert!(ddls.iter().any(|ddl| {
ddl.contains("DEFINE INDEX IF NOT EXISTS it_auto_filled_entry_created_at_id_pagin")
&& ddl.contains("ON it_auto_filled_entry")
&& ddl.contains("FIELDS created_at,id")
&& !ddl.contains("UNIQUE")
}));
}
#[test]
fn store_pagin_roundtrips_with_stable_desc_and_asc_cursor_order() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
ItPagedEntry::delete_all()
.await
.expect("paged entry cleanup should succeed");
ItPagedEntry::save_many(vec![
ItPagedEntry {
id: Id::from(1i64),
created_at: 100,
title: "first".to_owned(),
},
ItPagedEntry {
id: Id::from(2i64),
created_at: 100,
title: "second".to_owned(),
},
ItPagedEntry {
id: Id::from(3i64),
created_at: 90,
title: "third".to_owned(),
},
ItPagedEntry {
id: Id::from(4i64),
created_at: 80,
title: "fourth".to_owned(),
},
])
.await
.expect("save_many should succeed");
let first_desc = ItPagedEntry::pagin_desc(2, None)
.await
.expect("first desc page should succeed");
assert_eq!(
first_desc
.items
.iter()
.map(|item| item.id.clone())
.collect::<Vec<_>>(),
vec![Id::from(2i64), Id::from(1i64)]
);
assert!(
first_desc.next.is_some(),
"first desc page should expose a next cursor"
);
let second_desc = ItPagedEntry::pagin_desc(2, first_desc.next.clone())
.await
.expect("second desc page should succeed");
assert_eq!(
second_desc
.items
.iter()
.map(|item| item.id.clone())
.collect::<Vec<_>>(),
vec![Id::from(3i64), Id::from(4i64)]
);
assert!(
second_desc.next.is_none(),
"last desc page should not expose another cursor"
);
let first_asc = ItPagedEntry::pagin_asc(3, None)
.await
.expect("first asc page should succeed");
assert_eq!(
first_asc
.items
.iter()
.map(|item| item.id.clone())
.collect::<Vec<_>>(),
vec![Id::from(4i64), Id::from(3i64), Id::from(1i64)]
);
assert!(
first_asc.next.is_some(),
"first asc page should expose a next cursor"
);
let second_asc = ItPagedEntry::pagin_asc(3, first_asc.next.clone())
.await
.expect("second asc page should succeed");
assert_eq!(
second_asc
.items
.iter()
.map(|item| item.id.clone())
.collect::<Vec<_>>(),
vec![Id::from(2i64)]
);
assert!(
second_asc.next.is_none(),
"last asc page should not expose another cursor"
);
});
}
#[test]
fn store_pagin_supports_id_as_pagination_field() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
ItIdPagedEntry::delete_all()
.await
.expect("id-paged entry cleanup should succeed");
ItIdPagedEntry::save_many(vec![
ItIdPagedEntry {
id: Id::from(1i64),
title: "first".to_owned(),
},
ItIdPagedEntry {
id: Id::from(2i64),
title: "second".to_owned(),
},
ItIdPagedEntry {
id: Id::from(3i64),
title: "third".to_owned(),
},
ItIdPagedEntry {
id: Id::from(4i64),
title: "fourth".to_owned(),
},
])
.await
.expect("save_many should succeed");
let first_desc = ItIdPagedEntry::pagin_desc(2, None)
.await
.expect("first desc page should succeed");
assert_eq!(
first_desc
.items
.iter()
.map(|item| item.id.clone())
.collect::<Vec<_>>(),
vec![Id::from(4i64), Id::from(3i64)]
);
assert!(
first_desc.next.is_some(),
"first desc page should expose a next cursor"
);
let second_desc = ItIdPagedEntry::pagin_desc(2, first_desc.next.clone())
.await
.expect("second desc page should succeed");
assert_eq!(
second_desc
.items
.iter()
.map(|item| item.id.clone())
.collect::<Vec<_>>(),
vec![Id::from(2i64), Id::from(1i64)]
);
assert!(
second_desc.next.is_none(),
"last desc page should not expose another cursor"
);
let first_asc = ItIdPagedEntry::pagin_asc(3, None)
.await
.expect("first asc page should succeed");
assert_eq!(
first_asc
.items
.iter()
.map(|item| item.id.clone())
.collect::<Vec<_>>(),
vec![Id::from(1i64), Id::from(2i64), Id::from(3i64)]
);
assert!(
first_asc.next.is_some(),
"first asc page should expose a next cursor"
);
let second_asc = ItIdPagedEntry::pagin_asc(3, first_asc.next.clone())
.await
.expect("second asc page should succeed");
assert_eq!(
second_asc
.items
.iter()
.map(|item| item.id.clone())
.collect::<Vec<_>>(),
vec![Id::from(4i64)]
);
assert!(
second_asc.next.is_none(),
"last asc page should not expose another cursor"
);
});
}
#[test]
fn store_fill_now_resolves_pending_autofill_without_overwriting_existing_values() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
ItAutoFilledEntry::delete_all()
.await
.expect("auto filled entry cleanup should succeed");
let saved = ItAutoFilledEntry::save(ItAutoFilledEntry {
id: Id::from("auto-filled-now"),
created_at: AutoFill::pending(),
title: "first".to_owned(),
})
.await
.expect("save should resolve pending autofill");
assert!(
!saved.created_at.is_pending(),
"save should fill pending autofill values"
);
let saved_many = ItAutoFilledEntry::save_many(vec![
ItAutoFilledEntry {
id: Id::from("auto-filled-many-1"),
created_at: AutoFill::pending(),
title: "second".to_owned(),
},
ItAutoFilledEntry {
id: Id::from("auto-filled-many-2"),
created_at: AutoFill::pending(),
title: "third".to_owned(),
},
])
.await
.expect("save_many should resolve pending autofill values");
assert!(saved_many.iter().all(|item| !item.created_at.is_pending()));
let original_created_at = saved.created_at.clone();
let updated = ItAutoFilledEntry::save(ItAutoFilledEntry {
title: "updated".to_owned(),
..saved.clone()
})
.await
.expect("save should preserve resolved autofill values");
assert_eq!(updated.created_at, original_created_at);
});
}
#[test]
fn store_autofill_supports_ordered_list_and_keyset_pagination() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
ItAutoFilledEntry::delete_all()
.await
.expect("auto filled entry cleanup should succeed");
ItAutoFilledEntry::save_many(vec![
ItAutoFilledEntry {
id: Id::from(1i64),
created_at: AutoFill::resolved("2026-04-23T08:00:00Z"),
title: "first".to_owned(),
},
ItAutoFilledEntry {
id: Id::from(2i64),
created_at: AutoFill::resolved("2026-04-23T08:00:00Z"),
title: "second".to_owned(),
},
ItAutoFilledEntry {
id: Id::from(3i64),
created_at: AutoFill::resolved("2026-04-23T07:00:00Z"),
title: "third".to_owned(),
},
ItAutoFilledEntry {
id: Id::from(4i64),
created_at: AutoFill::resolved("2026-04-23T06:00:00Z"),
title: "fourth".to_owned(),
},
])
.await
.expect("save_many should succeed");
let ordered_desc = ItAutoFilledEntry::list()
.order_by("created_at", Order::Desc)
.await
.expect("ordered list should succeed");
assert_eq!(
ordered_desc
.iter()
.map(|item| item.id.clone())
.collect::<Vec<_>>(),
vec![
Id::from(2i64),
Id::from(1i64),
Id::from(3i64),
Id::from(4i64)
]
);
let ordered_by_id = ItAutoFilledEntry::list()
.order_by("id", Order::Asc)
.await
.expect("ordering by id should succeed");
assert_eq!(
ordered_by_id
.iter()
.map(|item| item.id.clone())
.collect::<Vec<_>>(),
vec![
Id::from(1i64),
Id::from(2i64),
Id::from(3i64),
Id::from(4i64)
]
);
let first_desc = ItAutoFilledEntry::pagin_desc(2, None)
.await
.expect("first desc page should succeed");
assert_eq!(
first_desc
.items
.iter()
.map(|item| item.id.clone())
.collect::<Vec<_>>(),
vec![Id::from(2i64), Id::from(1i64)]
);
let second_desc = ItAutoFilledEntry::pagin_desc(2, first_desc.next.clone())
.await
.expect("second desc page should succeed");
assert_eq!(
second_desc
.items
.iter()
.map(|item| item.id.clone())
.collect::<Vec<_>>(),
vec![Id::from(3i64), Id::from(4i64)]
);
let invalid = ItAutoFilledEntry::list()
.order_by("title", Order::Desc)
.await
.expect_err("ordering by undeclared fields should be rejected");
let typed = appdb::classify_db_error(&invalid);
assert!(
matches!(typed, DBError::InvalidModel(_)),
"expected invalid model error, got {typed:?}"
);
});
}
#[test]
fn table_as_reuses_target_table_name_and_lookup_metadata() {
assert_eq!(ItAliasedPost::table_name(), ItAliasedPostBase::table_name());
assert_eq!(
ItAliasedPost::storage_table(),
ItAliasedPostBase::storage_table()
);
assert_eq!(
ItAliasedPostBase::storage_table(),
ItAliasedPost::table_name()
);
assert_eq!(ItAliasedPostBase::lookup_fields(), &["slug"]);
}
#[test]
fn table_as_alias_preserves_foreign_roundtrip_through_target_table() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItAliasedForeignParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItAliasedPost>::delete_all()
.await
.expect("delete_all should succeed");
let post = ItAliasedPost {
id: Id::from("aliased-post"),
slug: "post-slug".to_owned(),
title: "Full title".to_owned(),
body: "Full body".to_owned(),
};
let saved_post = ItAliasedPost::save(post.clone())
.await
.expect("target save should succeed");
let post_base = ItAliasedPostBase::get("aliased-post")
.await
.expect("alias get should succeed on target table");
assert_eq!(saved_post.id, post_base.id);
assert_eq!(saved_post.slug, post_base.slug);
assert_eq!(saved_post.title, post_base.title);
let alias_lookup = Repo::<ItAliasedPostBase>::find_unique_id_for(&ItAliasedPostBase {
id: Id::from("ignored"),
slug: "post-slug".to_owned(),
title: "Different title ignored by lookup".to_owned(),
})
.await
.expect("alias lookup should resolve on shared table");
assert_eq!(
alias_lookup,
RecordId::new(ItAliasedPost::table_name(), "aliased-post")
);
let parent = ItAliasedForeignParent {
id: Id::from("aliased-parent"),
featured: Some(ItAliasedPostBase {
id: Id::from("aliased-post"),
slug: "post-slug".to_owned(),
title: "Full title".to_owned(),
}),
nested: Some(vec![vec![ItAliasedPostBase {
id: Id::from("aliased-post"),
slug: "post-slug".to_owned(),
title: "Full title".to_owned(),
}]]),
};
let saved_parent = ItAliasedForeignParent::save(parent.clone())
.await
.expect("aliased foreign parent save should succeed");
let loaded_parent = ItAliasedForeignParent::get("aliased-parent")
.await
.expect("aliased foreign parent get should succeed");
let listed_parent = ItAliasedForeignParent::list()
.await
.expect("aliased foreign parent list should succeed")
.into_iter()
.find(|row| row.id == Id::from("aliased-parent"))
.expect("aliased parent should be present in list results");
let limited_parent = ItAliasedForeignParent::list_limit(10)
.await
.expect("aliased foreign parent list_limit should succeed")
.into_iter()
.find(|row| row.id == Id::from("aliased-parent"))
.expect("aliased parent should be present in list_limit results");
let raw_parent = load_aliased_foreign_parent_raw("aliased-parent").await;
assert_eq!(saved_parent, parent);
assert_eq!(loaded_parent, parent);
assert_eq!(listed_parent, parent);
assert_eq!(limited_parent, parent);
let expected_record = RecordId::new(ItAliasedPost::table_name(), "aliased-post");
assert_eq!(raw_parent.featured, Some(expected_record.clone()));
assert_eq!(raw_parent.nested, Some(vec![vec![expected_record]]));
});
}
#[test]
fn table_as_alias_with_nested_foreign_roundtrips_across_all_reads() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
ensure_tables_exist(&[
ItAliasedNestedAuthor::table_name(),
ItAliasedNestedPost::table_name(),
ItAliasedNestedHolder::table_name(),
])
.await;
Repo::<ItAliasedNestedHolder>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItAliasedNestedPost>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItAliasedNestedAuthor>::delete_all()
.await
.expect("delete_all should succeed");
let holder = ItAliasedNestedHolder {
id: Id::from("aliased-holder-target"),
featured: ItAliasedNestedPostAlias {
id: Id::from("aliased-nested-post-target"),
slug: "alias-target".to_owned(),
headline: "Alias headline".to_owned(),
author: ItAliasedNestedAuthor {
id: Id::from("aliased-author-target"),
handle: "alias-author".to_owned(),
display_name: "Alias Author".to_owned(),
},
},
};
let other = ItAliasedNestedHolder {
id: Id::from("aliased-holder-other"),
featured: ItAliasedNestedPostAlias {
id: Id::from("aliased-nested-post-other"),
slug: "alias-other".to_owned(),
headline: "Other headline".to_owned(),
author: ItAliasedNestedAuthor {
id: Id::from("aliased-author-other"),
handle: "alias-author-other".to_owned(),
display_name: "Other Author".to_owned(),
},
},
};
ItAliasedNestedHolder::save(other)
.await
.expect("seed save should succeed");
let saved = ItAliasedNestedHolder::save(holder.clone())
.await
.expect("aliased nested holder save should succeed");
let got = ItAliasedNestedHolder::get("aliased-holder-target")
.await
.expect("aliased nested holder get should succeed");
let got_record = ItAliasedNestedHolder::get_record(RecordId::new(
ItAliasedNestedHolder::table_name(),
"aliased-holder-target",
))
.await
.expect("aliased nested holder get_record should succeed");
let listed = ItAliasedNestedHolder::list()
.await
.expect("aliased nested holder list should succeed")
.into_iter()
.find(|row| row.id == Id::from("aliased-holder-target"))
.expect("target holder should be present in list results");
let limited = ItAliasedNestedHolder::list_limit(10)
.await
.expect("aliased nested holder list_limit should succeed")
.into_iter()
.find(|row| row.id == Id::from("aliased-holder-target"))
.expect("target holder should be present in list_limit results");
let raw_alias_row = load_aliased_nested_post_raw("aliased-nested-post-target").await;
let raw_holder_row = load_aliased_nested_holder_raw("aliased-holder-target").await;
assert_eq!(saved, holder);
assert_eq!(got, holder);
assert_eq!(got_record, holder);
assert_eq!(listed, holder);
assert_eq!(limited, holder);
assert_eq!(
raw_alias_row.author,
RecordId::new(ItAliasedNestedAuthor::table_name(), "aliased-author-target")
);
assert_eq!(
raw_holder_row.featured,
RecordId::new(
ItAliasedNestedPost::table_name(),
"aliased-nested-post-target"
)
);
});
}
#[test]
fn store_auto_lookup_uses_unique_fields() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItProfile>::delete_all()
.await
.expect("delete_all should succeed");
let saved = Repo::<ItProfile>::save(ItProfile {
id: Id::from("p-lookup"),
name: "alice".to_owned(),
note: Some("x".to_owned()),
})
.await
.expect("save should succeed");
let id = Repo::<ItProfile>::find_unique_id_for(&ItProfile {
id: Id::from("ignored"),
name: "alice".to_owned(),
note: None,
})
.await
.expect("unique lookup should succeed");
assert_eq!(id, saved.id());
});
}
#[test]
fn store_auto_lookup_uses_all_unique_fields_together() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItCompositeUnique>::delete_all()
.await
.expect("delete_all should succeed");
let saved = Repo::<ItCompositeUnique>::save(ItCompositeUnique {
id: Id::from("cu-1"),
name: "alice".to_owned(),
locale: "en".to_owned(),
note: Some("hello".to_owned()),
})
.await
.expect("save should succeed");
let id = Repo::<ItCompositeUnique>::find_unique_id_for(&ItCompositeUnique {
id: Id::from("ignored"),
name: "alice".to_owned(),
locale: "en".to_owned(),
note: None,
})
.await
.expect("composite unique lookup should succeed");
assert_eq!(id, saved.id());
});
}
#[test]
fn store_auto_lookup_falls_back_to_non_id_fields() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItFallbackLookup>::delete_all()
.await
.expect("delete_all should succeed");
let created = Repo::<ItFallbackLookup>::create(ItFallbackLookup {
name: "fallback".to_owned(),
note: Some("note".to_owned()),
})
.await
.expect("create should succeed");
let id = Repo::<ItFallbackLookup>::find_unique_id_for(&ItFallbackLookup {
name: "fallback".to_owned(),
note: Some("note".to_owned()),
})
.await
.expect("fallback lookup should succeed");
let loaded = Repo::<ItFallbackLookup>::get_record(id)
.await
.expect("get_record should succeed");
assert_eq!(loaded.name, created.name);
assert_eq!(loaded.note, created.note);
});
}
#[test]
fn store_auto_lookup_resolves_foreign_only_models_through_record_ids() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItForeignOnlyLookup>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItLookupTarget>::delete_all()
.await
.expect("delete_all should succeed");
let child = Repo::<ItLookupTarget>::create(ItLookupTarget {
code: "foreign-only-target".to_owned(),
})
.await
.expect("foreign child create should succeed");
Repo::<ItForeignOnlyLookup>::create(ItForeignOnlyLookup {
child: child.clone(),
})
.await
.expect("foreign-only parent create should succeed");
assert_eq!(ItForeignOnlyLookup::lookup_fields(), &["child"]);
let id = Repo::<ItForeignOnlyLookup>::find_unique_id_for(&ItForeignOnlyLookup {
child: ItLookupTarget {
code: "foreign-only-target".to_owned(),
},
})
.await
.expect("foreign-only lookup should resolve through child record id");
let loaded = Repo::<ItForeignOnlyLookup>::get_record(id)
.await
.expect("get_record should succeed");
assert_eq!(loaded.child.code, child.code);
});
}
#[test]
fn store_auto_lookup_uses_unique_foreign_fields() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItForeignUniqueLookup>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItLookupTarget>::delete_all()
.await
.expect("delete_all should succeed");
let child = Repo::<ItLookupTarget>::create(ItLookupTarget {
code: "foreign-unique-target".to_owned(),
})
.await
.expect("foreign child create should succeed");
let saved = Repo::<ItForeignUniqueLookup>::save(ItForeignUniqueLookup {
id: Id::from("foreign-unique-parent"),
child: child.clone(),
})
.await
.expect("save should succeed");
assert_eq!(ItForeignUniqueLookup::lookup_fields(), &["child"]);
let id = Repo::<ItForeignUniqueLookup>::find_unique_id_for(&ItForeignUniqueLookup {
id: Id::from("ignored"),
child: ItLookupTarget {
code: "foreign-unique-target".to_owned(),
},
})
.await
.expect("unique foreign lookup should resolve through child record id");
assert_eq!(id, saved.id());
});
}
#[test]
fn store_auto_lookup_for_foreign_fields_does_not_create_missing_children() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
let bootstrap_child = Repo::<ItLookupTarget>::create(ItLookupTarget {
code: "foreign-missing-bootstrap".to_owned(),
})
.await
.expect("bootstrap child create should succeed");
Repo::<ItForeignOnlyLookup>::create(ItForeignOnlyLookup {
child: bootstrap_child,
})
.await
.expect("bootstrap parent create should succeed");
Repo::<ItForeignOnlyLookup>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItLookupTarget>::delete_all()
.await
.expect("delete_all should succeed");
let err = Repo::<ItForeignOnlyLookup>::find_unique_id_for(&ItForeignOnlyLookup {
child: ItLookupTarget {
code: "missing-foreign-child".to_owned(),
},
})
.await
.expect_err("missing foreign child should not resolve");
assert!(err.to_string().contains("Record not found"), "{err}");
assert!(
Repo::<ItLookupTarget>::list_record_ids()
.await
.expect("list_record_ids should succeed")
.is_empty()
);
assert!(
Repo::<ItForeignOnlyLookup>::list_record_ids()
.await
.expect("list_record_ids should succeed")
.is_empty()
);
});
}
#[test]
fn store_auto_lookup_errors_when_match_is_not_unique() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItFallbackLookup>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItFallbackLookup>::create(ItFallbackLookup {
name: "dup".to_owned(),
note: Some("same".to_owned()),
})
.await
.expect("first create should succeed");
Repo::<ItFallbackLookup>::create(ItFallbackLookup {
name: "dup".to_owned(),
note: Some("same".to_owned()),
})
.await
.expect("second create should succeed");
let err = Repo::<ItFallbackLookup>::find_unique_id_for(&ItFallbackLookup {
name: "dup".to_owned(),
note: Some("same".to_owned()),
})
.await
.expect_err("duplicate fallback lookup should fail");
assert!(err.to_string().contains("multiple records"), "{err}");
});
}
#[test]
fn store_auto_lookup_returns_not_found_when_unique_value_is_missing() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItProfile>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItProfile>::save(ItProfile {
id: Id::from("present"),
name: "alice".to_owned(),
note: Some("x".to_owned()),
})
.await
.expect("save should succeed");
let err = Repo::<ItProfile>::find_unique_id_for(&ItProfile {
id: Id::from("ignored"),
name: "missing".to_owned(),
note: None,
})
.await
.expect_err("missing unique value should not resolve");
assert!(err.to_string().contains("Record not found"), "{err}");
});
}
#[test]
fn store_auto_lookup_requires_all_unique_fields_to_match() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItCompositeUnique>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItCompositeUnique>::save(ItCompositeUnique {
id: Id::from("cu-present"),
name: "alice".to_owned(),
locale: "en".to_owned(),
note: Some("hello".to_owned()),
})
.await
.expect("save should succeed");
let err = Repo::<ItCompositeUnique>::find_unique_id_for(&ItCompositeUnique {
id: Id::from("ignored"),
name: "alice".to_owned(),
locale: "fr".to_owned(),
note: None,
})
.await
.expect_err("partial unique match should not resolve");
assert!(err.to_string().contains("Record not found"), "{err}");
});
}
#[test]
fn store_auto_lookup_with_missing_optional_field_does_not_match_unrelated_rows() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItFallbackLookup>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItFallbackLookup>::create(ItFallbackLookup {
name: "null-case-a".to_owned(),
note: None,
})
.await
.expect("first create should succeed");
Repo::<ItFallbackLookup>::create(ItFallbackLookup {
name: "null-case-b".to_owned(),
note: None,
})
.await
.expect("second create should succeed");
let err = Repo::<ItFallbackLookup>::find_unique_id_for(&ItFallbackLookup {
name: "null-case-missing".to_owned(),
note: None,
})
.await
.expect_err("missing optional-field lookup should not false-positive");
assert!(err.to_string().contains("Record not found"), "{err}");
});
}
#[test]
fn sensitive_store_lookup_metadata_excludes_secure_fields_from_fallback() {
assert_eq!(
ItSensitiveFallbackLookup::lookup_fields(),
&["alias", "note"]
);
}
#[test]
fn mixed_sensitive_models_resolve_unique_ids_through_non_secure_fields() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItSensitiveLookupSource>::delete_all()
.await
.expect("delete_all should succeed");
let source = Repo::<ItSensitiveLookupSource>::create(ItSensitiveLookupSource {
alias: "sensitive-source".to_owned(),
secret: "source-secret".to_owned(),
note: Some("source-note".to_owned()),
})
.await
.expect("source create should succeed");
assert_eq!(ItSensitiveLookupSource::lookup_fields(), &["alias"]);
let resolved = Repo::<ItSensitiveLookupSource>::find_unique_id_for(&source)
.await
.expect("mixed model lookup should resolve via non-secure unique field");
assert_eq!(
resolved,
source
.resolve_record_id()
.await
.expect("resolve_record_id should use the non-secure lookup path")
);
});
}
#[test]
fn save_many_batches_rows() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItProfile>::delete_all()
.await
.expect("delete_all should succeed");
let inserted = Repo::<ItProfile>::save_many(vec![
ItProfile {
id: Id::from("p1"),
name: "alice".to_owned(),
note: Some("a".to_owned()),
},
ItProfile {
id: Id::from("p2"),
name: "bob".to_owned(),
note: None,
},
])
.await
.expect("batch save should succeed");
assert_eq!(inserted.len(), 2);
assert_eq!(inserted[0].id, Id::from("p1"));
assert_eq!(inserted[0].name, "alice");
assert_eq!(inserted[0].note.as_deref(), Some("a"));
assert_eq!(inserted[1].id, Id::from("p2"));
assert_eq!(inserted[1].name, "bob");
assert_eq!(inserted[1].note, None);
let selected = Repo::<ItProfile>::list()
.await
.expect("list should succeed");
assert_eq!(selected.len(), 2);
});
}
#[test]
fn upsert_id_without_id_field_fails() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
let err = Repo::<ItNoId>::save(ItNoId {
name: "alice".to_owned(),
})
.await
.expect_err("missing `id` field should fail");
assert!(
err.to_string()
.contains("does not contain an `id` string or i64 field")
);
});
}
#[test]
fn graph_relation_roundtrip_passes() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItRecordUser>::delete_all()
.await
.expect("delete_all should succeed");
let a = ItRecordUser {
id: RecordId::new("it_record_user", "a"),
name: "A".to_owned(),
};
let b = ItRecordUser {
id: RecordId::new("it_record_user", "b"),
name: "B".to_owned(),
};
Repo::<ItRecordUser>::create_at(a.id.clone(), a.clone())
.await
.expect("create a should succeed");
Repo::<ItRecordUser>::create_at(b.id.clone(), b.clone())
.await
.expect("create b should succeed");
let rel = relation_name::<ItFollowsRel>();
GraphRepo::relate_at(a.id.clone(), b.id.clone(), rel)
.await
.expect("relate should succeed");
let outs = GraphRepo::out_ids(a.id.clone(), rel, "it_record_user")
.await
.expect("out_ids should succeed");
assert!(outs.iter().any(|id| id == &b.id));
GraphRepo::unrelate_at(a.id.clone(), b.id.clone(), rel)
.await
.expect("unrelate should succeed");
let outs_after = GraphRepo::out_ids(a.id.clone(), rel, "it_record_user")
.await
.expect("out_ids after unrelate should succeed");
assert!(!outs_after.iter().any(|id| id == &b.id));
});
}
#[test]
fn graph_back_relation_roundtrip_passes() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItRecordUser>::delete_all()
.await
.expect("delete_all should succeed");
let a = ItRecordUser {
id: RecordId::new("it_record_user", "back-a"),
name: "A".to_owned(),
};
let b = ItRecordUser {
id: RecordId::new("it_record_user", "back-b"),
name: "B".to_owned(),
};
Repo::<ItRecordUser>::create_at(a.id.clone(), a.clone())
.await
.expect("create a should succeed");
Repo::<ItRecordUser>::create_at(b.id.clone(), b.clone())
.await
.expect("create b should succeed");
let rel = relation_name::<ItFollowsRel>();
GraphRepo::back_relate_at(a.id.clone(), b.id.clone(), rel)
.await
.expect("back_relate should succeed");
let outs = GraphRepo::out_ids(b.id.clone(), rel, "it_record_user")
.await
.expect("reverse out_ids should succeed");
assert!(outs.iter().any(|id| id == &a.id));
let ins = GraphRepo::in_ids(a.id.clone(), rel, "it_record_user")
.await
.expect("reverse in_ids should succeed");
assert!(ins.iter().any(|id| id == &b.id));
GraphRepo::unrelate_at(b.id.clone(), a.id.clone(), rel)
.await
.expect("reverse unrelate should succeed");
let outs_after = GraphRepo::out_ids(b.id.clone(), rel, "it_record_user")
.await
.expect("reverse out_ids after unrelate should succeed");
assert!(!outs_after.iter().any(|id| id == &a.id));
});
}
#[test]
fn graph_instance_api_accepts_store_models_without_has_id() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItLookupSource>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItLookupTarget>::delete_all()
.await
.expect("delete_all should succeed");
let source = Repo::<ItLookupSource>::create(ItLookupSource {
name: "source-a".to_owned(),
})
.await
.expect("source create should succeed");
let target = Repo::<ItLookupTarget>::create(ItLookupTarget {
code: "target-b".to_owned(),
})
.await
.expect("target create should succeed");
source
.relate::<ItFollowsRel, _>(&target)
.await
.expect("instance relate should resolve record ids");
let source_id = Repo::<ItLookupSource>::find_unique_id_for(&source)
.await
.expect("source lookup should succeed");
let target_id = Repo::<ItLookupTarget>::find_unique_id_for(&target)
.await
.expect("target lookup should succeed");
let outs = ItFollowsRel::out_ids(&source, "it_lookup_target")
.await
.expect("out_ids should succeed");
assert!(outs.iter().any(|id| id == &target_id));
source
.unrelate::<ItFollowsRel, _>(&target)
.await
.expect("instance unrelate should resolve record ids");
let outs_after = ItFollowsRel::out_ids(&source, "it_lookup_target")
.await
.expect("out_ids after unrelate should succeed");
assert!(!outs_after.iter().any(|id| id == &target_id));
let _ = source_id;
});
}
#[test]
fn store_derived_graph_mutators_support_string_relation_names() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItLookupSource>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItLookupTarget>::delete_all()
.await
.expect("delete_all should succeed");
let source = Repo::<ItLookupSource>::create(ItLookupSource {
name: "string-rel-source".to_owned(),
})
.await
.expect("source create should succeed");
let target = Repo::<ItLookupTarget>::create(ItLookupTarget {
code: "string-rel-target".to_owned(),
})
.await
.expect("target create should succeed");
source
.relate_by_name(&target, "it_follows_rel")
.await
.expect("relate_by_name should succeed");
let target_id = target
.resolve_record_id()
.await
.expect("target id should resolve");
let source_id = source
.resolve_record_id()
.await
.expect("source id should resolve");
let outgoing = source
.outgoing_ids("it_follows_rel")
.await
.expect("outgoing_ids should succeed");
assert_eq!(outgoing, vec![target_id.clone()]);
source
.unrelate_by_name(&target, "it_follows_rel")
.await
.expect("unrelate_by_name should succeed");
let outgoing_after = source
.outgoing_ids("it_follows_rel")
.await
.expect("outgoing_ids after unrelate should succeed");
assert!(outgoing_after.is_empty());
source
.back_relate_by_name(&target, "it_follows_rel")
.await
.expect("back_relate_by_name should succeed");
let incoming = source
.incoming_ids("it_follows_rel")
.await
.expect("incoming_ids should succeed");
assert_eq!(incoming, vec![target_id.clone()]);
let target_outgoing = target
.outgoing_ids("it_follows_rel")
.await
.expect("target outgoing_ids should succeed");
assert_eq!(target_outgoing, vec![source_id]);
});
}
#[test]
fn store_derived_graph_accessors_support_ids_rows_and_counts() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItLookupSource>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItLookupTarget>::delete_all()
.await
.expect("delete_all should succeed");
let source = Repo::<ItLookupSource>::create(ItLookupSource {
name: "store-graph-source".to_owned(),
})
.await
.expect("source create should succeed");
let peer_source = Repo::<ItLookupSource>::create(ItLookupSource {
name: "store-graph-peer".to_owned(),
})
.await
.expect("peer source create should succeed");
let target = Repo::<ItLookupTarget>::create(ItLookupTarget {
code: "store-graph-target".to_owned(),
})
.await
.expect("target create should succeed");
source
.relate::<ItFollowsRel, _>(&peer_source)
.await
.expect("source -> peer relate should succeed");
source
.relate::<ItFollowsRel, _>(&target)
.await
.expect("source -> target relate should succeed");
let rel = relation_name::<ItFollowsRel>();
let source_id = source
.resolve_record_id()
.await
.expect("source id should resolve");
let peer_source_id = peer_source
.resolve_record_id()
.await
.expect("peer source id should resolve");
let target_id = target
.resolve_record_id()
.await
.expect("target id should resolve");
let outgoing_ids = source
.outgoing_ids(rel)
.await
.expect("outgoing ids should succeed");
assert_eq!(outgoing_ids.len(), 2);
assert!(outgoing_ids.iter().any(|id| id == &peer_source_id));
assert!(outgoing_ids.iter().any(|id| id == &target_id));
let outgoing_targets = source
.outgoing::<ItLookupTarget>(rel)
.await
.expect("typed outgoing target rows should succeed");
assert_eq!(outgoing_targets.len(), 1);
assert_eq!(outgoing_targets[0].code, "store-graph-target");
let outgoing_sources = source
.outgoing::<ItLookupSource>(rel)
.await
.expect("typed outgoing source rows should succeed");
assert_eq!(outgoing_sources.len(), 1);
assert_eq!(outgoing_sources[0].name, "store-graph-peer");
assert_eq!(
source
.outgoing_count(rel)
.await
.expect("outgoing count should succeed"),
2
);
assert_eq!(
source
.outgoing_count_as::<ItLookupTarget>(rel)
.await
.expect("typed outgoing target count should succeed"),
1
);
assert_eq!(
source
.outgoing_count_as::<ItLookupSource>(rel)
.await
.expect("typed outgoing source count should succeed"),
1
);
let incoming_ids = target
.incoming_ids(rel)
.await
.expect("incoming ids should succeed");
assert_eq!(incoming_ids.len(), 1);
assert_eq!(incoming_ids[0], source_id);
let incoming_sources = target
.incoming::<ItLookupSource>(rel)
.await
.expect("typed incoming source rows should succeed");
assert_eq!(incoming_sources.len(), 1);
assert_eq!(incoming_sources[0].name, "store-graph-source");
assert_eq!(
target
.incoming_count(rel)
.await
.expect("incoming count should succeed"),
1
);
assert_eq!(
target
.incoming_count_as::<ItLookupSource>(rel)
.await
.expect("typed incoming source count should succeed"),
1
);
assert_eq!(
target
.incoming_count_as::<ItLookupTarget>(rel)
.await
.expect("typed incoming target count should succeed"),
0
);
});
}
#[test]
fn graph_instance_api_fails_when_source_store_lookup_is_ambiguous() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItFallbackLookup>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItLookupTarget>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItFallbackLookup>::create(ItFallbackLookup {
name: "dup-source".to_owned(),
note: Some("same".to_owned()),
})
.await
.expect("first source create should succeed");
Repo::<ItFallbackLookup>::create(ItFallbackLookup {
name: "dup-source".to_owned(),
note: Some("same".to_owned()),
})
.await
.expect("second source create should succeed");
let target = Repo::<ItLookupTarget>::create(ItLookupTarget {
code: "ambiguous-target".to_owned(),
})
.await
.expect("target create should succeed");
let err = ItFallbackLookup {
name: "dup-source".to_owned(),
note: Some("same".to_owned()),
}
.relate::<ItFollowsRel, _>(&target)
.await
.expect_err("ambiguous source should fail graph relate");
assert!(err.to_string().contains("multiple records"), "{err}");
});
}
#[test]
fn graph_instance_api_fails_when_target_store_lookup_is_missing() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItLookupSource>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItLookupTarget>::delete_all()
.await
.expect("delete_all should succeed");
let source = Repo::<ItLookupSource>::create(ItLookupSource {
name: "good-source".to_owned(),
})
.await
.expect("source create should succeed");
let missing_target = ItLookupTarget {
code: "missing-target".to_owned(),
};
let err = source
.relate::<ItFollowsRel, _>(&missing_target)
.await
.expect_err("missing target should fail graph relate");
assert!(err.to_string().contains("Record not found"), "{err}");
});
}
#[test]
fn relation_out_ids_fails_when_store_input_is_ambiguous() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItFallbackLookup>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItFallbackLookup>::create(ItFallbackLookup {
name: "dup-out".to_owned(),
note: Some("same".to_owned()),
})
.await
.expect("first create should succeed");
Repo::<ItFallbackLookup>::create(ItFallbackLookup {
name: "dup-out".to_owned(),
note: Some("same".to_owned()),
})
.await
.expect("second create should succeed");
let err = ItFollowsRel::out_ids(
&ItFallbackLookup {
name: "dup-out".to_owned(),
note: Some("same".to_owned()),
},
"it_lookup_target",
)
.await
.expect_err("ambiguous source should fail out_ids");
assert!(err.to_string().contains("multiple records"), "{err}");
});
}
#[test]
fn relation_type_api_accepts_store_models_without_has_id() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItLookupSource>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItLookupTarget>::delete_all()
.await
.expect("delete_all should succeed");
let source = Repo::<ItLookupSource>::create(ItLookupSource {
name: "source-type".to_owned(),
})
.await
.expect("source create should succeed");
let target = Repo::<ItLookupTarget>::create(ItLookupTarget {
code: "target-type".to_owned(),
})
.await
.expect("target create should succeed");
ItFollowsRel::relate(&source, &target)
.await
.expect("type relate should resolve record ids");
let target_id = Repo::<ItLookupTarget>::find_unique_id_for(&target)
.await
.expect("target lookup should succeed");
let outs = ItFollowsRel::out_ids(&source, "it_lookup_target")
.await
.expect("type out_ids should succeed");
assert!(outs.iter().any(|id| id == &target_id));
});
}
#[test]
fn relation_type_api_accepts_mixed_sensitive_store_models_without_exposing_encrypted_types() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItSensitiveLookupSource>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItSensitiveLookupTarget>::delete_all()
.await
.expect("delete_all should succeed");
let source = Repo::<ItSensitiveLookupSource>::create(ItSensitiveLookupSource {
alias: "sensitive-type-source".to_owned(),
secret: "source-secret".to_owned(),
note: Some("source-note".to_owned()),
})
.await
.expect("source create should succeed");
let target = Repo::<ItSensitiveLookupTarget>::create(ItSensitiveLookupTarget {
code: "sensitive-type-target".to_owned(),
secret: "target-secret".to_owned(),
note: Some("target-note".to_owned()),
})
.await
.expect("target create should succeed");
assert_eq!(ItSensitiveLookupSource::lookup_fields(), &["alias"]);
assert_eq!(ItSensitiveLookupTarget::lookup_fields(), &["code"]);
ItFollowsRel::relate(&source, &target)
.await
.expect("type relate should resolve mixed sensitive models through legal non-secure lookup fields");
let target_id = target
.resolve_record_id()
.await
.expect("target resolve_record_id should use the legal non-secure lookup field");
let outs = ItFollowsRel::out_ids(&source, ItSensitiveLookupTarget::table_name())
.await
.expect("type out_ids should succeed for mixed sensitive models");
assert!(outs.iter().any(|id| id == &target_id));
ItFollowsRel::unrelate(&source, &target)
.await
.expect("type unrelate should resolve mixed sensitive models through legal non-secure lookup fields");
let outs_after = ItFollowsRel::out_ids(&source, ItSensitiveLookupTarget::table_name())
.await
.expect("type out_ids should still succeed after unrelate");
assert!(!outs_after.iter().any(|id| id == &target_id));
});
}
#[test]
fn relation_derive_registers_default_and_overridden_names() {
assert_eq!(relation_name::<ItFollowsRel>(), "it_follows_rel");
assert_eq!(
relation_name::<AutoNamedTestRelation>(),
"auto_named_test_relation"
);
assert_eq!(
relation_name::<NamedFieldTestRelation>(),
"named_field_test_relation"
);
}
#[test]
fn relation_type_api_roundtrip_passes() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItRecordUser>::delete_all()
.await
.expect("delete_all should succeed");
let a = ItRecordUser {
id: RecordId::new("it_record_user", "type-a"),
name: "A".to_owned(),
};
let b = ItRecordUser {
id: RecordId::new("it_record_user", "type-b"),
name: "B".to_owned(),
};
Repo::<ItRecordUser>::create_at(a.id.clone(), a.clone())
.await
.expect("create a should succeed");
Repo::<ItRecordUser>::create_at(b.id.clone(), b.clone())
.await
.expect("create b should succeed");
ItFollowsRel::relate(&a, &b)
.await
.expect("type-level relate should succeed");
let outs = ItFollowsRel::out_ids(&a, "it_record_user")
.await
.expect("type-level out_ids should succeed");
assert!(outs.iter().any(|id| id == &b.id));
ItFollowsRel::unrelate(&a, &b)
.await
.expect("type-level unrelate should succeed");
let outs_after = ItFollowsRel::out_ids(&a, "it_record_user")
.await
.expect("type-level out_ids after unrelate should succeed");
assert!(!outs_after.iter().any(|id| id == &b.id));
});
}
#[test]
fn relation_type_api_back_roundtrip_passes() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItRecordUser>::delete_all()
.await
.expect("delete_all should succeed");
let a = ItRecordUser {
id: RecordId::new("it_record_user", "type-back-a"),
name: "A".to_owned(),
};
let b = ItRecordUser {
id: RecordId::new("it_record_user", "type-back-b"),
name: "B".to_owned(),
};
Repo::<ItRecordUser>::create_at(a.id.clone(), a.clone())
.await
.expect("create a should succeed");
Repo::<ItRecordUser>::create_at(b.id.clone(), b.clone())
.await
.expect("create b should succeed");
ItFollowsRel::back_relate(&a, &b)
.await
.expect("type-level back_relate should succeed");
let outs = ItFollowsRel::out_ids(&b, "it_record_user")
.await
.expect("type-level reverse out_ids should succeed");
assert!(outs.iter().any(|id| id == &a.id));
let ins = ItFollowsRel::in_ids(&a, "it_record_user")
.await
.expect("type-level reverse in_ids should succeed");
assert!(ins.iter().any(|id| id == &b.id));
ItFollowsRel::unrelate(&b, &a)
.await
.expect("type-level reverse unrelate should succeed");
let outs_after = ItFollowsRel::out_ids(&b, "it_record_user")
.await
.expect("type-level reverse out_ids after unrelate should succeed");
assert!(!outs_after.iter().any(|id| id == &a.id));
});
}
#[test]
fn graph_instance_api_roundtrip_passes() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItRecordUser>::delete_all()
.await
.expect("delete_all should succeed");
let a = ItRecordUser {
id: RecordId::new("it_record_user", "inst-a"),
name: "A".to_owned(),
};
let b = ItRecordUser {
id: RecordId::new("it_record_user", "inst-b"),
name: "B".to_owned(),
};
Repo::<ItRecordUser>::create_at(a.id.clone(), a.clone())
.await
.expect("create a should succeed");
Repo::<ItRecordUser>::create_at(b.id.clone(), b.clone())
.await
.expect("create b should succeed");
a.relate::<ItFollowsRel, _>(&b)
.await
.expect("instance relate should succeed");
let outs = ItFollowsRel::out_ids(&a, "it_record_user")
.await
.expect("out_ids should succeed");
assert!(outs.iter().any(|id| id == &b.id));
a.unrelate::<ItFollowsRel, _>(&b)
.await
.expect("instance unrelate should succeed");
let outs_after = ItFollowsRel::out_ids(&a, "it_record_user")
.await
.expect("out_ids after instance unrelate should succeed");
assert!(!outs_after.iter().any(|id| id == &b.id));
});
}
#[test]
fn graph_instance_api_back_roundtrip_passes() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItRecordUser>::delete_all()
.await
.expect("delete_all should succeed");
let a = ItRecordUser {
id: RecordId::new("it_record_user", "inst-back-a"),
name: "A".to_owned(),
};
let b = ItRecordUser {
id: RecordId::new("it_record_user", "inst-back-b"),
name: "B".to_owned(),
};
Repo::<ItRecordUser>::create_at(a.id.clone(), a.clone())
.await
.expect("create a should succeed");
Repo::<ItRecordUser>::create_at(b.id.clone(), b.clone())
.await
.expect("create b should succeed");
a.back_relate::<ItFollowsRel, _>(&b)
.await
.expect("instance back_relate should succeed");
let outs = ItFollowsRel::out_ids(&b, "it_record_user")
.await
.expect("instance reverse out_ids should succeed");
assert!(outs.iter().any(|id| id == &a.id));
let ins = ItFollowsRel::in_ids(&a, "it_record_user")
.await
.expect("instance reverse in_ids should succeed");
assert!(ins.iter().any(|id| id == &b.id));
b.unrelate::<ItFollowsRel, _>(&a)
.await
.expect("instance reverse unrelate should succeed");
let outs_after = ItFollowsRel::out_ids(&b, "it_record_user")
.await
.expect("instance reverse out_ids after unrelate should succeed");
assert!(!outs_after.iter().any(|id| id == &a.id));
});
}
#[test]
fn inherent_record_model_api_roundtrip_passes() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
ItRecordUser::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItRecordUser>::create_at(
RecordId::new("it_record_user", "reload-me"),
ItRecordUser {
id: RecordId::new("it_record_user", "reload-me"),
name: "before".to_owned(),
},
)
.await
.expect("create_at should succeed");
let updated = Repo::<ItRecordUser>::update_at(
RecordId::new("it_record_user", "reload-me"),
ItRecordUser {
id: RecordId::new("it_record_user", "reload-me"),
name: "after".to_owned(),
},
)
.await
.expect("update_at should succeed");
let reloaded = ItRecordUser::get_record(updated.id.clone())
.await
.expect("get_record should succeed");
assert_eq!(reloaded.name, "after");
assert_eq!(reloaded.id, RecordId::new("it_record_user", "reload-me"));
Repo::<ItRecordUser>::delete_record(reloaded.id.clone())
.await
.expect("record delete should succeed");
});
}
#[test]
fn graph_relation_name_is_bound_as_identifier() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItRecordUser>::delete_all()
.await
.expect("delete_all should succeed");
let x = ItRecordUser {
id: RecordId::new("it_record_user", "x"),
name: "X".to_owned(),
};
let y = ItRecordUser {
id: RecordId::new("it_record_user", "y"),
name: "Y".to_owned(),
};
Repo::<ItRecordUser>::create_at(x.id.clone(), x.clone())
.await
.expect("create x should succeed");
Repo::<ItRecordUser>::create_at(y.id.clone(), y.clone())
.await
.expect("create y should succeed");
GraphRepo::relate_at(
x.id.clone(),
y.id.clone(),
"bad-name; DELETE it_record_user RETURN NONE;",
)
.await
.expect("relation name should be treated as bound identifier");
let selected_x = Repo::<ItRecordUser>::get_record(RecordId::new("it_record_user", "x"))
.await
.expect("x should still exist");
let selected_y = Repo::<ItRecordUser>::get_record(RecordId::new("it_record_user", "y"))
.await
.expect("y should still exist");
assert_eq!(selected_x.name, "X");
assert_eq!(selected_y.name, "Y");
});
}
#[test]
fn delete_target_string_bind_fails_but_table_bind_passes() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItRecordUser>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItRecordUser>::create_at(
RecordId::new("it_record_user", "z"),
ItRecordUser {
id: RecordId::new("it_record_user", "z"),
name: "Z".to_owned(),
},
)
.await
.expect("seed record should be created");
let db = get_db().expect("db should be initialized");
let bad_res = db
.query("DELETE $target RETURN NONE;")
.bind(("target", "it_record_user".to_owned()))
.await
.expect("query should execute");
let bad_err = bad_res
.check()
.expect_err("string bind should fail for DELETE target");
assert!(
bad_err
.to_string()
.contains("Cannot execute DELETE statement using value")
);
db.query("DELETE $target RETURN NONE;")
.bind(("target", Table::from("it_record_user")))
.await
.expect("query should execute")
.check()
.expect("table bind should pass for DELETE target");
});
}
#[test]
fn delete_record_query_accepts_bound_record_id() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItRecordUser>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItRecordUser>::create_at(
RecordId::new("it_record_user", "bound-delete"),
ItRecordUser {
id: RecordId::new("it_record_user", "bound-delete"),
name: "Bound Delete".to_owned(),
},
)
.await
.expect("seed record should be created");
Repo::<ItRecordUser>::delete_record(RecordId::new("it_record_user", "bound-delete"))
.await
.expect("bound record id delete should succeed");
let missing =
Repo::<ItRecordUser>::get_record(RecordId::new("it_record_user", "bound-delete"))
.await
.expect_err("deleted row should be gone");
assert!(matches!(
missing.downcast_ref::<DBError>(),
Some(DBError::NotFound)
));
});
}
#[test]
fn repo_delete_string_id_does_not_cross_table_boundary() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItStringUser>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItRecordUser>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItStringUser>::create(ItStringUser {
id: Id::from("it_record_user:cross-target"),
payload: "local string id".to_owned(),
})
.await
.expect("local string-shaped id should be created");
Repo::<ItRecordUser>::create_at(
RecordId::new("it_record_user", "cross-target"),
ItRecordUser {
id: RecordId::new("it_record_user", "cross-target"),
name: "foreign row".to_owned(),
},
)
.await
.expect("foreign record should be created");
Repo::<ItStringUser>::delete("it_record_user:cross-target")
.await
.expect("delete should treat string input as table-local");
let missing = Repo::<ItStringUser>::get("it_record_user:cross-target")
.await
.expect_err("local string-shaped row should be deleted");
assert!(matches!(
missing.downcast_ref::<DBError>(),
Some(DBError::NotFound)
));
let foreign =
Repo::<ItRecordUser>::get_record(RecordId::new("it_record_user", "cross-target"))
.await
.expect("foreign row should remain");
assert_eq!(foreign.name, "foreign row");
});
}
#[test]
fn delete_record_still_accepts_explicit_full_record_id() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItRecordUser>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItRecordUser>::create_at(
RecordId::new("it_record_user", "explicit-delete"),
ItRecordUser {
id: RecordId::new("it_record_user", "explicit-delete"),
name: "explicit delete".to_owned(),
},
)
.await
.expect("seed record should be created");
Repo::<ItRecordUser>::delete_record(RecordId::new("it_record_user", "explicit-delete"))
.await
.expect("explicit record delete should succeed");
let missing =
Repo::<ItRecordUser>::get_record(RecordId::new("it_record_user", "explicit-delete"))
.await
.expect_err("deleted row should be gone");
assert!(matches!(
missing.downcast_ref::<DBError>(),
Some(DBError::NotFound)
));
});
}
#[test]
fn transaction_runner_executes_and_returns_value() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
let stmt = TxStmt::new("RETURN $v;").bind("v", 42i64);
let mut res = run_tx(vec![stmt]).await.expect("tx should succeed");
let value: Option<i64> = res.take(0, 0).expect("take should decode value");
assert_eq!(value, Some(42));
});
}
#[test]
fn transaction_runner_returns_all_statement_results() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
let stmt1 = TxStmt::new("RETURN $v;").bind("v", 7i64);
let stmt2 = TxStmt::new("RETURN $v;").bind("v", 11i64);
let mut res = run_tx(vec![stmt1, stmt2]).await.expect("tx should succeed");
assert_eq!(res.len(), 2);
let first: Option<i64> = res.take(0, 0).expect("first statement should decode");
let second: Option<i64> = res.take(1, 0).expect("second statement should decode");
assert_eq!(first, Some(7));
assert_eq!(second, Some(11));
});
}
#[test]
fn raw_sql_stmt_binds_values_safely() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
let stmt = RawSqlStmt::new("RETURN $value;").bind("value", 99i64);
let value = query_bound_return::<i64>(stmt)
.await
.expect("bound raw sql should succeed");
assert_eq!(value, Some(99));
});
}
#[test]
fn store_sensitive_save_get_roundtrip_encrypts_at_rest() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItSensitiveProfile>::delete_all()
.await
.expect("delete_all should succeed");
let input = ItSensitiveProfile {
id: Id::from("s-save"),
alias: "alpha".to_owned(),
secret: "swordfish".to_owned(),
note: Some("memo".to_owned()),
};
let saved = ItSensitiveProfile::save(input.clone())
.await
.expect("save should succeed");
let loaded = ItSensitiveProfile::get("s-save")
.await
.expect("get should succeed");
assert_eq!(saved, input);
assert_eq!(loaded, input);
let raw = load_sensitive_profile_raw("s-save").await;
assert_sensitive_row_encrypted(&raw, "alpha", "swordfish", Some("memo"));
});
}
#[test]
fn store_sensitive_nested_child_shapes_roundtrip_with_plaintext_results_and_encrypted_rows() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItNestedSensitiveParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItOptionalNestedSensitiveParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItVecNestedSensitiveParent>::delete_all()
.await
.expect("delete_all should succeed");
let direct = ItNestedSensitiveParent {
id: Id::from("nested-sensitive-direct"),
alias: "direct-parent".to_owned(),
child: ItNestedSensitiveChild {
label: "direct-child".to_owned(),
secret: "direct-secret".to_owned(),
},
};
let optional_some = ItOptionalNestedSensitiveParent {
id: Id::from("nested-sensitive-optional-some"),
alias: "optional-parent".to_owned(),
child: Some(ItNestedSensitiveChild {
label: "optional-child".to_owned(),
secret: "optional-secret".to_owned(),
}),
};
let optional_none = ItOptionalNestedSensitiveParent {
id: Id::from("nested-sensitive-optional-none"),
alias: "optional-none-parent".to_owned(),
child: None,
};
let vec_parent = ItVecNestedSensitiveParent {
id: Id::from("nested-sensitive-vec"),
alias: "vec-parent".to_owned(),
children: vec![
ItNestedSensitiveChild {
label: "vec-a".to_owned(),
secret: "vec-secret-a".to_owned(),
},
ItNestedSensitiveChild {
label: "vec-b".to_owned(),
secret: "vec-secret-b".to_owned(),
},
],
};
let saved_direct = ItNestedSensitiveParent::save(direct.clone())
.await
.expect("direct save should succeed");
let saved_optional = ItOptionalNestedSensitiveParent::save(optional_some.clone())
.await
.expect("optional some save should succeed");
let saved_optional_none = ItOptionalNestedSensitiveParent::save(optional_none.clone())
.await
.expect("optional none save should succeed");
let saved_vec = ItVecNestedSensitiveParent::save(vec_parent.clone())
.await
.expect("vec save should succeed");
assert_eq!(saved_direct, direct);
assert_eq!(
ItNestedSensitiveParent::get("nested-sensitive-direct")
.await
.expect("direct get should succeed"),
direct
);
assert_eq!(saved_optional, optional_some);
assert_eq!(
ItOptionalNestedSensitiveParent::get("nested-sensitive-optional-some")
.await
.expect("optional some get should succeed"),
optional_some
);
assert_eq!(saved_optional_none, optional_none);
assert_eq!(
ItOptionalNestedSensitiveParent::get("nested-sensitive-optional-none")
.await
.expect("optional none get should succeed"),
optional_none
);
assert_eq!(saved_vec, vec_parent);
assert_eq!(
ItVecNestedSensitiveParent::get("nested-sensitive-vec")
.await
.expect("vec get should succeed"),
vec_parent
);
let listed_direct = ItNestedSensitiveParent::list()
.await
.expect("direct list should succeed");
assert_eq!(listed_direct, vec![direct.clone()]);
let listed_optional = ItOptionalNestedSensitiveParent::list()
.await
.expect("optional list should succeed");
assert!(listed_optional.contains(&optional_some));
assert!(listed_optional.contains(&optional_none));
let listed_vec = ItVecNestedSensitiveParent::list()
.await
.expect("vec list should succeed");
assert_eq!(listed_vec, vec![vec_parent.clone()]);
let raw_direct = load_nested_sensitive_parent_raw("nested-sensitive-direct").await;
assert_eq!(raw_direct.alias, "direct-parent");
assert_nested_sensitive_child_encrypted(&raw_direct.child, "direct-child", "direct-secret");
let raw_optional_some =
load_optional_nested_sensitive_parent_raw("nested-sensitive-optional-some").await;
assert_eq!(raw_optional_some.alias, "optional-parent");
let raw_optional_child = raw_optional_some
.child
.expect("optional some raw child should exist");
assert_nested_sensitive_child_encrypted(
&raw_optional_child,
"optional-child",
"optional-secret",
);
let raw_optional_none =
load_optional_nested_sensitive_parent_raw("nested-sensitive-optional-none").await;
assert_eq!(raw_optional_none.alias, "optional-none-parent");
assert!(raw_optional_none.child.is_none());
let raw_vec = load_vec_nested_sensitive_parent_raw("nested-sensitive-vec").await;
assert_eq!(raw_vec.alias, "vec-parent");
assert_eq!(raw_vec.children.len(), 2);
assert_nested_sensitive_child_encrypted(&raw_vec.children[0], "vec-a", "vec-secret-a");
assert_nested_sensitive_child_encrypted(&raw_vec.children[1], "vec-b", "vec-secret-b");
});
}
#[test]
fn store_sensitive_nested_field_account_overrides_keep_sibling_contexts_isolated() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItNestedOverrideParent>::delete_all()
.await
.expect("delete_all should succeed");
let parent = ItNestedOverrideParent {
id: Id::from("nested-override-parent"),
alias: "override-parent".to_owned(),
left: ItNestedOverrideLeaf {
label: "left-leaf".to_owned(),
secret: "left-secret".to_owned(),
},
right: ItNestedOverrideLeaf {
label: "right-leaf".to_owned(),
secret: "right-secret".to_owned(),
},
};
let saved = ItNestedOverrideParent::save(parent.clone())
.await
.expect("save should succeed");
let loaded = ItNestedOverrideParent::get("nested-override-parent")
.await
.expect("get should decrypt with generated override contexts");
assert_eq!(saved, parent);
assert_eq!(loaded, parent);
let raw = load_nested_override_parent_raw("nested-override-parent").await;
assert_eq!(raw.alias, "override-parent");
assert_eq!(raw.left.label, "left-leaf");
assert_eq!(raw.right.label, "right-leaf");
assert_ne!(raw.left.secret, b"left-secret");
assert_ne!(raw.right.secret, b"right-secret");
let left_ctx = appdb::crypto::resolve_crypto_context_for::<
AppdbSensitiveFieldTagItNestedOverrideParentLeft,
>()
.expect("left override context should resolve");
let right_ctx = appdb::crypto::resolve_crypto_context_for::<
AppdbSensitiveFieldTagItNestedOverrideParentRight,
>()
.expect("right override context should resolve");
assert_eq!(
ItNestedOverrideLeaf::decrypt_with_context(&raw.left, &left_ctx)
.expect("left leaf should decrypt only with left context"),
parent.left
);
assert_eq!(
ItNestedOverrideLeaf::decrypt_with_context(&raw.right, &right_ctx)
.expect("right leaf should decrypt only with right context"),
parent.right
);
assert!(matches!(
ItNestedOverrideLeaf::decrypt_with_context(&raw.left, &right_ctx),
Err(appdb::crypto::CryptoError::Decrypt)
));
assert!(matches!(
ItNestedOverrideLeaf::decrypt_with_context(&raw.right, &left_ctx),
Err(appdb::crypto::CryptoError::Decrypt)
));
});
}
#[test]
fn nested_ref_cross_area_regressions() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItNestedParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedIdChild>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedOptionalParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedLookupChild>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItSensitiveProfile>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItProfile>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItFallbackLookup>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItLookupSource>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItLookupTarget>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItSensitiveLookupSource>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItSensitiveLookupTarget>::delete_all()
.await
.expect("delete_all should succeed");
let direct_input = ItNestedParent {
id: Id::from("cross-area-parent"),
child: ItNestedIdChild {
id: Id::from("cross-area-child"),
name: "cross-alpha".to_owned(),
},
};
let seeded_child = Repo::<ItNestedIdChild>::create(direct_input.child.clone())
.await
.expect("direct child seed should succeed");
assert_eq!(
seeded_child
.resolve_record_id()
.await
.expect("seeded direct child should resolve"),
RecordId::new(ItNestedIdChild::table_name(), "cross-area-child")
);
let saved_parent = ItNestedParent::save(direct_input.clone())
.await
.expect("nested parent save should succeed");
let loaded_parent = ItNestedParent::get("cross-area-parent")
.await
.expect("nested parent get should succeed");
let raw_parent = load_nested_parent_raw("cross-area-parent").await;
assert_eq!(saved_parent, direct_input);
assert_eq!(loaded_parent, direct_input);
assert_eq!(
raw_parent.child,
RecordId::new(ItNestedIdChild::table_name(), "cross-area-child")
);
let parent_row_json =
serde_json::to_value(&raw_parent).expect("nested parent raw row should serialize");
let parent_fields = parent_row_json
.as_object()
.expect("nested parent raw row should be an object");
assert!(!parent_fields.contains_key("name"));
let sensitive = ItSensitiveProfile::save(ItSensitiveProfile {
id: Id::from("cross-sensitive"),
alias: "cross-sensitive-alias".to_owned(),
secret: "cross-sensitive-secret".to_owned(),
note: Some("cross-sensitive-note".to_owned()),
})
.await
.expect("sensitive save should succeed");
assert_eq!(sensitive.secret, "cross-sensitive-secret");
let raw_sensitive = load_sensitive_profile_raw("cross-sensitive").await;
assert_sensitive_row_encrypted(
&raw_sensitive,
"cross-sensitive-alias",
"cross-sensitive-secret",
Some("cross-sensitive-note"),
);
Repo::<ItFallbackLookup>::create(ItFallbackLookup {
name: "dup-cross".to_owned(),
note: Some("same".to_owned()),
})
.await
.expect("first duplicate child create should succeed");
Repo::<ItFallbackLookup>::create(ItFallbackLookup {
name: "dup-cross".to_owned(),
note: Some("same".to_owned()),
})
.await
.expect("second duplicate child create should succeed");
let child_ids_before = Repo::<ItFallbackLookup>::list_record_ids()
.await
.expect("duplicate child ids before relation checks should load");
let ambiguous_err = ItFallbackLookup {
name: "dup-cross".to_owned(),
note: Some("same".to_owned()),
}
.resolve_record_id()
.await
.expect_err("ambiguous child lookup should fail");
assert!(
ambiguous_err.to_string().contains("multiple records"),
"{ambiguous_err}"
);
let child_ids_after = Repo::<ItFallbackLookup>::list_record_ids()
.await
.expect("duplicate child ids after relation checks should load");
assert_eq!(child_ids_after, child_ids_before);
let plain_created_id = Repo::<ItProfile>::create_return_id(ItProfile {
id: Id::from("cross-plain-create-return-id"),
name: "cross-plain".to_owned(),
note: Some("works".to_owned()),
})
.await
.expect("plain create_return_id should succeed");
assert_eq!(
plain_created_id,
RecordId::new(ItProfile::table_name(), "cross-plain-create-return-id")
);
let guarded_err = Repo::<ItSensitiveProfile>::create_return_id(ItSensitiveProfile {
id: Id::from("cross-sensitive-create-return-id"),
alias: "guarded".to_owned(),
secret: "blocked".to_owned(),
note: Some("nope".to_owned()),
})
.await
.expect_err("sensitive create_return_id should remain guarded");
assert!(
guarded_err
.to_string()
.contains("does not support create_return_id")
);
let source = Repo::<ItLookupSource>::create(ItLookupSource {
name: "cross-source".to_owned(),
})
.await
.expect("relation source create should succeed");
let target = Repo::<ItLookupTarget>::create(ItLookupTarget {
code: "cross-target".to_owned(),
})
.await
.expect("relation target create should succeed");
ItFollowsRel::relate(&source, &target)
.await
.expect("relation helper should succeed");
let target_id = target
.resolve_record_id()
.await
.expect("relation target should resolve");
let outs = ItFollowsRel::out_ids(&source, ItLookupTarget::table_name())
.await
.expect("relation out_ids should succeed");
assert!(outs.iter().any(|id| id == &target_id));
let relation_ambiguous_err = ItFollowsRel::out_ids(
&ItFallbackLookup {
name: "dup-cross".to_owned(),
note: Some("same".to_owned()),
},
ItLookupTarget::table_name(),
)
.await
.expect_err("relation helper should preserve ambiguous error path");
assert!(
relation_ambiguous_err
.to_string()
.contains("multiple records"),
"{relation_ambiguous_err}"
);
let missing_target_err = source
.relate::<ItFollowsRel, _>(&ItLookupTarget {
code: "cross-missing-target".to_owned(),
})
.await
.expect_err("relation helper should preserve missing target error path");
assert!(
missing_target_err.to_string().contains("Record not found"),
"{missing_target_err}"
);
let sensitive_source = Repo::<ItSensitiveLookupSource>::create(ItSensitiveLookupSource {
alias: "cross-sensitive-source".to_owned(),
secret: "cross-sensitive-source-secret".to_owned(),
note: Some("cross-sensitive-source-note".to_owned()),
})
.await
.expect("sensitive relation source create should succeed");
let sensitive_target = Repo::<ItSensitiveLookupTarget>::create(ItSensitiveLookupTarget {
code: "cross-sensitive-target".to_owned(),
secret: "cross-sensitive-target-secret".to_owned(),
note: Some("cross-sensitive-target-note".to_owned()),
})
.await
.expect("sensitive relation target create should succeed");
ItFollowsRel::relate(&sensitive_source, &sensitive_target)
.await
.expect("mixed sensitive relation helper should succeed");
let sensitive_target_id = sensitive_target
.resolve_record_id()
.await
.expect("sensitive target should resolve via legal lookup");
let sensitive_outs =
ItFollowsRel::out_ids(&sensitive_source, ItSensitiveLookupTarget::table_name())
.await
.expect("mixed sensitive relation out_ids should succeed");
assert!(sensitive_outs.iter().any(|id| id == &sensitive_target_id));
});
}
#[test]
fn nested_foreign_models_roundtrip_through_get() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
ensure_tables_exist(&[
ItNestedForeignLeaf::table_name(),
ItNestedForeignBranch::table_name(),
ItNestedForeignRoot::table_name(),
])
.await;
Repo::<ItNestedForeignRoot>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedForeignBranch>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedForeignLeaf>::delete_all()
.await
.expect("delete_all should succeed");
let input = ItNestedForeignRoot {
id: Id::from("nested-foreign-root"),
branch: ItNestedForeignBranch {
id: Id::from("nested-foreign-branch"),
leaf: ItNestedForeignLeaf {
id: Id::from("nested-foreign-leaf"),
code: "nested-leaf-code".to_owned(),
},
},
};
let saved = ItNestedForeignRoot::save(input.clone())
.await
.expect("save should succeed for nested foreign graph");
let loaded = ItNestedForeignRoot::get("nested-foreign-root")
.await
.expect("get should hydrate nested foreign graph");
let root_raw = load_nested_foreign_root_raw("nested-foreign-root").await;
let branch_raw = load_nested_foreign_branch_raw("nested-foreign-branch").await;
assert_eq!(saved, input);
assert_eq!(loaded, input);
assert_eq!(
root_raw.branch,
RecordId::new(ItNestedForeignBranch::table_name(), "nested-foreign-branch")
);
assert_eq!(
branch_raw.leaf,
RecordId::new(ItNestedForeignLeaf::table_name(), "nested-foreign-leaf")
);
});
}
#[test]
fn nested_foreign_models_upsert_existing_child_branches() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
ensure_tables_exist(&[
ItNestedForeignLeaf::table_name(),
ItNestedForeignBranch::table_name(),
ItNestedForeignRoot::table_name(),
])
.await;
Repo::<ItNestedForeignRoot>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedForeignBranch>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedForeignLeaf>::delete_all()
.await
.expect("delete_all should succeed");
ItNestedForeignRoot::save(ItNestedForeignRoot {
id: Id::from("nested-existing-root"),
branch: ItNestedForeignBranch {
id: Id::from("nested-existing-branch"),
leaf: ItNestedForeignLeaf {
id: Id::from("nested-existing-leaf"),
code: "nested-existing-code".to_owned(),
},
},
})
.await
.expect("seed save should succeed");
let updated = ItNestedForeignRoot {
id: Id::from("nested-existing-root"),
branch: ItNestedForeignBranch {
id: Id::from("nested-existing-branch"),
leaf: ItNestedForeignLeaf {
id: Id::from("nested-existing-leaf"),
code: "nested-existing-code-updated".to_owned(),
},
},
};
let saved = ItNestedForeignRoot::save(updated.clone())
.await
.expect("save should upsert existing nested foreign branches");
let loaded = ItNestedForeignRoot::get("nested-existing-root")
.await
.expect("get should hydrate updated nested foreign graph");
let branch_raw = load_nested_foreign_branch_raw("nested-existing-branch").await;
let leaf = ItNestedForeignLeaf::get("nested-existing-leaf")
.await
.expect("leaf should load after nested upsert");
assert_eq!(saved, updated);
assert_eq!(loaded, updated);
assert_eq!(
branch_raw.leaf,
RecordId::new(ItNestedForeignLeaf::table_name(), "nested-existing-leaf")
);
assert_eq!(leaf.code, "nested-existing-code-updated");
});
}
#[test]
fn raw_query_string_record_links_decode_into_foreign_models() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
ensure_tables_exist(&[
ItNestedForeignLeaf::table_name(),
ItNestedForeignBranch::table_name(),
ItNestedForeignRoot::table_name(),
])
.await;
Repo::<ItNestedForeignRoot>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedForeignBranch>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedForeignLeaf>::delete_all()
.await
.expect("delete_all should succeed");
let leaf_sql = format!(
"CREATE ONLY {}:`raw-link-leaf` CONTENT {{ code: 'raw-link-code' }};",
ItNestedForeignLeaf::table_name()
);
query_bound_checked(RawSqlStmt::new(leaf_sql))
.await
.expect("leaf seed query should succeed");
let branch_sql = format!(
"CREATE ONLY {}:`raw-link-branch` CONTENT {{ leaf: '{}:`raw-link-leaf`' }};",
ItNestedForeignBranch::table_name(),
ItNestedForeignLeaf::table_name()
);
query_bound_checked(RawSqlStmt::new(branch_sql))
.await
.expect("branch raw string record link query should succeed");
let root_sql = format!(
"CREATE ONLY {}:`raw-link-root` CONTENT {{ branch: '{}:`raw-link-branch`' }};",
ItNestedForeignRoot::table_name(),
ItNestedForeignBranch::table_name()
);
query_bound_checked(RawSqlStmt::new(root_sql))
.await
.expect("root raw string record link query should succeed");
let loaded = ItNestedForeignRoot::get("raw-link-root")
.await
.expect("get should hydrate raw string record links through foreign models");
assert_eq!(
loaded,
ItNestedForeignRoot {
id: Id::from("raw-link-root"),
branch: ItNestedForeignBranch {
id: Id::from("raw-link-branch"),
leaf: ItNestedForeignLeaf {
id: Id::from("raw-link-leaf"),
code: "raw-link-code".to_owned(),
},
},
}
);
});
}
#[test]
fn decode_raw_row_helper_accepts_string_and_object_record_links() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
ensure_tables_exist(&[
ItNestedForeignLeaf::table_name(),
ItNestedForeignBranch::table_name(),
ItNestedForeignRoot::table_name(),
])
.await;
Repo::<ItNestedForeignRoot>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedForeignBranch>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedForeignLeaf>::delete_all()
.await
.expect("delete_all should succeed");
let leaf_sql = format!(
"CREATE ONLY {}:`decode-helper-leaf` CONTENT {{ code: 'decode-helper-code' }};",
ItNestedForeignLeaf::table_name()
);
query_bound_checked(RawSqlStmt::new(leaf_sql))
.await
.expect("leaf seed query should succeed");
let branch_sql = format!(
"CREATE ONLY {}:`decode-helper-branch` CONTENT {{ leaf: {{ id: '{}:`decode-helper-leaf`' }} }};",
ItNestedForeignBranch::table_name(),
ItNestedForeignLeaf::table_name()
);
query_bound_checked(RawSqlStmt::new(branch_sql))
.await
.expect("branch object-form record link query should succeed");
let root_sql = format!(
"CREATE ONLY {}:`decode-helper-root` CONTENT {{ branch: '{}:`decode-helper-branch`' }};",
ItNestedForeignRoot::table_name(),
ItNestedForeignBranch::table_name()
);
query_bound_checked(RawSqlStmt::new(root_sql))
.await
.expect("root string-form record link query should succeed");
let expected = ItNestedForeignRoot {
id: Id::from("decode-helper-root"),
branch: ItNestedForeignBranch {
id: Id::from("decode-helper-branch"),
leaf: ItNestedForeignLeaf {
id: Id::from("decode-helper-leaf"),
code: "decode-helper-code".to_owned(),
},
},
};
let got = ItNestedForeignRoot::get("decode-helper-root")
.await
.expect("get should decode mixed string/object foreign links");
let got_record = ItNestedForeignRoot::get_record(RecordId::new(
ItNestedForeignRoot::table_name(),
"decode-helper-root",
))
.await
.expect("get_record should reuse the shared decode helper");
let listed = ItNestedForeignRoot::list()
.await
.expect("list should reuse the shared decode helper")
.into_iter()
.find(|row| row.id == Id::from("decode-helper-root"))
.expect("decode-helper root should be present in list results");
let limited = ItNestedForeignRoot::list_limit(10)
.await
.expect("list_limit should reuse the shared decode helper")
.into_iter()
.find(|row| row.id == Id::from("decode-helper-root"))
.expect("decode-helper root should be present in list_limit results");
assert_eq!(got, expected);
assert_eq!(got_record, expected);
assert_eq!(listed, expected);
assert_eq!(limited, expected);
});
}
#[test]
fn foreign_read_paths_are_consistent_across_get_variants() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
ensure_tables_exist(&[
ItNestedForeignLeaf::table_name(),
ItNestedForeignBranch::table_name(),
ItNestedForeignRoot::table_name(),
])
.await;
Repo::<ItNestedForeignRoot>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedForeignBranch>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedForeignLeaf>::delete_all()
.await
.expect("delete_all should succeed");
let target = ItNestedForeignRoot {
id: Id::from("consistent-root-target"),
branch: ItNestedForeignBranch {
id: Id::from("consistent-branch-target"),
leaf: ItNestedForeignLeaf {
id: Id::from("consistent-leaf-target"),
code: "consistent-target".to_owned(),
},
},
};
let other = ItNestedForeignRoot {
id: Id::from("consistent-root-other"),
branch: ItNestedForeignBranch {
id: Id::from("consistent-branch-other"),
leaf: ItNestedForeignLeaf {
id: Id::from("consistent-leaf-other"),
code: "consistent-other".to_owned(),
},
},
};
let saved = seed_nested_foreign_root_pair(other, target.clone()).await;
let got = ItNestedForeignRoot::get("consistent-root-target")
.await
.expect("get should succeed");
let got_record = ItNestedForeignRoot::get_record(RecordId::new(
ItNestedForeignRoot::table_name(),
"consistent-root-target",
))
.await
.expect("get_record should succeed");
let listed = ItNestedForeignRoot::list()
.await
.expect("list should succeed");
let listed_target = listed
.into_iter()
.find(|row| row.id == Id::from("consistent-root-target"))
.expect("target row should be present in list results");
let limited = ItNestedForeignRoot::list_limit(2)
.await
.expect("list_limit should succeed");
let limited_target = limited
.into_iter()
.find(|row| row.id == Id::from("consistent-root-target"))
.expect("target row should be present in list_limit results");
assert_eq!(saved, target);
assert_eq!(got, target);
assert_eq!(got_record, target);
assert_eq!(listed_target, target);
assert_eq!(limited_target, target);
});
}
#[test]
fn successful_foreign_save_matches_all_read_entrypoints() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
ensure_tables_exist(&[
ItNestedForeignLeaf::table_name(),
ItNestedForeignBranch::table_name(),
ItNestedForeignRoot::table_name(),
])
.await;
Repo::<ItNestedForeignRoot>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedForeignBranch>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedForeignLeaf>::delete_all()
.await
.expect("delete_all should succeed");
let other = ItNestedForeignRoot {
id: Id::from("save-return-root-other"),
branch: ItNestedForeignBranch {
id: Id::from("save-return-branch-other"),
leaf: ItNestedForeignLeaf {
id: Id::from("save-return-leaf-other"),
code: "save-return-code-other".to_owned(),
},
},
};
let input = ItNestedForeignRoot {
id: Id::from("save-return-root"),
branch: ItNestedForeignBranch {
id: Id::from("save-return-branch"),
leaf: ItNestedForeignLeaf {
id: Id::from("save-return-leaf"),
code: "save-return-code".to_owned(),
},
},
};
let saved = seed_nested_foreign_root_pair(other.clone(), input.clone()).await;
let got = ItNestedForeignRoot::get("save-return-root")
.await
.expect("get should succeed");
let got_record = ItNestedForeignRoot::get_record(RecordId::new(
ItNestedForeignRoot::table_name(),
"save-return-root",
))
.await
.expect("get_record should succeed");
let listed = ItNestedForeignRoot::list()
.await
.expect("list should succeed")
.into_iter()
.find(|row| row.id == Id::from("save-return-root"))
.expect("saved row should be present in list results");
let limited = ItNestedForeignRoot::list_limit(2)
.await
.expect("list_limit should succeed");
let limited_target = limited
.into_iter()
.find(|row| row.id == Id::from("save-return-root"))
.expect("saved row should be present in list_limit results");
let root_raw = load_nested_foreign_root_raw("save-return-root").await;
let branch_raw = load_nested_foreign_branch_raw("save-return-branch").await;
assert_ne!(
other.id, input.id,
"fixture should include a distinct second row"
);
assert_eq!(saved, input);
assert_eq!(got, input);
assert_eq!(got_record, input);
assert_eq!(listed, input);
assert_eq!(limited_target, input);
assert_eq!(
root_raw.branch,
RecordId::new(ItNestedForeignBranch::table_name(), "save-return-branch")
);
assert_eq!(
branch_raw.leaf,
RecordId::new(ItNestedForeignLeaf::table_name(), "save-return-leaf")
);
});
}
#[test]
fn relate_fields_are_stored_in_edge_tables_and_hydrated_on_read() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItRelateRoot>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItRelateLeaf>::delete_all()
.await
.expect("delete_all should succeed");
let first = ItRelateLeaf {
id: Id::from("relate-leaf-1"),
label: "first".to_owned(),
};
let second = ItRelateLeaf {
id: Id::from("relate-leaf-2"),
label: "second".to_owned(),
};
let third = ItRelateLeaf {
id: Id::from("relate-leaf-3"),
label: "third".to_owned(),
};
let initial = ItRelateRoot {
id: Id::from("relate-root"),
title: "alpha".to_owned(),
primary: first.clone(),
optional: Some(second.clone()),
items: vec![third.clone(), first.clone(), third.clone()],
};
let saved = ItRelateRoot::save(initial.clone())
.await
.expect("relate save should succeed");
let loaded = ItRelateRoot::get("relate-root")
.await
.expect("relate get should succeed");
let listed = ItRelateRoot::list()
.await
.expect("relate list should succeed")
.into_iter()
.find(|row| row.id == Id::from("relate-root"))
.expect("saved relate row should be present in list results");
let limited = ItRelateRoot::list_limit(5)
.await
.expect("relate list_limit should succeed")
.into_iter()
.find(|row| row.id == Id::from("relate-root"))
.expect("saved relate row should be present in list_limit results");
let raw_root = load_relate_root_raw("relate-root").await;
let primary_edges = load_relate_edges("it_relate_primary", "relate-root").await;
let optional_edges = load_relate_edges("it_relate_optional", "relate-root").await;
let item_edges = load_relate_edges("it_relate_many", "relate-root").await;
assert_eq!(saved, initial);
assert_eq!(loaded, initial);
assert_eq!(listed, initial);
assert_eq!(limited, initial);
let raw_root_object = raw_root
.as_object()
.expect("relate root raw row should be an object");
assert_eq!(
raw_root_object.get("title"),
Some(&serde_json::Value::String("alpha".to_owned()))
);
assert!(
!raw_root_object.contains_key("primary"),
"root row should not inline relate single field"
);
assert!(
!raw_root_object.contains_key("optional"),
"root row should not inline relate optional field"
);
assert!(
!raw_root_object.contains_key("items"),
"root row should not inline relate vec field"
);
assert_eq!(primary_edges.len(), 1);
assert_eq!(primary_edges[0].position, 0);
assert_eq!(
primary_edges[0].out,
RecordId::new(ItRelateLeaf::table_name(), "relate-leaf-1")
);
assert_eq!(optional_edges.len(), 1);
assert_eq!(optional_edges[0].position, 0);
assert_eq!(
optional_edges[0].out,
RecordId::new(ItRelateLeaf::table_name(), "relate-leaf-2")
);
assert_eq!(item_edges.len(), 3);
assert_eq!(item_edges[0].position, 0);
assert_eq!(item_edges[1].position, 1);
assert_eq!(item_edges[2].position, 2);
assert_eq!(
item_edges
.iter()
.map(|row| row.out.clone())
.collect::<Vec<_>>(),
vec![
RecordId::new(ItRelateLeaf::table_name(), "relate-leaf-3"),
RecordId::new(ItRelateLeaf::table_name(), "relate-leaf-1"),
RecordId::new(ItRelateLeaf::table_name(), "relate-leaf-3"),
]
);
let updated = ItRelateRoot {
id: Id::from("relate-root"),
title: "beta".to_owned(),
primary: third.clone(),
optional: None,
items: vec![second.clone(), first.clone()],
};
let updated_saved = ItRelateRoot::save(updated.clone())
.await
.expect("relate update should succeed");
let updated_loaded = ItRelateRoot::get("relate-root")
.await
.expect("relate get after update should succeed");
let updated_primary_edges = load_relate_edges("it_relate_primary", "relate-root").await;
let updated_optional_edges = load_relate_edges("it_relate_optional", "relate-root").await;
let updated_item_edges = load_relate_edges("it_relate_many", "relate-root").await;
assert_eq!(updated_saved, updated);
assert_eq!(updated_loaded, updated);
assert_eq!(updated_primary_edges.len(), 1);
assert_eq!(
updated_primary_edges[0].out,
RecordId::new(ItRelateLeaf::table_name(), "relate-leaf-3")
);
assert!(
updated_optional_edges.is_empty(),
"optional relate edges should be cleared when the field becomes None"
);
assert_eq!(
updated_item_edges
.iter()
.map(|row| (row.position, row.out.clone()))
.collect::<Vec<_>>(),
vec![
(
0,
RecordId::new(ItRelateLeaf::table_name(), "relate-leaf-2")
),
(
1,
RecordId::new(ItRelateLeaf::table_name(), "relate-leaf-1")
),
]
);
});
}
#[test]
fn relate_fields_roundtrip_through_create() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItRelateRoot>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItRelateLeaf>::delete_all()
.await
.expect("delete_all should succeed");
let input = ItRelateRoot {
id: Id::from("relate-create-root"),
title: "created".to_owned(),
primary: ItRelateLeaf {
id: Id::from("relate-create-leaf-1"),
label: "first".to_owned(),
},
optional: Some(ItRelateLeaf {
id: Id::from("relate-create-leaf-2"),
label: "second".to_owned(),
}),
items: vec![
ItRelateLeaf {
id: Id::from("relate-create-leaf-3"),
label: "third".to_owned(),
},
ItRelateLeaf {
id: Id::from("relate-create-leaf-1"),
label: "first".to_owned(),
},
],
};
let created = Repo::<ItRelateRoot>::create(input.clone())
.await
.expect("relate create should succeed");
let loaded = ItRelateRoot::get("relate-create-root")
.await
.expect("relate get after create should succeed");
let primary_edges = load_relate_edges("it_relate_primary", "relate-create-root").await;
let optional_edges = load_relate_edges("it_relate_optional", "relate-create-root").await;
let item_edges = load_relate_edges("it_relate_many", "relate-create-root").await;
assert_eq!(created, input);
assert_eq!(loaded, input);
assert_eq!(primary_edges.len(), 1);
assert_eq!(
primary_edges[0].out,
RecordId::new(ItRelateLeaf::table_name(), "relate-create-leaf-1")
);
assert_eq!(optional_edges.len(), 1);
assert_eq!(
optional_edges[0].out,
RecordId::new(ItRelateLeaf::table_name(), "relate-create-leaf-2")
);
assert_eq!(
item_edges
.iter()
.map(|row| (row.position, row.out.clone()))
.collect::<Vec<_>>(),
vec![
(
0,
RecordId::new(ItRelateLeaf::table_name(), "relate-create-leaf-3")
),
(
1,
RecordId::new(ItRelateLeaf::table_name(), "relate-create-leaf-1")
),
]
);
});
}
#[test]
fn relate_fields_roundtrip_through_save_many() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItRelateRoot>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItRelateLeaf>::delete_all()
.await
.expect("delete_all should succeed");
let first = ItRelateRoot {
id: Id::from("relate-batch-a"),
title: "batch-a".to_owned(),
primary: ItRelateLeaf {
id: Id::from("relate-batch-leaf-a1"),
label: "a1".to_owned(),
},
optional: Some(ItRelateLeaf {
id: Id::from("relate-batch-leaf-a2"),
label: "a2".to_owned(),
}),
items: vec![ItRelateLeaf {
id: Id::from("relate-batch-leaf-a3"),
label: "a3".to_owned(),
}],
};
let second = ItRelateRoot {
id: Id::from("relate-batch-b"),
title: "batch-b".to_owned(),
primary: ItRelateLeaf {
id: Id::from("relate-batch-leaf-b1"),
label: "b1".to_owned(),
},
optional: None,
items: vec![
ItRelateLeaf {
id: Id::from("relate-batch-leaf-b2"),
label: "b2".to_owned(),
},
ItRelateLeaf {
id: Id::from("relate-batch-leaf-b3"),
label: "b3".to_owned(),
},
],
};
let saved = ItRelateRoot::save_many(vec![first.clone(), second.clone()])
.await
.expect("relate save_many should succeed");
let listed = ItRelateRoot::list()
.await
.expect("relate list after save_many should succeed");
assert_eq!(saved, vec![first.clone(), second.clone()]);
assert!(listed.contains(&first));
assert!(listed.contains(&second));
assert_eq!(
load_relate_edges("it_relate_many", "relate-batch-b")
.await
.iter()
.map(|row| row.position)
.collect::<Vec<_>>(),
vec![0, 1]
);
});
}
#[test]
fn back_relate_fields_roundtrip_through_create() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItBackRelateRoot>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItBackRelateLeaf>::delete_all()
.await
.expect("delete_all should succeed");
let input = ItBackRelateRoot {
id: Id::from("back-relate-create-root"),
title: "created".to_owned(),
primary: ItBackRelateLeaf {
id: Id::from("back-relate-create-leaf-1"),
label: "first".to_owned(),
},
optional: Some(ItBackRelateLeaf {
id: Id::from("back-relate-create-leaf-2"),
label: "second".to_owned(),
}),
items: vec![
ItBackRelateLeaf {
id: Id::from("back-relate-create-leaf-3"),
label: "third".to_owned(),
},
ItBackRelateLeaf {
id: Id::from("back-relate-create-leaf-1"),
label: "first".to_owned(),
},
],
maybe_items: Some(vec![ItBackRelateLeaf {
id: Id::from("back-relate-create-leaf-4"),
label: "fourth".to_owned(),
}]),
};
let created = Repo::<ItBackRelateRoot>::create(input.clone())
.await
.expect("back relate create should succeed");
let loaded = ItBackRelateRoot::get("back-relate-create-root")
.await
.expect("back relate get after create should succeed");
let primary_edges =
load_back_relate_edges("it_back_relate_primary", "back-relate-create-root").await;
let optional_edges =
load_back_relate_edges("it_back_relate_optional", "back-relate-create-root").await;
let item_edges =
load_back_relate_edges("it_back_relate_many", "back-relate-create-root").await;
let maybe_item_edges =
load_back_relate_edges("it_back_relate_optional_many", "back-relate-create-root").await;
assert_eq!(created, input);
assert_eq!(loaded, input);
assert_eq!(primary_edges.len(), 1);
assert_eq!(
primary_edges[0].source,
RecordId::new(ItBackRelateLeaf::table_name(), "back-relate-create-leaf-1")
);
assert_eq!(optional_edges.len(), 1);
assert_eq!(
optional_edges[0].source,
RecordId::new(ItBackRelateLeaf::table_name(), "back-relate-create-leaf-2")
);
assert_eq!(
item_edges
.iter()
.map(|row| (row.position, row.source.clone()))
.collect::<Vec<_>>(),
vec![
(
0,
RecordId::new(ItBackRelateLeaf::table_name(), "back-relate-create-leaf-3")
),
(
1,
RecordId::new(ItBackRelateLeaf::table_name(), "back-relate-create-leaf-1")
),
]
);
assert_eq!(maybe_item_edges.len(), 1);
assert_eq!(
maybe_item_edges[0].source,
RecordId::new(ItBackRelateLeaf::table_name(), "back-relate-create-leaf-4")
);
});
}
#[test]
fn back_relate_fields_are_stored_in_edge_tables_and_hydrated_on_read() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItBackRelateRoot>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItBackRelateLeaf>::delete_all()
.await
.expect("delete_all should succeed");
let first = ItBackRelateLeaf {
id: Id::from("back-relate-leaf-1"),
label: "first".to_owned(),
};
let second = ItBackRelateLeaf {
id: Id::from("back-relate-leaf-2"),
label: "second".to_owned(),
};
let third = ItBackRelateLeaf {
id: Id::from("back-relate-leaf-3"),
label: "third".to_owned(),
};
let fourth = ItBackRelateLeaf {
id: Id::from("back-relate-leaf-4"),
label: "fourth".to_owned(),
};
let initial = ItBackRelateRoot {
id: Id::from("back-relate-root"),
title: "alpha".to_owned(),
primary: first.clone(),
optional: Some(second.clone()),
items: vec![third.clone(), first.clone()],
maybe_items: Some(vec![fourth.clone(), second.clone()]),
};
let saved = ItBackRelateRoot::save(initial.clone())
.await
.expect("back relate save should succeed");
let loaded = ItBackRelateRoot::get("back-relate-root")
.await
.expect("back relate get should succeed");
let raw_root = load_back_relate_root_raw("back-relate-root").await;
let primary_edges =
load_back_relate_edges("it_back_relate_primary", "back-relate-root").await;
let optional_edges =
load_back_relate_edges("it_back_relate_optional", "back-relate-root").await;
let item_edges = load_back_relate_edges("it_back_relate_many", "back-relate-root").await;
let maybe_item_edges =
load_back_relate_edges("it_back_relate_optional_many", "back-relate-root").await;
assert_eq!(saved, initial);
assert_eq!(loaded, initial);
let raw_root_object = raw_root
.as_object()
.expect("back relate root raw row should be an object");
assert_eq!(
raw_root_object.get("title"),
Some(&serde_json::Value::String("alpha".to_owned()))
);
assert!(
!raw_root_object.contains_key("primary"),
"root row should not inline back relate single field"
);
assert!(
!raw_root_object.contains_key("optional"),
"root row should not inline back relate optional field"
);
assert!(
!raw_root_object.contains_key("items"),
"root row should not inline back relate vec field"
);
assert!(
!raw_root_object.contains_key("maybe_items"),
"root row should not inline back relate optional vec field"
);
assert_eq!(primary_edges.len(), 1);
assert_eq!(primary_edges[0].position, 0);
assert_eq!(
primary_edges[0].source,
RecordId::new(ItBackRelateLeaf::table_name(), "back-relate-leaf-1")
);
assert_eq!(
primary_edges[0].out,
RecordId::new(ItBackRelateRoot::table_name(), "back-relate-root")
);
assert_eq!(optional_edges.len(), 1);
assert_eq!(
optional_edges[0].source,
RecordId::new(ItBackRelateLeaf::table_name(), "back-relate-leaf-2")
);
assert_eq!(
item_edges
.iter()
.map(|row| (row.position, row.source.clone(), row.out.clone()))
.collect::<Vec<_>>(),
vec![
(
0,
RecordId::new(ItBackRelateLeaf::table_name(), "back-relate-leaf-3"),
RecordId::new(ItBackRelateRoot::table_name(), "back-relate-root"),
),
(
1,
RecordId::new(ItBackRelateLeaf::table_name(), "back-relate-leaf-1"),
RecordId::new(ItBackRelateRoot::table_name(), "back-relate-root"),
),
]
);
assert_eq!(
maybe_item_edges
.iter()
.map(|row| (row.position, row.source.clone()))
.collect::<Vec<_>>(),
vec![
(
0,
RecordId::new(ItBackRelateLeaf::table_name(), "back-relate-leaf-4")
),
(
1,
RecordId::new(ItBackRelateLeaf::table_name(), "back-relate-leaf-2")
),
]
);
let cleared = ItBackRelateRoot {
id: Id::from("back-relate-root"),
title: "beta".to_owned(),
primary: third.clone(),
optional: None,
items: vec![second.clone(), first.clone()],
maybe_items: None,
};
let cleared_saved = ItBackRelateRoot::save(cleared.clone())
.await
.expect("back relate update should succeed");
let cleared_loaded = ItBackRelateRoot::get("back-relate-root")
.await
.expect("back relate get after clear should succeed");
assert_eq!(cleared_saved, cleared);
assert_eq!(cleared_loaded, cleared);
assert!(
load_back_relate_edges("it_back_relate_optional", "back-relate-root")
.await
.is_empty(),
"optional back relate edges should be cleared when the field becomes None"
);
assert!(
load_back_relate_edges("it_back_relate_optional_many", "back-relate-root")
.await
.is_empty(),
"optional vec back relate edges should be cleared when the field becomes None"
);
let normalized_input = ItBackRelateRoot {
id: Id::from("back-relate-root"),
title: "gamma".to_owned(),
primary: fourth.clone(),
optional: Some(first.clone()),
items: vec![],
maybe_items: Some(vec![]),
};
let normalized_expected = ItBackRelateRoot {
id: Id::from("back-relate-root"),
title: "gamma".to_owned(),
primary: fourth,
optional: Some(first),
items: vec![],
maybe_items: None,
};
let normalized_saved = ItBackRelateRoot::save(normalized_input)
.await
.expect("back relate empty optional vec save should succeed");
let normalized_loaded = ItBackRelateRoot::get("back-relate-root")
.await
.expect("back relate empty optional vec get should succeed");
assert_eq!(normalized_saved.maybe_items, Some(vec![]));
assert_eq!(normalized_loaded, normalized_expected);
assert!(
load_back_relate_edges("it_back_relate_optional_many", "back-relate-root")
.await
.is_empty(),
"empty optional vec back relate edges should hydrate back as None"
);
});
}
#[test]
fn store_sensitive_save_get_uses_generated_resolver_tags_without_call_site_context() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
clear_crypto_context_registry();
let secret_ctx = CryptoContext::new([10; 32]).expect("secret context should be valid");
let note_ctx = CryptoContext::new([11; 32]).expect("note context should be valid");
assert_eq!(
<AppdbSensitiveFieldTagItSensitiveProfileSecret as SensitiveFieldTag>::model_tag(),
<ItSensitiveProfile as SensitiveModelTag>::model_tag()
);
assert_eq!(
<AppdbSensitiveFieldTagItSensitiveProfileSecret as SensitiveFieldTag>::field_tag(),
"secret"
);
assert_eq!(
<AppdbSensitiveFieldTagItSensitiveProfileNote as SensitiveFieldTag>::model_tag(),
<ItSensitiveProfile as SensitiveModelTag>::model_tag()
);
assert_eq!(
<AppdbSensitiveFieldTagItSensitiveProfileNote as SensitiveFieldTag>::field_tag(),
"note"
);
register_crypto_context_for::<AppdbSensitiveFieldTagItSensitiveProfileSecret>(secret_ctx);
register_crypto_context_for::<AppdbSensitiveFieldTagItSensitiveProfileNote>(note_ctx);
let input = ItSensitiveProfile {
id: Id::from("s-runtime-tags"),
alias: "runtime-tags".to_owned(),
secret: "resolver-secret".to_owned(),
note: Some("resolver-note".to_owned()),
};
let saved = ItSensitiveProfile::save(input.clone())
.await
.expect("save should resolve contexts from generated field tags");
let loaded = ItSensitiveProfile::get("s-runtime-tags")
.await
.expect("get should decrypt through generated field tags");
assert_eq!(saved, input);
assert_eq!(loaded, input);
let raw = load_sensitive_profile_raw("s-runtime-tags").await;
assert_sensitive_row_encrypted(
&raw,
"runtime-tags",
"resolver-secret",
Some("resolver-note"),
);
});
}
#[test]
fn store_sensitive_list_and_list_limit_return_plaintext_models() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItSensitiveProfile>::delete_all()
.await
.expect("delete_all should succeed");
let first = ItSensitiveProfile {
id: Id::from("s-list-1"),
alias: "alpha".to_owned(),
secret: "one".to_owned(),
note: Some("first".to_owned()),
};
let second = ItSensitiveProfile {
id: Id::from("s-list-2"),
alias: "beta".to_owned(),
secret: "two".to_owned(),
note: None,
};
ItSensitiveProfile::save_many(vec![first.clone(), second.clone()])
.await
.expect("save_many should succeed");
let listed = ItSensitiveProfile::list()
.await
.expect("list should succeed");
assert_eq!(listed.len(), 2);
assert!(listed.contains(&first));
assert!(listed.contains(&second));
let limited = ItSensitiveProfile::list_limit(1)
.await
.expect("list_limit should succeed");
assert_eq!(limited.len(), 1);
assert!(limited[0] == first || limited[0] == second);
let raw_first = load_sensitive_profile_raw("s-list-1").await;
let raw_second = load_sensitive_profile_raw("s-list-2").await;
assert_sensitive_row_encrypted(&raw_first, "alpha", "one", Some("first"));
assert_sensitive_row_encrypted(&raw_second, "beta", "two", None);
});
}
#[test]
fn store_sensitive_create_and_create_at_keep_plaintext_surface() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItSensitiveProfile>::delete_all()
.await
.expect("delete_all should succeed");
let created = Repo::<ItSensitiveProfile>::create(ItSensitiveProfile {
id: Id::from("s-create"),
alias: "gamma".to_owned(),
secret: "three".to_owned(),
note: Some("create".to_owned()),
})
.await
.expect("create should succeed");
assert_eq!(created.secret, "three");
let create_at_id = RecordId::new(ItSensitiveProfile::table_name(), "s-create-at");
let created_at = Repo::<ItSensitiveProfile>::create_at(
create_at_id,
ItSensitiveProfile {
id: Id::from("s-create-at"),
alias: "delta".to_owned(),
secret: "four".to_owned(),
note: None,
},
)
.await
.expect("create_at should succeed");
assert_eq!(created_at.alias, "delta");
assert_eq!(created_at.secret, "four");
let raw_created = load_sensitive_profile_raw("s-create").await;
let raw_created_at = load_sensitive_profile_raw("s-create-at").await;
assert_sensitive_row_encrypted(&raw_created, "gamma", "three", Some("create"));
assert_sensitive_row_encrypted(&raw_created_at, "delta", "four", None);
});
}
#[test]
fn store_create_return_id_keeps_plain_store_behavior_and_guards_sensitive_models() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItProfile>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItSensitiveProfile>::delete_all()
.await
.expect("delete_all should succeed");
let created_id = Repo::<ItProfile>::create_return_id(ItProfile {
id: Id::from("plain-create-return-id"),
name: "plain".to_owned(),
note: Some("works".to_owned()),
})
.await
.expect("plain Store create_return_id should succeed");
assert_eq!(
created_id,
RecordId::new(ItProfile::table_name(), "plain-create-return-id")
);
let loaded = Repo::<ItProfile>::get("plain-create-return-id")
.await
.expect("plain Store row should exist after create_return_id");
assert_eq!(loaded.name, "plain");
assert_eq!(loaded.note.as_deref(), Some("works"));
let err = Repo::<ItSensitiveProfile>::create_return_id(ItSensitiveProfile {
id: Id::from("sensitive-create-return-id"),
alias: "guarded".to_owned(),
secret: "blocked".to_owned(),
note: Some("nope".to_owned()),
})
.await
.expect_err("sensitive Store create_return_id should be guarded");
let message = err.to_string();
assert!(message.contains("does not support create_return_id"));
assert!(message.contains("use create or create_at instead"));
let listed = Repo::<ItSensitiveProfile>::list().await;
match listed {
Ok(rows) => assert!(rows.is_empty()),
Err(err) => assert!(
err.to_string().contains("does not exist"),
"unexpected error after guarded sensitive create_return_id: {err}"
),
}
});
}
#[test]
fn store_sensitive_upsert_and_update_paths_replace_ciphertext() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItSensitiveProfile>::delete_all()
.await
.expect("delete_all should succeed");
let upserted = Repo::<ItSensitiveProfile>::upsert(ItSensitiveProfile {
id: Id::from("s-upsert"),
alias: "before".to_owned(),
secret: "alpha-secret".to_owned(),
note: Some("alpha-note".to_owned()),
})
.await
.expect("upsert should succeed");
assert_eq!(upserted.secret, "alpha-secret");
let raw_before = load_sensitive_profile_raw("s-upsert").await;
let upserted_at = Repo::<ItSensitiveProfile>::upsert_at(
RecordId::new(ItSensitiveProfile::table_name(), "s-upsert-at"),
ItSensitiveProfile {
id: Id::from("s-upsert-at"),
alias: "beta".to_owned(),
secret: "beta-secret".to_owned(),
note: None,
},
)
.await
.expect("upsert_at should succeed");
assert_eq!(upserted_at.secret, "beta-secret");
let updated = Repo::<ItSensitiveProfile>::update_at(
RecordId::new(ItSensitiveProfile::table_name(), "s-upsert"),
ItSensitiveProfile {
id: Id::from("s-upsert"),
alias: "after".to_owned(),
secret: "omega-secret".to_owned(),
note: Some("omega-note".to_owned()),
},
)
.await
.expect("update_at should succeed");
assert_eq!(updated.alias, "after");
assert_eq!(updated.secret, "omega-secret");
let reloaded = Repo::<ItSensitiveProfile>::get("s-upsert")
.await
.expect("get should succeed");
assert_eq!(reloaded, updated);
let raw_after = load_sensitive_profile_raw("s-upsert").await;
assert_sensitive_row_encrypted(&raw_after, "after", "omega-secret", Some("omega-note"));
assert_ne!(raw_before.secret, raw_after.secret);
let raw_upsert_at = load_sensitive_profile_raw("s-upsert-at").await;
assert_sensitive_row_encrypted(&raw_upsert_at, "beta", "beta-secret", None);
});
}
#[test]
fn store_sensitive_save_many_and_insert_return_plaintext_models() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItSensitiveProfile>::delete_all()
.await
.expect("delete_all should succeed");
let saved_many = Repo::<ItSensitiveProfile>::save_many(vec![
ItSensitiveProfile {
id: Id::from("s-many-1"),
alias: "alpha".to_owned(),
secret: "one".to_owned(),
note: Some("batch".to_owned()),
},
ItSensitiveProfile {
id: Id::from("s-many-2"),
alias: "beta".to_owned(),
secret: "two".to_owned(),
note: None,
},
])
.await
.expect("save_many should succeed");
assert_eq!(saved_many.len(), 2);
assert_eq!(saved_many[0].secret, "one");
assert_eq!(saved_many[1].secret, "two");
let inserted = Repo::<ItSensitiveProfile>::insert(vec![
ItSensitiveProfile {
id: Id::from("s-insert-1"),
alias: "gamma".to_owned(),
secret: "three".to_owned(),
note: Some("inserted".to_owned()),
},
ItSensitiveProfile {
id: Id::from("s-insert-2"),
alias: "delta".to_owned(),
secret: "four".to_owned(),
note: None,
},
])
.await
.expect("insert should succeed");
assert_eq!(inserted.len(), 2);
assert_eq!(inserted[0].secret, "three");
assert_eq!(inserted[1].secret, "four");
let listed = Repo::<ItSensitiveProfile>::list()
.await
.expect("list should succeed");
assert_eq!(listed.len(), 4);
assert_sensitive_row_encrypted(
&load_sensitive_profile_raw("s-many-1").await,
"alpha",
"one",
Some("batch"),
);
assert_sensitive_row_encrypted(
&load_sensitive_profile_raw("s-insert-1").await,
"gamma",
"three",
Some("inserted"),
);
});
}
#[test]
fn store_sensitive_auto_crypto_overrides_and_default_isolation_hold_across_interleaved_models() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
clear_crypto_context_registry();
reset_default_crypto_config();
let local_appdata = std::env::temp_dir().join(format!(
"appdb_integration_auto_crypto_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock before epoch")
.as_nanos()
));
unsafe {
std::env::set_var("LOCALAPPDATA", &local_appdata);
}
Repo::<ItSensitiveProfile>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItSensitiveOverrideProfile>::delete_all()
.await
.expect("delete_all should succeed");
set_default_crypto_config("integration-default-svc-a", "integration-default-acct-a");
let default_a = ItSensitiveProfile {
id: Id::from("s-default-a"),
alias: "default-a".to_owned(),
secret: "alpha-secret".to_owned(),
note: Some("alpha-note".to_owned()),
};
let saved_default_a = ItSensitiveProfile::save(default_a.clone())
.await
.expect("default sensitive save should auto-ensure from global defaults");
set_default_crypto_config("integration-default-svc-b", "integration-default-acct-b");
let overridden = ItSensitiveOverrideProfile {
id: Id::from("s-override"),
alias: "override".to_owned(),
secret: "override-secret".to_owned(),
note: Some("override-note".to_owned()),
};
let saved_override = ItSensitiveOverrideProfile::save(overridden.clone())
.await
.expect("type override save should ignore mutated global defaults");
let default_b = ItSensitiveProfile {
id: Id::from("s-default-b"),
alias: "default-b".to_owned(),
secret: "beta-secret".to_owned(),
note: Some("beta-note".to_owned()),
};
let saved_default_b = ItSensitiveProfile::save(default_b.clone())
.await
.expect("repeated default-model save should reuse original initialization");
assert_eq!(saved_default_a, default_a);
assert_eq!(saved_override, overridden);
assert_eq!(saved_default_b, default_b);
assert_eq!(
ItSensitiveProfile::get("s-default-a")
.await
.expect("default-a should decrypt"),
default_a
);
assert_eq!(
ItSensitiveProfile::get("s-default-b")
.await
.expect("default-b should decrypt"),
default_b
);
assert_eq!(
ItSensitiveOverrideProfile::get("s-override")
.await
.expect("override row should decrypt"),
overridden
);
let default_list = ItSensitiveProfile::list()
.await
.expect("default list should succeed");
assert!(default_list.contains(&default_a));
assert!(default_list.contains(&default_b));
let saved_many = ItSensitiveOverrideProfile::save_many(vec![
ItSensitiveOverrideProfile {
id: Id::from("s-override-many-1"),
alias: "override-many-1".to_owned(),
secret: "many-secret-1".to_owned(),
note: Some("many-note-1".to_owned()),
},
ItSensitiveOverrideProfile {
id: Id::from("s-override-many-2"),
alias: "override-many-2".to_owned(),
secret: "many-secret-2".to_owned(),
note: None,
},
])
.await
.expect("override save_many should auto-ensure and reuse override contexts");
assert_eq!(saved_many[0].secret, "many-secret-1");
assert_eq!(saved_many[1].secret, "many-secret-2");
assert_sensitive_row_encrypted(
&load_sensitive_profile_raw("s-default-a").await,
"default-a",
"alpha-secret",
Some("alpha-note"),
);
assert_sensitive_row_encrypted(
&load_sensitive_profile_raw("s-default-b").await,
"default-b",
"beta-secret",
Some("beta-note"),
);
assert_sensitive_override_row_encrypted(
&load_sensitive_override_profile_raw("s-override").await,
"override",
"override-secret",
Some("override-note"),
);
assert_sensitive_override_row_encrypted(
&load_sensitive_override_profile_raw("s-override-many-1").await,
"override-many-1",
"many-secret-1",
Some("many-note-1"),
);
unsafe {
std::env::remove_var("LOCALAPPDATA");
}
let _ = std::fs::remove_dir_all(local_appdata);
clear_crypto_context_registry();
reset_default_crypto_config();
});
}
#[test]
fn plain_store_enum_fields_roundtrip_through_save_get_list_and_save_many() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItPlainEnumProfile>::delete_all()
.await
.expect("delete_all should succeed");
let saved = ItPlainEnumProfile::save(ItPlainEnumProfile {
id: Id::from("plain-enum-save"),
name: "plain-one".to_owned(),
status: ItPlainStatus::Draft {
kind: "Draft".to_owned(),
},
optional_status: Some(ItPlainStatus::Published {
kind: "Published".to_owned(),
}),
state: ItPayloadState::Draft {
kind: "Draft".to_owned(),
note: "draft-note".to_owned(),
},
})
.await
.expect("save should roundtrip plain enum fields");
let raw_saved = load_plain_enum_profile_raw("plain-enum-save").await;
assert_eq!(raw_saved.name, "plain-one");
assert_eq!(
raw_saved.status,
serde_json::json!({ "Draft": { "kind": "Draft" } })
);
assert_eq!(
raw_saved.optional_status,
Some(serde_json::json!({ "Published": { "kind": "Published" } }))
);
assert_eq!(
raw_saved.state,
serde_json::json!({ "Draft": { "kind": "Draft", "note": "draft-note" } })
);
assert_eq!(
saved,
ItPlainEnumProfile {
id: Id::from("plain-enum-save"),
name: "plain-one".to_owned(),
status: ItPlainStatus::Draft {
kind: "Draft".to_owned(),
},
optional_status: Some(ItPlainStatus::Published {
kind: "Published".to_owned(),
}),
state: ItPayloadState::Draft {
kind: "Draft".to_owned(),
note: "draft-note".to_owned(),
},
}
);
let loaded = ItPlainEnumProfile::get("plain-enum-save")
.await
.expect("get should reload plain enum fields");
assert_eq!(loaded, saved);
let batch_first = ItPlainEnumProfile {
id: Id::from("plain-enum-batch-1"),
name: "plain-batch-one".to_owned(),
status: ItPlainStatus::Published {
kind: "Published".to_owned(),
},
optional_status: None,
state: ItPayloadState::Published {
kind: "Published".to_owned(),
version: 7,
tags: vec!["release".to_owned(), "stable".to_owned()],
},
};
let batch_second = ItPlainEnumProfile {
id: Id::from("plain-enum-batch-2"),
name: "plain-batch-two".to_owned(),
status: ItPlainStatus::Draft {
kind: "Draft".to_owned(),
},
optional_status: Some(ItPlainStatus::Draft {
kind: "Draft".to_owned(),
}),
state: ItPayloadState::Draft {
kind: "Draft".to_owned(),
note: "batch-draft".to_owned(),
},
};
let saved_many =
ItPlainEnumProfile::save_many(vec![batch_first.clone(), batch_second.clone()])
.await
.expect("save_many should preserve enum row association");
assert_eq!(saved_many, vec![batch_first.clone(), batch_second.clone()]);
let listed = ItPlainEnumProfile::list()
.await
.expect("list should return all enum-bearing rows");
assert_eq!(listed.len(), 3);
assert!(listed.contains(&saved));
assert!(listed.contains(&batch_first));
assert!(listed.contains(&batch_second));
let reloaded_batch_first = ItPlainEnumProfile::get("plain-enum-batch-1")
.await
.expect("first batch row should reload");
let reloaded_batch_second = ItPlainEnumProfile::get("plain-enum-batch-2")
.await
.expect("second batch row should reload");
assert_eq!(reloaded_batch_first, batch_first);
assert_eq!(reloaded_batch_second, batch_second);
});
}
#[test]
fn sensitive_enum_shape_runtime_seam_supports_store_roundtrip_without_secure_enum_syntax() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItSensitiveRuntimeSeamParent>::delete_all()
.await
.expect("delete_all should succeed");
let saved = ItSensitiveRuntimeSeamParent::save(ItSensitiveRuntimeSeamParent {
id: Id::from("sensitive-enum-seam-save"),
alias: "secure-enum-parent".to_owned(),
payload: SensitiveValueOf::from(ItSensitiveRuntimeSeamPayload {
alias: "secure-enum-payload".to_owned(),
status: ItSensitiveRuntimeSeamStatus::Draft,
optional_status: Some(ItSensitiveRuntimeSeamStatus::Published),
state: ItSensitiveRuntimeSeamState::Published {
version: 9,
tags: vec!["stable".to_owned(), "tagged".to_owned()],
},
}),
})
.await
.expect("save should roundtrip enum-bearing sensitive payload");
let loaded = ItSensitiveRuntimeSeamParent::get("sensitive-enum-seam-save")
.await
.expect("get should reload enum-bearing sensitive payload");
assert_eq!(loaded, saved);
let batch_first = ItSensitiveRuntimeSeamParent {
id: Id::from("sensitive-enum-seam-batch-1"),
alias: "secure-enum-batch-one".to_owned(),
payload: SensitiveValueOf::from(ItSensitiveRuntimeSeamPayload {
alias: "payload-one".to_owned(),
status: ItSensitiveRuntimeSeamStatus::Published,
optional_status: None,
state: ItSensitiveRuntimeSeamState::Draft {
note: "batch-note".to_owned(),
},
}),
};
let batch_second = ItSensitiveRuntimeSeamParent {
id: Id::from("sensitive-enum-seam-batch-2"),
alias: "secure-enum-batch-two".to_owned(),
payload: SensitiveValueOf::from(ItSensitiveRuntimeSeamPayload {
alias: "payload-two".to_owned(),
status: ItSensitiveRuntimeSeamStatus::Draft,
optional_status: Some(ItSensitiveRuntimeSeamStatus::Draft),
state: ItSensitiveRuntimeSeamState::Published {
version: 3,
tags: vec!["canary".to_owned()],
},
}),
};
let saved_many = ItSensitiveRuntimeSeamParent::save_many(vec![
batch_first.clone(),
batch_second.clone(),
])
.await
.expect("save_many should preserve enum-bearing sensitive row association");
assert_eq!(saved_many, vec![batch_first.clone(), batch_second.clone()]);
let listed = ItSensitiveRuntimeSeamParent::list()
.await
.expect("list should return all sensitive enum-bearing rows");
assert_eq!(listed.len(), 3);
assert!(listed.contains(&saved));
assert!(listed.contains(&batch_first));
assert!(listed.contains(&batch_second));
let raw = load_sensitive_runtime_seam_parent_raw("sensitive-enum-seam-save").await;
assert_eq!(raw.alias, "secure-enum-parent");
assert_ne!(
raw.payload,
serde_json::to_vec(&*saved.payload).expect("payload should serialize")
);
let raw_text = String::from_utf8_lossy(&raw.payload);
assert!(!raw_text.contains("Draft"));
assert!(!raw_text.contains("Published"));
assert!(!raw_text.contains("stable"));
});
}
#[test]
fn sensitive_contained_enum_fields_roundtrip_through_save_get_list_and_save_many() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItSensitiveRuntimeSeamParent>::delete_all()
.await
.expect("delete_all should succeed");
let saved_row = ItSensitiveRuntimeSeamParent {
id: Id::from("sensitive-enum-contract-save"),
alias: "contract-parent".to_owned(),
payload: SensitiveValueOf::from(ItSensitiveRuntimeSeamPayload {
alias: "contract-payload".to_owned(),
status: ItSensitiveRuntimeSeamStatus::Draft,
optional_status: Some(ItSensitiveRuntimeSeamStatus::Published),
state: ItSensitiveRuntimeSeamState::Published {
version: 11,
tags: vec!["contract".to_owned(), "release".to_owned()],
},
}),
};
let saved = ItSensitiveRuntimeSeamParent::save(saved_row.clone())
.await
.expect("save should roundtrip sensitive-contained enum fields");
assert_eq!(saved, saved_row);
let loaded = ItSensitiveRuntimeSeamParent::get("sensitive-enum-contract-save")
.await
.expect("get should reload sensitive-contained enum fields");
assert_eq!(loaded, saved_row);
let batch_first = ItSensitiveRuntimeSeamParent {
id: Id::from("sensitive-enum-contract-batch-1"),
alias: "contract-batch-one".to_owned(),
payload: SensitiveValueOf::from(ItSensitiveRuntimeSeamPayload {
alias: "batch-payload-one".to_owned(),
status: ItSensitiveRuntimeSeamStatus::Published,
optional_status: None,
state: ItSensitiveRuntimeSeamState::Draft {
note: "batch-contract-note".to_owned(),
},
}),
};
let batch_second = ItSensitiveRuntimeSeamParent {
id: Id::from("sensitive-enum-contract-batch-2"),
alias: "contract-batch-two".to_owned(),
payload: SensitiveValueOf::from(ItSensitiveRuntimeSeamPayload {
alias: "batch-payload-two".to_owned(),
status: ItSensitiveRuntimeSeamStatus::Draft,
optional_status: Some(ItSensitiveRuntimeSeamStatus::Draft),
state: ItSensitiveRuntimeSeamState::Published {
version: 4,
tags: vec!["contract-canary".to_owned()],
},
}),
};
let saved_many = ItSensitiveRuntimeSeamParent::save_many(vec![
batch_first.clone(),
batch_second.clone(),
])
.await
.expect("save_many should preserve sensitive enum row association");
assert_eq!(saved_many, vec![batch_first.clone(), batch_second.clone()]);
let listed = ItSensitiveRuntimeSeamParent::list()
.await
.expect("list should return all sensitive enum-bearing rows");
assert_eq!(listed.len(), 3);
assert!(listed.contains(&saved_row));
assert!(listed.contains(&batch_first));
assert!(listed.contains(&batch_second));
assert_eq!(
ItSensitiveRuntimeSeamParent::get("sensitive-enum-contract-batch-1")
.await
.expect("first batch row should reload"),
batch_first
);
assert_eq!(
ItSensitiveRuntimeSeamParent::get("sensitive-enum-contract-batch-2")
.await
.expect("second batch row should reload"),
batch_second
);
let raw = load_sensitive_runtime_seam_parent_raw("sensitive-enum-contract-save").await;
assert_eq!(raw.alias, "contract-parent");
assert_ne!(
raw.payload,
serde_json::to_vec(&*saved_row.payload).expect("payload should serialize")
);
let raw_text = String::from_utf8_lossy(&raw.payload);
assert!(!raw_text.contains("Draft"));
assert!(!raw_text.contains("Published"));
assert!(!raw_text.contains("contract"));
assert!(!raw_text.contains("release"));
});
}
#[test]
fn enum_roundtrip_remains_correct_when_nested_overrides_are_present() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItNestedOverrideParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItSensitiveRuntimeSeamParent>::delete_all()
.await
.expect("delete_all should succeed");
let nested_parent = ItNestedOverrideParent {
id: Id::from("nested-override-enum-parent"),
alias: "nested-override-enum".to_owned(),
left: ItNestedOverrideLeaf {
label: "left-enum-leaf".to_owned(),
secret: "left-enum-secret".to_owned(),
},
right: ItNestedOverrideLeaf {
label: "right-enum-leaf".to_owned(),
secret: "right-enum-secret".to_owned(),
},
};
ItNestedOverrideParent::save(nested_parent.clone())
.await
.expect("nested override save should succeed");
let enum_saved = ItSensitiveRuntimeSeamParent::save(ItSensitiveRuntimeSeamParent {
id: Id::from("nested-override-enum-save"),
alias: "enum-with-overrides".to_owned(),
payload: SensitiveValueOf::from(ItSensitiveRuntimeSeamPayload {
alias: "enum-save-payload".to_owned(),
status: ItSensitiveRuntimeSeamStatus::Published,
optional_status: Some(ItSensitiveRuntimeSeamStatus::Draft),
state: ItSensitiveRuntimeSeamState::Published {
version: 21,
tags: vec!["overrides".to_owned(), "exact".to_owned()],
},
}),
})
.await
.expect("enum save should still succeed with nested overrides present");
let enum_batch_first = ItSensitiveRuntimeSeamParent {
id: Id::from("nested-override-enum-batch-1"),
alias: "enum-batch-one".to_owned(),
payload: SensitiveValueOf::from(ItSensitiveRuntimeSeamPayload {
alias: "enum-batch-payload-one".to_owned(),
status: ItSensitiveRuntimeSeamStatus::Draft,
optional_status: None,
state: ItSensitiveRuntimeSeamState::Draft {
note: "override-batch-note".to_owned(),
},
}),
};
let enum_batch_second = ItSensitiveRuntimeSeamParent {
id: Id::from("nested-override-enum-batch-2"),
alias: "enum-batch-two".to_owned(),
payload: SensitiveValueOf::from(ItSensitiveRuntimeSeamPayload {
alias: "enum-batch-payload-two".to_owned(),
status: ItSensitiveRuntimeSeamStatus::Published,
optional_status: Some(ItSensitiveRuntimeSeamStatus::Published),
state: ItSensitiveRuntimeSeamState::Published {
version: 22,
tags: vec!["override".to_owned(), "batch".to_owned()],
},
}),
};
let saved_many = ItSensitiveRuntimeSeamParent::save_many(vec![
enum_batch_first.clone(),
enum_batch_second.clone(),
])
.await
.expect("save_many should keep exact enum fidelity while nested overrides coexist");
assert_eq!(
saved_many,
vec![enum_batch_first.clone(), enum_batch_second.clone()]
);
assert_eq!(
ItNestedOverrideParent::get("nested-override-enum-parent")
.await
.expect("nested override row should still decrypt"),
nested_parent
);
assert_eq!(
ItSensitiveRuntimeSeamParent::get("nested-override-enum-save")
.await
.expect("enum row should reload exactly"),
enum_saved
);
assert_eq!(
ItSensitiveRuntimeSeamParent::get("nested-override-enum-batch-1")
.await
.expect("first enum batch row should reload"),
enum_batch_first
);
assert_eq!(
ItSensitiveRuntimeSeamParent::get("nested-override-enum-batch-2")
.await
.expect("second enum batch row should reload"),
enum_batch_second
);
let enum_list = ItSensitiveRuntimeSeamParent::list()
.await
.expect("enum list should succeed with nested overrides present");
assert!(enum_list.contains(&enum_saved));
assert!(enum_list.contains(&enum_batch_first));
assert!(enum_list.contains(&enum_batch_second));
let nested_raw = load_nested_override_parent_raw("nested-override-enum-parent").await;
assert_ne!(nested_raw.left.secret, b"left-enum-secret");
assert_ne!(nested_raw.right.secret, b"right-enum-secret");
let enum_raw = load_sensitive_runtime_seam_parent_raw("nested-override-enum-save").await;
let enum_raw_text = String::from_utf8_lossy(&enum_raw.payload);
assert!(!enum_raw_text.contains("Published"));
assert!(!enum_raw_text.contains("overrides"));
assert!(!enum_raw_text.contains("exact"));
});
}
#[test]
fn save_many_mixed_sensitive_rows_preserve_auto_ensure_and_isolation_semantics() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
clear_crypto_context_registry();
reset_default_crypto_config();
let local_appdata = std::env::temp_dir().join(format!(
"appdb_integration_cross_batch_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock before epoch")
.as_nanos()
));
unsafe {
std::env::set_var("LOCALAPPDATA", &local_appdata);
}
Repo::<ItSensitiveProfile>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItNestedOverrideParent>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItSensitiveRuntimeSeamParent>::delete_all()
.await
.expect("delete_all should succeed");
set_default_crypto_config(
"integration-cross-batch-svc",
"integration-cross-batch-acct",
);
let default_rows = vec![
ItSensitiveProfile {
id: Id::from("cross-batch-default-1"),
alias: "default-cross-1".to_owned(),
secret: "default-cross-secret-1".to_owned(),
note: Some("default-cross-note-1".to_owned()),
},
ItSensitiveProfile {
id: Id::from("cross-batch-default-2"),
alias: "default-cross-2".to_owned(),
secret: "default-cross-secret-2".to_owned(),
note: None,
},
];
let default_saved = ItSensitiveProfile::save_many(default_rows.clone())
.await
.expect("default sensitive save_many should auto-ensure");
assert_eq!(default_saved, default_rows);
let nested_rows = vec![
ItNestedOverrideParent {
id: Id::from("cross-batch-nested-1"),
alias: "nested-cross-1".to_owned(),
left: ItNestedOverrideLeaf {
label: "nested-left-1".to_owned(),
secret: "nested-left-secret-1".to_owned(),
},
right: ItNestedOverrideLeaf {
label: "nested-right-1".to_owned(),
secret: "nested-right-secret-1".to_owned(),
},
},
ItNestedOverrideParent {
id: Id::from("cross-batch-nested-2"),
alias: "nested-cross-2".to_owned(),
left: ItNestedOverrideLeaf {
label: "nested-left-2".to_owned(),
secret: "nested-left-secret-2".to_owned(),
},
right: ItNestedOverrideLeaf {
label: "nested-right-2".to_owned(),
secret: "nested-right-secret-2".to_owned(),
},
},
];
let nested_saved = ItNestedOverrideParent::save_many(nested_rows.clone())
.await
.expect("nested override save_many should preserve leaf isolation");
assert_eq!(nested_saved, nested_rows);
let enum_rows = vec![
ItSensitiveRuntimeSeamParent {
id: Id::from("cross-batch-enum-1"),
alias: "enum-cross-1".to_owned(),
payload: SensitiveValueOf::from(ItSensitiveRuntimeSeamPayload {
alias: "enum-cross-payload-1".to_owned(),
status: ItSensitiveRuntimeSeamStatus::Draft,
optional_status: Some(ItSensitiveRuntimeSeamStatus::Published),
state: ItSensitiveRuntimeSeamState::Published {
version: 31,
tags: vec!["cross".to_owned(), "enum".to_owned()],
},
}),
},
ItSensitiveRuntimeSeamParent {
id: Id::from("cross-batch-enum-2"),
alias: "enum-cross-2".to_owned(),
payload: SensitiveValueOf::from(ItSensitiveRuntimeSeamPayload {
alias: "enum-cross-payload-2".to_owned(),
status: ItSensitiveRuntimeSeamStatus::Published,
optional_status: None,
state: ItSensitiveRuntimeSeamState::Draft {
note: "cross-batch-enum-note".to_owned(),
},
}),
},
];
let enum_saved = ItSensitiveRuntimeSeamParent::save_many(enum_rows.clone())
.await
.expect("enum-bearing save_many should preserve exact row/value association");
assert_eq!(enum_saved, enum_rows);
assert_eq!(
ItSensitiveProfile::list()
.await
.expect("default list should succeed"),
default_rows
);
assert_eq!(
ItNestedOverrideParent::list()
.await
.expect("nested override list should succeed"),
nested_rows
);
assert_eq!(
ItSensitiveRuntimeSeamParent::list()
.await
.expect("enum list should succeed"),
enum_rows
);
assert_sensitive_row_encrypted(
&load_sensitive_profile_raw("cross-batch-default-1").await,
"default-cross-1",
"default-cross-secret-1",
Some("default-cross-note-1"),
);
assert_sensitive_row_encrypted(
&load_sensitive_profile_raw("cross-batch-default-2").await,
"default-cross-2",
"default-cross-secret-2",
None,
);
let nested_raw = load_nested_override_parent_raw("cross-batch-nested-1").await;
let left_ctx = appdb::crypto::resolve_crypto_context_for::<
AppdbSensitiveFieldTagItNestedOverrideParentLeft,
>()
.expect("left override context should resolve");
let right_ctx = appdb::crypto::resolve_crypto_context_for::<
AppdbSensitiveFieldTagItNestedOverrideParentRight,
>()
.expect("right override context should resolve");
assert_eq!(
ItNestedOverrideLeaf::decrypt_with_context(&nested_raw.left, &left_ctx)
.expect("left nested row should decrypt with left context"),
nested_rows[0].left
);
assert_eq!(
ItNestedOverrideLeaf::decrypt_with_context(&nested_raw.right, &right_ctx)
.expect("right nested row should decrypt with right context"),
nested_rows[0].right
);
assert!(matches!(
ItNestedOverrideLeaf::decrypt_with_context(&nested_raw.left, &right_ctx),
Err(appdb::crypto::CryptoError::Decrypt)
));
assert!(matches!(
ItNestedOverrideLeaf::decrypt_with_context(&nested_raw.right, &left_ctx),
Err(appdb::crypto::CryptoError::Decrypt)
));
let enum_raw = load_sensitive_runtime_seam_parent_raw("cross-batch-enum-1").await;
assert_ne!(
enum_raw.payload,
serde_json::to_vec(&*enum_rows[0].payload).expect("payload should serialize")
);
let enum_raw_text = String::from_utf8_lossy(&enum_raw.payload);
assert!(!enum_raw_text.contains("Published"));
assert!(!enum_raw_text.contains("cross"));
assert!(!enum_raw_text.contains("enum"));
unsafe {
std::env::remove_var("LOCALAPPDATA");
}
let _ = std::fs::remove_dir_all(local_appdata);
clear_crypto_context_registry();
reset_default_crypto_config();
});
}
#[test]
fn store_sensitive_concurrent_first_use_initialization_is_single_flight_per_model() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
clear_crypto_context_registry();
reset_default_crypto_config();
let local_appdata = std::env::temp_dir().join(format!(
"appdb_integration_single_flight_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock before epoch")
.as_nanos()
));
unsafe {
std::env::set_var("LOCALAPPDATA", &local_appdata);
}
#[derive(
Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store, Sensitive,
)]
struct ItSensitiveSingleFlightProfile {
id: Id,
alias: String,
#[secure]
secret: String,
}
Repo::<ItSensitiveSingleFlightProfile>::delete_all()
.await
.expect("delete_all should succeed");
let first = ItSensitiveSingleFlightProfile {
id: Id::from("single-flight-a"),
alias: "alpha".to_owned(),
secret: "secret-a".to_owned(),
};
let second = ItSensitiveSingleFlightProfile {
id: Id::from("single-flight-b"),
alias: "beta".to_owned(),
secret: "secret-b".to_owned(),
};
let first_task = tokio::spawn({
let value = first.clone();
async move {
let encrypted = value.encrypt_with_runtime_resolver()?;
ItSensitiveSingleFlightProfile::decrypt_with_runtime_resolver(&encrypted)
}
});
let second_task = tokio::spawn({
let value = second.clone();
async move {
let encrypted = value.encrypt_with_runtime_resolver()?;
ItSensitiveSingleFlightProfile::decrypt_with_runtime_resolver(&encrypted)
}
});
let saved_first = first_task
.await
.expect("first store worker should not panic")
.expect("first store path should initialize crypto");
let saved_second = second_task
.await
.expect("second store worker should not panic")
.expect("second store path should reuse initialization");
set_default_crypto_config(
"integration-single-flight-mutated-svc",
"integration-single-flight-mutated-acct",
);
assert_eq!(saved_first, first);
assert_eq!(saved_second, second);
let persisted_first = ItSensitiveSingleFlightProfile::save(first.clone())
.await
.expect("save after single-flight init should succeed");
let persisted_second = ItSensitiveSingleFlightProfile::save(second.clone())
.await
.expect("second save after single-flight init should succeed");
assert_eq!(persisted_first, first);
assert_eq!(persisted_second, second);
assert_eq!(
ItSensitiveSingleFlightProfile::get("single-flight-a")
.await
.expect("first row should decrypt with established context"),
first
);
assert_eq!(
ItSensitiveSingleFlightProfile::get("single-flight-b")
.await
.expect("second row should decrypt with established context"),
second
);
unsafe {
std::env::remove_var("LOCALAPPDATA");
}
let _ = std::fs::remove_dir_all(local_appdata);
clear_crypto_context_registry();
reset_default_crypto_config();
});
}
#[test]
fn store_sensitive_later_first_use_models_pick_up_new_global_defaults() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
clear_crypto_context_registry();
reset_default_crypto_config();
let local_appdata = std::env::temp_dir().join(format!(
"appdb_integration_global_mutation_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock before epoch")
.as_nanos()
));
unsafe {
std::env::set_var("LOCALAPPDATA", &local_appdata);
}
#[derive(
Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store, Sensitive,
)]
struct ItSensitiveLateDefaultsA {
id: Id,
alias: String,
#[secure]
secret: String,
}
#[derive(
Debug, Clone, PartialEq, Serialize, Deserialize, SurrealValue, Store, Sensitive,
)]
struct ItSensitiveLateDefaultsB {
id: Id,
alias: String,
#[secure]
secret: String,
}
Repo::<ItSensitiveLateDefaultsA>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItSensitiveLateDefaultsB>::delete_all()
.await
.expect("delete_all should succeed");
set_default_crypto_config("integration-late-svc-a", "integration-late-acct-a");
let a = ItSensitiveLateDefaultsA {
id: Id::from("late-a"),
alias: "late-a".to_owned(),
secret: "secret-a".to_owned(),
};
let saved_a = ItSensitiveLateDefaultsA::save(a.clone())
.await
.expect("first model should initialize under initial defaults");
set_default_crypto_config("integration-late-svc-b", "integration-late-acct-b");
let b = ItSensitiveLateDefaultsB {
id: Id::from("late-b"),
alias: "late-b".to_owned(),
secret: "secret-b".to_owned(),
};
let saved_b = ItSensitiveLateDefaultsB::save(b.clone())
.await
.expect("later first-use model should initialize under updated defaults");
assert_eq!(saved_a, a);
assert_eq!(saved_b, b);
assert_eq!(
ItSensitiveLateDefaultsA::get("late-a")
.await
.expect("model a should still decrypt under original context"),
a
);
assert_eq!(
ItSensitiveLateDefaultsB::get("late-b")
.await
.expect("model b should decrypt under later defaults"),
b
);
unsafe {
std::env::remove_var("LOCALAPPDATA");
}
let _ = std::fs::remove_dir_all(local_appdata);
clear_crypto_context_registry();
reset_default_crypto_config();
});
}
#[test]
fn store_sensitive_insert_ignore_and_insert_or_replace_preserve_plaintext_semantics() {
let _guard = acquire_test_lock();
run_async(async {
ensure_db().await;
Repo::<ItSensitiveProfile>::delete_all()
.await
.expect("delete_all should succeed");
Repo::<ItSensitiveProfile>::insert(vec![ItSensitiveProfile {
id: Id::from("s-conflict"),
alias: "original".to_owned(),
secret: "original-secret".to_owned(),
note: Some("original-note".to_owned()),
}])
.await
.expect("seed insert should succeed");
let ignored = Repo::<ItSensitiveProfile>::insert_ignore(vec![
ItSensitiveProfile {
id: Id::from("s-conflict"),
alias: "ignored".to_owned(),
secret: "ignored-secret".to_owned(),
note: Some("ignored-note".to_owned()),
},
ItSensitiveProfile {
id: Id::from("s-new"),
alias: "new".to_owned(),
secret: "new-secret".to_owned(),
note: None,
},
])
.await
.expect("insert_ignore should succeed");
assert_eq!(ignored.len(), 1);
assert_eq!(ignored[0].id, Id::from("s-new"));
let after_ignore = Repo::<ItSensitiveProfile>::get("s-conflict")
.await
.expect("conflict row should still exist");
assert_eq!(after_ignore.alias, "original");
assert_eq!(after_ignore.secret, "original-secret");
let replaced = Repo::<ItSensitiveProfile>::insert_or_replace(vec![
ItSensitiveProfile {
id: Id::from("s-conflict"),
alias: "replacement".to_owned(),
secret: "replacement-secret".to_owned(),
note: Some("replacement-note".to_owned()),
},
ItSensitiveProfile {
id: Id::from("s-fresh"),
alias: "fresh".to_owned(),
secret: "fresh-secret".to_owned(),
note: None,
},
])
.await
.expect("insert_or_replace should succeed");
assert_eq!(replaced.len(), 2);
assert!(
replaced
.iter()
.any(|row| row.id == Id::from("s-conflict") && row.secret == "replacement-secret")
);
let after_replace = Repo::<ItSensitiveProfile>::get("s-conflict")
.await
.expect("replaced row should load");
assert_eq!(after_replace.alias, "replacement");
assert_eq!(after_replace.secret, "replacement-secret");
assert_sensitive_row_encrypted(
&load_sensitive_profile_raw("s-conflict").await,
"replacement",
"replacement-secret",
Some("replacement-note"),
);
assert_sensitive_row_encrypted(
&load_sensitive_profile_raw("s-new").await,
"new",
"new-secret",
None,
);
});
}