Skip to main content

valence_core/ownership/service/
mod.rs

1//! Row ownership persistence (`valence_data_ownership` / `valence_ownership_transfer`).
2//!
3//! Uses the database backend directly for these platform tables so `valence` does not need a
4//! self-hosted `valence-codegen` pass (which would create a Cargo build dependency cycle).
5
6mod gates;
7mod helpers;
8mod read;
9mod write;
10
11pub use helpers::{
12    normalize_record_id_for_ownership, owner_ref_from_ownership_json, ownership_colocate_enabled,
13    ownership_get_join_enabled, ownership_unified_fetch_enabled, parse_owner_kind,
14    skip_ownership_for_table, OwnerDataSummary, OwnerSchemaRowCount, OwnershipGateStatus,
15    RecordOwnershipBundle, SKIP_OWNERSHIP_TABLES,
16};
17
18use std::sync::Arc;
19
20use crate::backend::DatabaseBackend;
21use crate::error::{Error, Result};
22use crate::runtime::Valence;
23use crate::schema::SchemaRegistry;
24
25use helpers::system_valence;
26
27/// Ownership persistence and transfer helpers.
28pub struct OwnershipService;
29
30/// Ensure ownership lookup indexes exist when the active backend supports them (idempotent).
31/// # Errors
32///
33/// Returns an error when the requested operation cannot be completed.
34pub async fn ensure_lookup_indexes(v: &Valence) -> Result<()> {
35    let sys = system_valence(v);
36    let q = concat!(
37        "DEFINE INDEX IF NOT EXISTS idx_valence_data_ownership_model_status ",
38        "ON TABLE valence_data_ownership COLUMNS valence_model, status"
39    );
40    let compiled = crate::compiled_query::CompiledQuery::new(q.to_string(), vec![]);
41
42    let mut backends: Vec<Arc<dyn DatabaseBackend>> = Vec::new();
43
44    let mut push_unique = |backend: Arc<dyn DatabaseBackend>| {
45        if !backends.iter().any(|b| Arc::ptr_eq(b, &backend)) {
46            backends.push(backend);
47        }
48    };
49
50    if let Ok(backend) = sys.backend_for_table("valence_data_ownership") {
51        push_unique(backend);
52    }
53
54    if ownership_colocate_enabled() {
55        for table in SchemaRegistry::global().list_schemas() {
56            if skip_ownership_for_table(table) {
57                continue;
58            }
59            if let Ok(backend) = OwnershipService::ownership_backend(table, &sys) {
60                push_unique(backend);
61            }
62        }
63    }
64
65    for backend in backends {
66        backend
67            .execute_compiled_query(&compiled)
68            .await
69            .map_err(|e| Error::database(e.to_string()))?;
70    }
71    Ok(())
72}
73
74#[cfg(test)]
75mod tests {
76    use super::helpers::{
77        normalize_record_id_for_ownership, owner_id_query_values, owner_ref_from_ownership_json,
78        parse_owner_kind, skip_ownership_for_table,
79    };
80    use crate::owner_ref::{OwnerKind, OwnerRef};
81    use serde_json::json;
82
83    #[test]
84    fn owner_id_query_values_matches_session_and_signup_user_ids() {
85        assert_eq!(
86            owner_id_query_values("user:abc-123", "user"),
87            vec!["user:abc-123".to_string(), "abc-123".to_string()]
88        );
89        assert_eq!(
90            owner_id_query_values("abc-123", "user"),
91            vec!["abc-123".to_string(), "user:abc-123".to_string()]
92        );
93        assert_eq!(
94            owner_id_query_values("service-bot", "service"),
95            vec!["service-bot".to_string()]
96        );
97    }
98
99    #[test]
100    fn parse_owner_kind_known_and_default() {
101        assert_eq!(parse_owner_kind("user"), OwnerKind::User);
102        assert_eq!(parse_owner_kind("account"), OwnerKind::Account);
103        assert_eq!(parse_owner_kind("application"), OwnerKind::Application);
104        assert_eq!(parse_owner_kind("service"), OwnerKind::Service);
105        assert_eq!(parse_owner_kind("anything_else"), OwnerKind::System);
106    }
107
108    #[test]
109    fn skip_ownership_for_platform_tables_only() {
110        assert!(skip_ownership_for_table("valence_data_ownership"));
111        assert!(skip_ownership_for_table("valence_ownership_transfer"));
112        assert!(!skip_ownership_for_table("counter"));
113    }
114
115    #[test]
116    fn owner_ref_from_ownership_json_extracts_fields() {
117        let v = json!({
118            "owner_id": "u1",
119            "owner_type": "user",
120        });
121        let r = owner_ref_from_ownership_json(&v).expect("parsed");
122        assert_eq!(r.owner_id, "u1");
123        assert_eq!(r.owner_kind, OwnerKind::User);
124        assert!(owner_ref_from_ownership_json(&json!({})).is_none());
125    }
126
127    #[test]
128    fn normalize_record_id_strips_table_prefix() {
129        assert_eq!(
130            normalize_record_id_for_ownership("counter:singleton"),
131            "singleton"
132        );
133        assert_eq!(normalize_record_id_for_ownership("bare-id"), "bare-id");
134    }
135
136    #[test]
137    fn normalize_record_id_keeps_composite_id_without_known_table() {
138        assert_eq!(
139            normalize_record_id_for_ownership("default-deployment:wiztop:0"),
140            "default-deployment:wiztop:0"
141        );
142    }
143
144    #[test]
145    fn owner_ref_round_trip_from_stored_json() {
146        let v = json!({"owner_id": "svc", "owner_type": "service"});
147        let r = owner_ref_from_ownership_json(&v).unwrap();
148        assert_eq!(
149            r,
150            OwnerRef {
151                owner_id: "svc".into(),
152                owner_kind: OwnerKind::Service,
153            }
154        );
155    }
156
157    #[test]
158    fn ownership_get_join_enabled_defaults_on() {
159        assert!(super::helpers::ownership_get_join_enabled());
160    }
161
162    #[test]
163    fn ownership_colocate_enabled_defaults_on() {
164        assert!(super::helpers::ownership_colocate_enabled());
165    }
166
167    #[test]
168    fn pending_deletion_status_blocks_reads() {
169        use super::helpers::OwnershipGateStatus;
170        use super::OwnershipService;
171        use crate::error::Error;
172
173        let err = OwnershipService::apply_pending_deletion_gate(
174            "widget",
175            "w1",
176            OwnershipGateStatus::Status("pending_deletion".into()),
177        )
178        .expect_err("pending deletion must block");
179        assert!(matches!(err, Error::PendingDeletion(_)));
180
181        OwnershipService::apply_pending_deletion_gate("widget", "w1", OwnershipGateStatus::Absent)
182            .expect("absent ownership allows");
183    }
184}