keepsake-sqlx 0.4.0

SQLx/Postgres adapter for keepsake lifecycle storage
Documentation
use super::support::*;

#[tokio::test]
#[ignore = "requires docker postgres; run `make test-db`"]
async fn relation_upsert_is_idempotent_by_natural_key() -> TestResult<()> {
    let repo = repo().await?;
    let key = RelationKey::new("tag", unique_key("idempotent"))?;
    let first =
        RelationDefinition::new(Uuid::now_v7(), key.clone(), true, ExpiryPolicy::ManualOnly)?;
    let second = RelationDefinition::new(
        Uuid::now_v7(),
        key,
        false,
        ExpiryPolicy::At {
            timestamp: ts("2026-02-01T00:00:00Z")?,
        },
    )?;

    let inserted = upsert_relation(&repo, &first).await?;
    let updated = upsert_relation(&repo, &second).await?;

    assert_eq!(inserted.id, updated.id);
    assert!(!updated.enabled);
    assert!(matches!(updated.expiry, ExpiryPolicy::At { .. }));
    Ok(())
}

#[tokio::test]
#[ignore = "requires docker postgres; run `make test-db`"]
async fn relation_reads_return_stored_relation_definition() -> TestResult<()> {
    let repo = repo().await?;
    let key = RelationKey::new("tag", unique_key("lookup"))?;
    let relation = RelationDefinition::new(
        Uuid::now_v7(),
        key.clone(),
        true,
        ExpiryPolicy::At {
            timestamp: ts("2026-02-01T00:00:00Z")?,
        },
    )?;
    let stored = upsert_relation(&repo, &relation).await?;

    assert_eq!(repo.relation_by_id(stored.id).await?, Some(stored.clone()));
    assert_eq!(repo.relation_by_key(&key).await?, Some(stored));
    assert_eq!(repo.relation_by_id(Uuid::now_v7()).await?, None);
    assert_eq!(
        repo.relation_by_key(&RelationKey::new("tag", unique_key("missing"))?)
            .await?,
        None
    );
    Ok(())
}

#[tokio::test]
#[ignore = "requires docker postgres; run `make test-db`"]
async fn typed_relation_specs_upsert_and_apply_by_marker_type() -> TestResult<()> {
    let repo = repo().await?;
    let now = ts("2026-01-01T00:00:00Z")?;
    let relation = repo.upsert_relation_spec::<TrustedAccountTag>(now).await?;
    let subject = SubjectRef::new("account", format!("typed_{}", Uuid::now_v7()))?;

    let command =
        ApplyKeepsake::for_spec::<TrustedAccountTag>(subject.clone(), now, test_context("worker")?);
    let applied = repo.apply(&command).await?;

    assert_eq!(relation.id, TrustedAccountTag::ID);
    assert_eq!(relation.key.kind(), "tag");
    assert_eq!(relation.key.name(), "trusted_account");
    assert_eq!(applied.keepsake.relation_id(), TrustedAccountTag::ID);
    assert_eq!(repo.active_for_subject(&subject).await?.len(), 1);
    Ok(())
}

#[tokio::test]
#[ignore = "requires docker postgres; run `make test-db`"]
async fn typed_relation_specs_reject_existing_key_with_different_id() -> TestResult<()> {
    let repo = repo().await?;
    let now = ts("2026-01-01T00:00:00Z")?;
    let existing = RelationDefinition::enabled(
        Uuid::now_v7(),
        ConflictingTrustedAccountTag::KEY.to_relation_key()?,
        ExpiryPolicy::At {
            timestamp: ts("2026-02-01T00:00:00Z")?,
        },
    )?;
    let existing = upsert_relation(&repo, &existing).await?;

    let result = repo
        .upsert_relation_spec::<ConflictingTrustedAccountTag>(now)
        .await;

    assert!(matches!(
        result,
        Err(RepositoryError::RelationSpecIdMismatch {
            expected_relation_id,
            stored_relation_id,
            ..
        }) if expected_relation_id == ConflictingTrustedAccountTag::ID
            && stored_relation_id == existing.id
    ));
    assert_eq!(repo.relation_by_id(existing.id).await?, Some(existing));
    Ok(())
}

#[cfg(feature = "cache")]
#[tokio::test]
#[ignore = "requires docker postgres; run `make test-db`"]
async fn relation_cache_serves_reads_and_invalidates_local_writes() -> TestResult<()> {
    let database_url = std::env::var("DATABASE_URL")?;
    let pool = PgPool::connect(&database_url).await?;
    let repo = KeepsakeRepository::new(pool.clone())
        .with_local_relation_cache(LocalRelationCacheConfig::new(Duration::from_mins(1)));
    repo.migrate().await?;
    reset_database(&pool).await?;

    let key = RelationKey::new("tag", unique_key("cached"))?;
    let relation =
        RelationDefinition::new(Uuid::now_v7(), key.clone(), true, ExpiryPolicy::ManualOnly)?;
    let stored = upsert_relation(&repo, &relation).await?;

    assert_eq!(repo.relation_by_key(&key).await?, Some(stored.clone()));

    sqlx::query(
        r"
        update keepsake_relation_definitions
        set enabled = false
        where id = $1
        ",
    )
    .bind(stored.id)
    .execute(&pool)
    .await?;

    let cached = repo.relation_by_id(stored.id).await?;
    assert_eq!(cached, Some(stored.clone()));

    assert!(set_relation_enabled(&repo, stored.id, true).await?);
    let refreshed = repo.relation_by_id(stored.id).await?;
    assert_eq!(refreshed, Some(stored));
    Ok(())
}