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).
31pub async fn ensure_lookup_indexes(v: &Valence) -> Result<()> {
32    let sys = system_valence(v);
33    let q = concat!(
34        "DEFINE INDEX IF NOT EXISTS idx_valence_data_ownership_model_status ",
35        "ON TABLE valence_data_ownership COLUMNS valence_model, status"
36    );
37    let compiled = crate::compiled_query::CompiledQuery::new(q.to_string(), vec![]);
38
39    let mut backends: Vec<Arc<dyn DatabaseBackend>> = Vec::new();
40
41    let mut push_unique = |backend: Arc<dyn DatabaseBackend>| {
42        if !backends.iter().any(|b| Arc::ptr_eq(b, &backend)) {
43            backends.push(backend);
44        }
45    };
46
47    if let Ok(backend) = sys.backend_for_table("valence_data_ownership") {
48        push_unique(backend);
49    }
50
51    if ownership_colocate_enabled() {
52        for table in SchemaRegistry::global().list_schemas() {
53            if skip_ownership_for_table(table) {
54                continue;
55            }
56            if let Ok(backend) = OwnershipService::ownership_backend(table, &sys) {
57                push_unique(backend);
58            }
59        }
60    }
61
62    for backend in backends {
63        backend
64            .execute_compiled_query(&compiled)
65            .await
66            .map_err(|e| Error::Database(e.to_string()))?;
67    }
68    Ok(())
69}
70
71#[cfg(test)]
72mod tests {
73    use super::helpers::{
74        normalize_record_id_for_ownership, owner_id_query_values, owner_ref_from_ownership_json,
75        parse_owner_kind, skip_ownership_for_table,
76    };
77    use crate::owner_ref::{OwnerKind, OwnerRef};
78    use serde_json::json;
79
80    #[test]
81    fn owner_id_query_values_matches_session_and_signup_user_ids() {
82        assert_eq!(
83            owner_id_query_values("user:abc-123", "user"),
84            vec!["user:abc-123".to_string(), "abc-123".to_string()]
85        );
86        assert_eq!(
87            owner_id_query_values("abc-123", "user"),
88            vec!["abc-123".to_string(), "user:abc-123".to_string()]
89        );
90        assert_eq!(
91            owner_id_query_values("service-bot", "service"),
92            vec!["service-bot".to_string()]
93        );
94    }
95
96    #[test]
97    fn parse_owner_kind_known_and_default() {
98        assert_eq!(parse_owner_kind("user"), OwnerKind::User);
99        assert_eq!(parse_owner_kind("account"), OwnerKind::Account);
100        assert_eq!(parse_owner_kind("application"), OwnerKind::Application);
101        assert_eq!(parse_owner_kind("service"), OwnerKind::Service);
102        assert_eq!(parse_owner_kind("anything_else"), OwnerKind::System);
103    }
104
105    #[test]
106    fn skip_ownership_for_platform_tables_only() {
107        assert!(skip_ownership_for_table("valence_data_ownership"));
108        assert!(skip_ownership_for_table("valence_ownership_transfer"));
109        assert!(!skip_ownership_for_table("counter"));
110    }
111
112    #[test]
113    fn owner_ref_from_ownership_json_extracts_fields() {
114        let v = json!({
115            "owner_id": "u1",
116            "owner_type": "user",
117        });
118        let r = owner_ref_from_ownership_json(&v).expect("parsed");
119        assert_eq!(r.owner_id, "u1");
120        assert_eq!(r.owner_kind, OwnerKind::User);
121        assert!(owner_ref_from_ownership_json(&json!({})).is_none());
122    }
123
124    #[test]
125    fn normalize_record_id_strips_table_prefix() {
126        assert_eq!(
127            normalize_record_id_for_ownership("counter:singleton"),
128            "singleton"
129        );
130        assert_eq!(normalize_record_id_for_ownership("bare-id"), "bare-id");
131    }
132
133    #[test]
134    fn normalize_record_id_keeps_composite_id_without_known_table() {
135        assert_eq!(
136            normalize_record_id_for_ownership("default-deployment:wiztop:0"),
137            "default-deployment:wiztop:0"
138        );
139    }
140
141    #[test]
142    fn owner_ref_round_trip_from_stored_json() {
143        let v = json!({"owner_id": "svc", "owner_type": "service"});
144        let r = owner_ref_from_ownership_json(&v).unwrap();
145        assert_eq!(
146            r,
147            OwnerRef {
148                owner_id: "svc".into(),
149                owner_kind: OwnerKind::Service,
150            }
151        );
152    }
153
154    #[test]
155    fn ownership_get_join_enabled_defaults_on() {
156        assert!(super::helpers::ownership_get_join_enabled());
157    }
158
159    #[test]
160    fn ownership_colocate_enabled_defaults_on() {
161        assert!(super::helpers::ownership_colocate_enabled());
162    }
163}