#![allow(missing_docs)]
use sea_orm::ActiveValue::Set;
use sea_orm::Database;
use sea_orm::entity::prelude::*;
use sea_orm::sea_query::OnConflict;
use std::sync::Arc;
pub const SYNC_STATUS_PENDING: &str = "pending";
pub const SYNC_STATUS_SYNCED: &str = "synced";
pub const SYNC_STATUS_FAILED: &str = "failed";
const BATCH_SYNC_CHUNK_SIZE: usize = 500;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "global_index")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: String,
pub table_name: String,
pub record_id: String,
pub shard_id: i32,
pub index_key: String,
pub index_value: String,
pub created_at: String,
pub updated_at: String,
pub last_modified: String,
pub sync_status: String,
}
impl sea_orm::ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
#[derive(Debug, Clone)]
pub struct IndexEntry {
pub table_name: String,
pub record_id: String,
pub shard_id: u32,
pub index_key: String,
pub index_value: String,
}
#[derive(Debug, Clone)]
pub enum SyncEvent {
Insert(IndexEntry),
Update(IndexEntry),
Delete(IndexEntry),
}
#[derive(Debug)]
pub struct SyncResult {
pub success: bool,
pub synced_count: usize,
pub failed_count: usize,
pub errors: Vec<String>,
}
pub struct GlobalIndex {
pool: Arc<sea_orm::DbConn>,
}
impl GlobalIndex {
pub async fn new(db_url: &str) -> Result<Self, sea_orm::DbErr> {
let db = Database::connect(db_url).await?;
Self::create_table_if_not_exists(&db).await?;
Ok(Self { pool: Arc::new(db) })
}
async fn create_table_if_not_exists(db: &sea_orm::DbConn) -> Result<(), sea_orm::DbErr> {
use sea_orm::Schema;
let builder = db.get_database_backend();
let schema = Schema::new(builder);
let mut create_table_stmt = schema.create_table_from_entity(Entity);
create_table_stmt.if_not_exists();
db.execute(&create_table_stmt).await?;
Ok(())
}
pub async fn query_by_index(
&self,
table_name: &str,
index_key: &str,
index_value: &str,
) -> Result<Vec<IndexEntry>, sea_orm::DbErr> {
let results = Entity::find()
.filter(Column::TableName.eq(table_name))
.filter(Column::IndexKey.eq(index_key))
.filter(Column::IndexValue.eq(index_value))
.all(&*self.pool)
.await?;
Ok(results
.into_iter()
.map(|m| IndexEntry {
table_name: m.table_name,
record_id: m.record_id,
shard_id: m.shard_id as u32,
index_key: m.index_key,
index_value: m.index_value,
})
.collect())
}
pub async fn batch_sync(&self, entries: Vec<IndexEntry>) -> Result<SyncResult, sea_orm::DbErr> {
if entries.is_empty() {
return Ok(SyncResult {
success: true,
synced_count: 0,
failed_count: 0,
errors: Vec::new(),
});
}
let total = entries.len();
let (total_synced, all_errors) = self.chunk_and_upsert(&entries, BATCH_SYNC_CHUNK_SIZE).await;
let failed_count = total.saturating_sub(total_synced);
Ok(SyncResult {
success: all_errors.is_empty(),
synced_count: total_synced,
failed_count,
errors: all_errors,
})
}
async fn chunk_and_upsert(&self, entries: &[IndexEntry], chunk_size: usize) -> (usize, Vec<String>) {
let mut total_synced = 0usize;
let mut all_errors: Vec<String> = Vec::new();
for (batch_idx, chunk) in entries.chunks(chunk_size).enumerate() {
let active_models: Vec<ActiveModel> = chunk
.iter()
.map(|entry| {
let now = chrono::Utc::now().to_rfc3339();
let id = format!("{}:{}:{}", entry.table_name, entry.index_key, entry.record_id);
ActiveModel {
id: Set(id),
table_name: Set(entry.table_name.clone()),
record_id: Set(entry.record_id.clone()),
shard_id: Set(entry.shard_id as i32),
index_key: Set(entry.index_key.clone()),
index_value: Set(entry.index_value.clone()),
created_at: Set(now.clone()),
updated_at: Set(now.clone()),
last_modified: Set(now),
sync_status: Set(SYNC_STATUS_SYNCED.to_string()),
}
})
.collect();
match Entity::insert_many(active_models)
.on_conflict(
OnConflict::columns([Column::Id])
.update_columns([
Column::TableName,
Column::RecordId,
Column::ShardId,
Column::IndexKey,
Column::IndexValue,
Column::UpdatedAt,
Column::LastModified,
Column::SyncStatus,
])
.to_owned(),
)
.exec(&*self.pool)
.await
{
Ok(_) => {
total_synced += chunk.len();
}
Err(e) => {
all_errors.push(format!(
"Batch {} (entries {}-{}) failed: {:?}",
batch_idx,
batch_idx * chunk_size,
(batch_idx + 1) * chunk_size - 1,
e
));
}
}
}
(total_synced, all_errors)
}
}
#[cfg(test)]
mod tests {
use super::*;
async fn create_global_index() -> GlobalIndex {
GlobalIndex::new("sqlite::memory:")
.await
.expect("Failed to create GlobalIndex")
}
fn make_entry(table: &str, record_id: &str, shard: u32, key: &str, value: &str) -> IndexEntry {
IndexEntry {
table_name: table.to_string(),
record_id: record_id.to_string(),
shard_id: shard,
index_key: key.to_string(),
index_value: value.to_string(),
}
}
#[test]
fn test_sync_status_constants() {
assert_eq!(SYNC_STATUS_PENDING, "pending");
assert_eq!(SYNC_STATUS_SYNCED, "synced");
assert_eq!(SYNC_STATUS_FAILED, "failed");
}
#[test]
fn test_index_entry_construction() {
let entry = make_entry("users", "user_1", 1, "email", "test@example.com");
assert_eq!(entry.table_name, "users");
assert_eq!(entry.record_id, "user_1");
assert_eq!(entry.shard_id, 1);
assert_eq!(entry.index_key, "email");
assert_eq!(entry.index_value, "test@example.com");
}
#[test]
fn test_sync_event_insert_variant() {
let entry = make_entry("users", "user_1", 1, "email", "test@example.com");
let event = SyncEvent::Insert(entry.clone());
match event {
SyncEvent::Insert(e) => {
assert_eq!(e.table_name, "users");
assert_eq!(e.record_id, "user_1");
}
_ => panic!("expected Insert"),
}
}
#[test]
fn test_sync_event_update_variant() {
let entry = make_entry("users", "user_1", 1, "email", "updated@example.com");
let event = SyncEvent::Update(entry);
assert!(matches!(event, SyncEvent::Update(_)));
}
#[test]
fn test_sync_event_delete_variant() {
let entry = make_entry("users", "user_1", 1, "email", "test@example.com");
let event = SyncEvent::Delete(entry);
assert!(matches!(event, SyncEvent::Delete(_)));
}
#[test]
fn test_sync_result_success_construction() {
let result = SyncResult {
success: true,
synced_count: 5,
failed_count: 0,
errors: vec![],
};
assert!(result.success);
assert_eq!(result.synced_count, 5);
assert_eq!(result.failed_count, 0);
assert!(result.errors.is_empty());
}
#[test]
fn test_sync_result_failure_construction() {
let result = SyncResult {
success: false,
synced_count: 3,
failed_count: 2,
errors: vec!["batch 1 failed".to_string()],
};
assert!(!result.success);
assert_eq!(result.synced_count, 3);
assert_eq!(result.failed_count, 2);
assert_eq!(result.errors.len(), 1);
}
#[cfg(all(feature = "sqlite", feature = "runtime-tokio-rustls"))]
#[tokio::test]
async fn test_global_index_new_creates_table() {
let index = create_global_index().await;
let results = index.query_by_index("any", "any", "any").await;
assert!(results.is_ok());
assert!(results.unwrap().is_empty());
}
#[cfg(all(feature = "sqlite", feature = "runtime-tokio-rustls"))]
#[tokio::test]
async fn test_batch_sync_empty_returns_success() {
let index = create_global_index().await;
let result = index.batch_sync(vec![]).await.expect("batch_sync failed");
assert!(result.success);
assert_eq!(result.synced_count, 0);
assert_eq!(result.failed_count, 0);
assert!(result.errors.is_empty());
}
#[cfg(all(feature = "sqlite", feature = "runtime-tokio-rustls"))]
#[tokio::test]
async fn test_batch_sync_single_entry() {
let index = create_global_index().await;
let entry = make_entry("users", "user_1", 1, "email", "test@example.com");
let result = index.batch_sync(vec![entry]).await.expect("batch_sync failed");
assert!(result.success);
assert_eq!(result.synced_count, 1);
assert_eq!(result.failed_count, 0);
let queried = index
.query_by_index("users", "email", "test@example.com")
.await
.expect("query failed");
assert_eq!(queried.len(), 1);
assert_eq!(queried[0].record_id, "user_1");
assert_eq!(queried[0].shard_id, 1);
}
#[cfg(all(feature = "sqlite", feature = "runtime-tokio-rustls"))]
#[tokio::test]
async fn test_batch_sync_multiple_entries() {
let index = create_global_index().await;
let entries = vec![
make_entry("users", "user_1", 0, "email", "a@example.com"),
make_entry("users", "user_2", 1, "email", "b@example.com"),
make_entry("users", "user_3", 2, "email", "c@example.com"),
];
let result = index.batch_sync(entries).await.expect("batch_sync failed");
assert!(result.success);
assert_eq!(result.synced_count, 3);
assert_eq!(result.failed_count, 0);
let queried = index
.query_by_index("users", "email", "a@example.com")
.await
.expect("query failed");
assert_eq!(queried.len(), 1);
assert_eq!(queried[0].record_id, "user_1");
}
#[cfg(all(feature = "sqlite", feature = "runtime-tokio-rustls"))]
#[tokio::test]
async fn test_batch_sync_upsert_on_conflict() {
let index = create_global_index().await;
let entry_v1 = make_entry("users", "user_1", 0, "email", "old@example.com");
index.batch_sync(vec![entry_v1]).await.expect("first sync failed");
let entry_v2 = make_entry("users", "user_1", 1, "email", "new@example.com");
let result = index.batch_sync(vec![entry_v2]).await.expect("second sync failed");
assert!(result.success);
assert_eq!(result.synced_count, 1);
let all = index
.query_by_index("users", "email", "new@example.com")
.await
.expect("query failed");
assert_eq!(all.len(), 1);
assert_eq!(all[0].shard_id, 1);
let old = index
.query_by_index("users", "email", "old@example.com")
.await
.expect("query failed");
assert!(old.is_empty());
}
#[cfg(all(feature = "sqlite", feature = "runtime-tokio-rustls"))]
#[tokio::test]
async fn test_query_by_index_no_match() {
let index = create_global_index().await;
let entry = make_entry("users", "user_1", 0, "email", "test@example.com");
index.batch_sync(vec![entry]).await.expect("sync failed");
let results = index
.query_by_index("users", "email", "nonexistent@example.com")
.await
.expect("query failed");
assert!(results.is_empty());
let results = index
.query_by_index("orders", "email", "test@example.com")
.await
.expect("query failed");
assert!(results.is_empty());
let results = index
.query_by_index("users", "phone", "test@example.com")
.await
.expect("query failed");
assert!(results.is_empty());
}
#[cfg(all(feature = "sqlite", feature = "runtime-tokio-rustls"))]
#[tokio::test]
async fn test_query_by_index_multiple_matches() {
let index = create_global_index().await;
let entries = vec![
make_entry("users", "user_1", 0, "status", "active"),
make_entry("users", "user_2", 1, "status", "active"),
make_entry("users", "user_3", 2, "status", "inactive"),
];
index.batch_sync(entries).await.expect("sync failed");
let results = index
.query_by_index("users", "status", "active")
.await
.expect("query failed");
assert_eq!(results.len(), 2);
}
#[cfg(all(feature = "sqlite", feature = "runtime-tokio-rustls"))]
#[tokio::test]
async fn test_batch_sync_large_batch() {
let index = create_global_index().await;
let entries: Vec<IndexEntry> = (0..600)
.map(|i| make_entry("users", &format!("user_{}", i), i % 4, "id", &i.to_string()))
.collect();
let result = index.batch_sync(entries).await.expect("batch_sync failed");
assert!(result.success);
assert_eq!(result.synced_count, 600);
assert_eq!(result.failed_count, 0);
let queried = index.query_by_index("users", "id", "599").await.expect("query failed");
assert_eq!(queried.len(), 1);
assert_eq!(queried[0].record_id, "user_599");
}
}