use std::collections::BTreeSet;
use std::sync::{Arc, Mutex};
use type_bridge_contract::reserved::{
LEGACY_CUTOVER_ANCHOR_ENTITY, LEGACY_CUTOVER_ANCHOR_FINGERPRINT, LEGACY_CUTOVER_ANCHOR_KEY,
LEGACY_CUTOVER_ANCHOR_SCOPE, LEGACY_CUTOVER_ANCHOR_SINGLETON_KEY,
LEGACY_CUTOVER_SENTINEL_APP_LABEL, LEGACY_CUTOVER_SENTINEL_APPLIED_AT,
LEGACY_LEDGER_APPLIED_ENTITY, LEGACY_WRITER_GUARD_QUERY_TAG, MANAGED_CONTROL_ENTITY,
MANAGED_CONTROL_LEASE_FENCE, MANAGED_CONTROL_LEASE_HOLDER, MANAGED_CONTROL_LEASE_STATE,
MANAGED_CONTROL_SCOPE,
};
use type_bridge_orm::session::backend::{BoxFuture, DriverBackend, QueryResult, TransactionOps};
use type_bridge_orm::{ClassifiedCommitError, CommitFailureCertainty, OrmError, TxType};
use type_bridge_schema_compat::{LEGACY_LEDGER_SCHEMA_TYPEQL, MANAGED_FENCE_SCHEMA_TYPEQL};
#[derive(Debug, Clone, PartialEq)]
pub enum MockEvent {
OpenTx(TxType),
Query(TxType, String),
Commit,
Rollback,
Close,
}
pub type EventLog = Arc<Mutex<Vec<MockEvent>>>;
fn all_state_labels() -> BTreeSet<String> {
let schema = crate::state::schema::migration_state_schema();
schema
.attributes
.keys()
.chain(schema.entities.keys())
.chain(schema.relations.keys())
.cloned()
.collect()
}
fn state_label_documents(labels: &BTreeSet<String>, typeql: &str) -> Vec<serde_json::Value> {
let schema = crate::state::schema::migration_state_schema();
let expected = if typeql.contains("match attribute $t") {
schema.attributes.keys().collect::<Vec<_>>()
} else if typeql.contains("match entity $t") {
schema.entities.keys().collect::<Vec<_>>()
} else {
schema.relations.keys().collect::<Vec<_>>()
};
expected
.into_iter()
.filter(|label| labels.contains(*label))
.map(|label| serde_json::json!({"label": label}))
.collect()
}
const MOCK_CUTOVER_FINGERPRINT: &str =
"0000000000000000000000000000000000000000000000000000000000000000";
fn legacy_guard_documents(
typeql: &str,
cutover_present: bool,
state_labels: Option<&BTreeSet<String>>,
) -> Vec<serde_json::Value> {
if typeql.contains("match entity $t") {
let mut values = state_labels
.map(|labels| state_label_documents(labels, typeql))
.unwrap_or_default();
if cutover_present {
values.push(serde_json::json!({"label": MANAGED_CONTROL_ENTITY}));
values.push(serde_json::json!({"label": LEGACY_CUTOVER_ANCHOR_ENTITY}));
}
return values;
}
if typeql.contains("match attribute $t") {
let mut values = state_labels
.map(|labels| state_label_documents(labels, typeql))
.unwrap_or_default();
if cutover_present {
values.extend([
serde_json::json!({"label": MANAGED_CONTROL_SCOPE}),
serde_json::json!({"label": MANAGED_CONTROL_LEASE_HOLDER}),
serde_json::json!({"label": MANAGED_CONTROL_LEASE_FENCE}),
serde_json::json!({"label": MANAGED_CONTROL_LEASE_STATE}),
serde_json::json!({"label": LEGACY_CUTOVER_ANCHOR_KEY}),
serde_json::json!({"label": LEGACY_CUTOVER_ANCHOR_SCOPE}),
serde_json::json!({"label": LEGACY_CUTOVER_ANCHOR_FINGERPRINT}),
]);
}
return values;
}
if !cutover_present {
return Vec::new();
}
if typeql.contains(&format!("isa {MANAGED_CONTROL_ENTITY}")) {
if typeql.contains("\"scope\": $scope") {
return vec![serde_json::json!({
"scope": "mock-scope",
"fence": "1",
"state": "free",
})];
}
if typeql.contains("\"holder\": $holder") {
return Vec::new();
}
return vec![serde_json::json!({"exists": true})];
}
if typeql.contains(&format!("isa {LEGACY_CUTOVER_ANCHOR_ENTITY}")) {
if typeql.contains("\"fingerprint\": $fingerprint") {
return vec![serde_json::json!({
"key": LEGACY_CUTOVER_ANCHOR_SINGLETON_KEY,
"scope": "mock-scope",
"fingerprint": MOCK_CUTOVER_FINGERPRINT,
})];
}
return vec![serde_json::json!({"exists": true})];
}
if typeql.contains(&format!("isa {LEGACY_LEDGER_APPLIED_ENTITY}")) {
if typeql.contains("\"checksum\": $checksum") {
return vec![serde_json::json!({
"app": LEGACY_CUTOVER_SENTINEL_APP_LABEL,
"applied": LEGACY_CUTOVER_SENTINEL_APPLIED_AT,
"checksum": MOCK_CUTOVER_FINGERPRINT,
})];
}
return vec![serde_json::json!({"exists": true})];
}
Vec::new()
}
pub struct MockMigrationBackend {
log: EventLog,
query_count: Arc<Mutex<usize>>,
fail_on_query_index: Option<usize>,
scripted_responses: Option<Arc<Mutex<Vec<QueryResult>>>>,
commit_count: Arc<Mutex<usize>>,
fail_on_commit_index: Option<usize>,
commit_failure_certainty: Option<CommitFailureCertainty>,
close_count: Arc<Mutex<usize>>,
fail_on_close_index: Option<usize>,
legacy_cutover_present: bool,
legacy_state_labels: Option<Arc<Mutex<BTreeSet<String>>>>,
}
impl MockMigrationBackend {
pub fn new(fail_on_query_index: Option<usize>) -> (Self, EventLog) {
let log = Arc::new(Mutex::new(Vec::new()));
let backend = Self {
log: Arc::clone(&log),
query_count: Arc::new(Mutex::new(0)),
fail_on_query_index,
scripted_responses: None,
commit_count: Arc::new(Mutex::new(0)),
fail_on_commit_index: None,
commit_failure_certainty: None,
close_count: Arc::new(Mutex::new(0)),
fail_on_close_index: None,
legacy_cutover_present: false,
legacy_state_labels: None,
};
(backend, log)
}
pub fn with_responses(responses: Vec<QueryResult>) -> (Self, EventLog) {
let log = Arc::new(Mutex::new(Vec::new()));
let backend = Self {
log: Arc::clone(&log),
query_count: Arc::new(Mutex::new(0)),
fail_on_query_index: None,
scripted_responses: Some(Arc::new(Mutex::new(responses))),
commit_count: Arc::new(Mutex::new(0)),
fail_on_commit_index: None,
commit_failure_certainty: None,
close_count: Arc::new(Mutex::new(0)),
fail_on_close_index: None,
legacy_cutover_present: false,
legacy_state_labels: None,
};
(backend, log)
}
pub fn with_legacy_cutover() -> (Self, EventLog) {
let (mut backend, log) = Self::new(None);
backend.legacy_cutover_present = true;
backend.legacy_state_labels = Some(Arc::new(Mutex::new(all_state_labels())));
(backend, log)
}
pub fn with_state_read_responses(responses: Vec<QueryResult>) -> (Self, EventLog) {
let (mut backend, log) = Self::with_responses(responses);
backend.legacy_state_labels = Some(Arc::new(Mutex::new(all_state_labels())));
(backend, log)
}
pub fn with_state_read_and_close_failure(
fail_on_query_index: usize,
fail_on_close_index: usize,
) -> (Self, EventLog) {
let (mut backend, log) = Self::new(Some(fail_on_query_index));
backend.legacy_state_labels = Some(Arc::new(Mutex::new(all_state_labels())));
backend.fail_on_close_index = Some(fail_on_close_index);
(backend, log)
}
pub fn with_partial_state_schema(
missing: &[&str],
legacy_cutover_present: bool,
) -> (Self, EventLog, Arc<Mutex<BTreeSet<String>>>) {
let (mut backend, log) = Self::new(None);
let mut labels = all_state_labels();
for label in missing {
labels.remove(*label);
}
let labels = Arc::new(Mutex::new(labels));
backend.legacy_cutover_present = legacy_cutover_present;
backend.legacy_state_labels = Some(Arc::clone(&labels));
(backend, log, labels)
}
pub fn with_commit_failure(fail_on_commit_index: usize) -> (Self, EventLog) {
let (mut backend, log) = Self::new(None);
backend.fail_on_commit_index = Some(fail_on_commit_index);
(backend, log)
}
pub fn with_definitely_aborted_commit_failure(fail_on_commit_index: usize) -> (Self, EventLog) {
let (mut backend, log) = Self::new(None);
backend.fail_on_commit_index = Some(fail_on_commit_index);
backend.commit_failure_certainty = Some(CommitFailureCertainty::DefinitelyAborted);
(backend, log)
}
}
impl DriverBackend for MockMigrationBackend {
fn open_transaction(
&self,
_database: &str,
tx_type: TxType,
) -> BoxFuture<'_, Result<Box<dyn TransactionOps>, OrmError>> {
self.log.lock().unwrap().push(MockEvent::OpenTx(tx_type));
let log = Arc::clone(&self.log);
let query_count = Arc::clone(&self.query_count);
let fail_on = self.fail_on_query_index;
let scripted = self.scripted_responses.as_ref().map(Arc::clone);
let commit_count = Arc::clone(&self.commit_count);
let fail_on_commit_index = self.fail_on_commit_index;
let commit_failure_certainty = self.commit_failure_certainty;
let close_count = Arc::clone(&self.close_count);
let fail_on_close_index = self.fail_on_close_index;
let legacy_cutover_present = self.legacy_cutover_present;
let legacy_state_labels = self.legacy_state_labels.as_ref().map(Arc::clone);
Box::pin(async move {
let tx: Box<dyn TransactionOps> = Box::new(MockMigrationTransaction {
tx_type,
log,
query_count,
fail_on,
scripted_responses: scripted,
commit_count,
fail_on_commit_index,
commit_failure_certainty,
close_count,
fail_on_close_index,
legacy_cutover_present,
legacy_state_labels,
});
Ok(tx)
})
}
fn is_open(&self) -> bool {
true
}
}
struct MockMigrationTransaction {
tx_type: TxType,
log: EventLog,
query_count: Arc<Mutex<usize>>,
fail_on: Option<usize>,
scripted_responses: Option<Arc<Mutex<Vec<QueryResult>>>>,
commit_count: Arc<Mutex<usize>>,
fail_on_commit_index: Option<usize>,
commit_failure_certainty: Option<CommitFailureCertainty>,
close_count: Arc<Mutex<usize>>,
fail_on_close_index: Option<usize>,
legacy_cutover_present: bool,
legacy_state_labels: Option<Arc<Mutex<BTreeSet<String>>>>,
}
impl TransactionOps for MockMigrationTransaction {
fn schema_snapshot(&mut self) -> BoxFuture<'_, Result<Option<String>, OrmError>> {
let snapshot = self
.legacy_cutover_present
.then(|| format!("{MANAGED_FENCE_SCHEMA_TYPEQL}\n{LEGACY_LEDGER_SCHEMA_TYPEQL}"));
Box::pin(async move { Ok(snapshot) })
}
fn query(&mut self, typeql: &str) -> BoxFuture<'_, Result<QueryResult, OrmError>> {
if typeql.starts_with(LEGACY_WRITER_GUARD_QUERY_TAG) {
let labels = self
.legacy_state_labels
.as_ref()
.map(|labels| labels.lock().unwrap());
let values =
legacy_guard_documents(typeql, self.legacy_cutover_present, labels.as_deref());
return Box::pin(async move { Ok(QueryResult::Documents(values)) });
}
if crate::state::typedb::is_legacy_state_schema_probe_query(typeql) {
let values = self
.legacy_state_labels
.as_ref()
.map(|labels| state_label_documents(&labels.lock().unwrap(), typeql))
.unwrap_or_default();
return Box::pin(async move { Ok(QueryResult::Documents(values)) });
}
if typeql.contains("fetch { \"label\": label($t) }")
&& let Some(labels) = &self.legacy_state_labels
{
let values = state_label_documents(&labels.lock().unwrap(), typeql);
return Box::pin(async move { Ok(QueryResult::Documents(values)) });
}
let idx = {
let mut count = self.query_count.lock().unwrap();
let current = *count;
*count += 1;
current
};
let typeql_owned = typeql.to_string();
let tx_type = self.tx_type;
self.log
.lock()
.unwrap()
.push(MockEvent::Query(tx_type, typeql_owned));
if typeql.trim_start().starts_with("define")
&& let Some(labels) = &self.legacy_state_labels
{
let schema = crate::state::schema::migration_state_schema();
let mut installed = labels.lock().unwrap();
for label in schema.attributes.keys() {
if typeql.contains(&format!("attribute {label}")) {
installed.insert(label.clone());
}
}
for label in schema.entities.keys() {
if typeql.contains(&format!("entity {label}")) {
installed.insert(label.clone());
}
}
}
let response: Result<QueryResult, OrmError> =
if let Some(scripted) = &self.scripted_responses {
let mut responses = scripted.lock().unwrap();
if responses.is_empty() {
Ok(QueryResult::Ok)
} else {
Ok(responses.remove(0))
}
} else {
let should_fail = self.fail_on == Some(idx);
if should_fail {
Err(OrmError::Transaction(
"injected query failure for testing".to_string(),
))
} else {
Ok(QueryResult::Ok)
}
};
Box::pin(async move { response })
}
fn commit(&mut self) -> BoxFuture<'_, Result<(), OrmError>> {
self.log.lock().unwrap().push(MockEvent::Commit);
let index = {
let mut count = self.commit_count.lock().unwrap();
let index = *count;
*count += 1;
index
};
let fail = self.fail_on_commit_index == Some(index);
let certainty = self.commit_failure_certainty;
Box::pin(async move {
if fail {
Err(match certainty {
Some(_) => OrmError::Transaction(
"Commit failed: injected rejected commit response for testing".to_string(),
),
None => OrmError::Transaction(
"injected ambiguous commit response for testing".to_string(),
),
})
} else {
Ok(())
}
})
}
fn commit_classified(&mut self) -> BoxFuture<'_, Result<(), ClassifiedCommitError>> {
self.log.lock().unwrap().push(MockEvent::Commit);
let index = {
let mut count = self.commit_count.lock().unwrap();
let index = *count;
*count += 1;
index
};
let fail = self.fail_on_commit_index == Some(index);
let certainty = self.commit_failure_certainty;
Box::pin(async move {
if fail {
Err(match certainty {
Some(certainty) => ClassifiedCommitError::Driver {
certainty,
message: "injected rejected commit response for testing".to_string(),
},
None => ClassifiedCommitError::from(OrmError::Transaction(
"injected ambiguous commit response for testing".to_string(),
)),
})
} else {
Ok(())
}
})
}
fn rollback(&mut self) -> BoxFuture<'_, Result<(), OrmError>> {
self.log.lock().unwrap().push(MockEvent::Rollback);
Box::pin(async { Ok(()) })
}
fn close(&mut self) -> BoxFuture<'_, Result<(), OrmError>> {
self.log.lock().unwrap().push(MockEvent::Close);
let index = {
let mut count = self.close_count.lock().unwrap();
let index = *count;
*count += 1;
index
};
let fail = self.fail_on_close_index == Some(index);
Box::pin(async move {
if fail {
Err(OrmError::Transaction(
"injected close failure for testing".to_owned(),
))
} else {
Ok(())
}
})
}
}