use crate::state::{ChannelId, Seq, TemporaryId};
use helix_core::effect::{MonotonicUpsertSpec, Row, StorageOp, UpsertSpec};
use helix_core::{Correlation, Effect};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecoveryLedgerEntry {
pub tenant_id: String,
pub channel_id: String,
pub event_seq: u64,
pub event_kind: String,
pub message_id: Option<String>,
pub event_hash: String,
pub coverage_id: String,
pub applied_at_ms: i64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecoveryCoverage {
pub coverage_id: String,
pub tenant_id: String,
pub channel_id: String,
pub from_seq: u64,
pub to_seq: u64,
pub event_count: u64,
pub facts_hash: String,
pub correlation_id: String,
pub committed_at_ms: i64,
}
pub fn recovery_tenant_actor_scope(tenant_id: &str, actor_id: &str) -> Option<String> {
if tenant_id.is_empty() || actor_id.is_empty() {
return None;
}
Some(format!("{}:{}:{}", tenant_id.len(), tenant_id, actor_id))
}
pub fn recovery_ledger_op(entry: RecoveryLedgerEntry) -> StorageOp {
use helix_core::effect::SqlValue;
StorageOp::BatchUpsert(UpsertSpec {
version_column: None,
update_guard: None,
table: "channel_event_ledger",
rows: vec![vec![
("tenant_id".to_string(), SqlValue::Text(entry.tenant_id)),
("channel_id".to_string(), SqlValue::Text(entry.channel_id)),
(
"event_seq".to_string(),
SqlValue::Integer(entry.event_seq as i64),
),
("event_kind".to_string(), SqlValue::Text(entry.event_kind)),
(
"message_id".to_string(),
entry.message_id.map_or(SqlValue::Null, SqlValue::Text),
),
("event_hash".to_string(), SqlValue::Text(entry.event_hash)),
("coverage_id".to_string(), SqlValue::Text(entry.coverage_id)),
(
"applied_at_ms".to_string(),
SqlValue::Integer(entry.applied_at_ms),
),
]],
conflict_key: Some("tenant_id,channel_id,event_seq"),
exclude_from_update: Vec::new(),
})
}
pub fn recovery_coverage_op(coverage: RecoveryCoverage) -> StorageOp {
use helix_core::effect::SqlValue;
StorageOp::BatchUpsert(UpsertSpec {
version_column: None,
update_guard: None,
table: "channel_sync_coverage",
rows: vec![vec![
(
"coverage_id".to_string(),
SqlValue::Text(coverage.coverage_id),
),
("tenant_id".to_string(), SqlValue::Text(coverage.tenant_id)),
(
"channel_id".to_string(),
SqlValue::Text(coverage.channel_id),
),
(
"from_seq".to_string(),
SqlValue::Integer(coverage.from_seq as i64),
),
(
"to_seq".to_string(),
SqlValue::Integer(coverage.to_seq as i64),
),
(
"event_count".to_string(),
SqlValue::Integer(coverage.event_count as i64),
),
(
"facts_hash".to_string(),
SqlValue::Text(coverage.facts_hash),
),
(
"correlation_id".to_string(),
SqlValue::Text(coverage.correlation_id),
),
(
"commit_state".to_string(),
SqlValue::Text("committed".to_string()),
),
(
"committed_at_ms".to_string(),
SqlValue::Integer(coverage.committed_at_ms),
),
]],
conflict_key: Some("coverage_id"),
exclude_from_update: Vec::new(),
})
}
pub fn upsert_message(_temporary_id: &TemporaryId, row: Row, corr: Correlation) -> Effect {
Effect::Persist {
corr,
ops: vec![StorageOp::BatchUpsert(UpsertSpec {
version_column: None,
update_guard: None,
table: "message",
rows: vec![row],
conflict_key: Some("temporary_id"),
exclude_from_update: Vec::new(),
})],
}
}
pub fn upsert_channel_full(
cols: Vec<(&'static str, helix_core::effect::SqlValue)>,
exclude_from_update: Vec<&'static str>,
) -> Effect {
Effect::PersistFire {
ops: vec![upsert_channel_full_op(cols, exclude_from_update)],
}
}
pub fn upsert_channel_full_op(
cols: Vec<(&'static str, helix_core::effect::SqlValue)>,
exclude_from_update: Vec<&'static str>,
) -> StorageOp {
let row: Row = cols.into_iter().map(|(k, v)| (k.to_string(), v)).collect();
StorageOp::BatchUpsert(UpsertSpec {
version_column: None,
update_guard: None,
table: "channel",
rows: vec![row],
conflict_key: Some("id"),
exclude_from_update,
})
}
pub fn update_channel_partial(
channel_id: ChannelId,
cols: Vec<(&'static str, helix_core::effect::SqlValue)>,
) -> Option<Effect> {
Some(Effect::PersistFire {
ops: vec![update_channel_partial_op(channel_id, cols)?],
})
}
pub fn update_channel_partial_op(
channel_id: ChannelId,
cols: Vec<(&'static str, helix_core::effect::SqlValue)>,
) -> Option<StorageOp> {
if cols.is_empty() {
return None;
}
use helix_core::effect::{BatchUpdateSpec, SqlValue};
let patch: Row = cols.into_iter().map(|(k, v)| (k.to_string(), v)).collect();
Some(StorageOp::BatchUpdate(BatchUpdateSpec {
table: "channel",
key_col: "id",
key_vals: vec![SqlValue::Text(channel_id.as_str().to_string())],
patch,
}))
}
pub fn upsert_channel_member_channel(row: Row) -> Option<Effect> {
Some(Effect::PersistFire {
ops: vec![upsert_channel_member_channel_op(row)?],
})
}
pub fn upsert_channel_member_channel_op(row: Row) -> Option<StorageOp> {
if row.is_empty() {
return None;
}
Some(StorageOp::BatchUpsert(UpsertSpec {
version_column: None,
update_guard: None,
table: "channel_member",
rows: vec![row],
conflict_key: Some("channel_id,user_id"),
exclude_from_update: Vec::new(),
}))
}
pub fn canonical_member_projection_ops(
channel_id: ChannelId,
user_id: &str,
revision: u64,
row: Row,
) -> Vec<StorageOp> {
use helix_core::effect::{ScopedGuardedBumpSpec, SqlValue};
let baseline = StorageOp::BatchUpsert(UpsertSpec {
version_column: None,
update_guard: None,
table: "channel_member",
rows: vec![vec![
(
"channel_id".to_string(),
SqlValue::Text(channel_id.as_str().to_string()),
),
("user_id".to_string(), SqlValue::Text(user_id.to_string())),
]],
conflict_key: Some("channel_id,user_id"),
exclude_from_update: vec!["channel_id", "user_id"],
});
let set_cols = row
.into_iter()
.filter(|(column, _)| !matches!(column.as_str(), "channel_id" | "user_id" | "updated_at"))
.collect();
vec![
baseline,
StorageOp::ScopedGuardedBump(ScopedGuardedBumpSpec {
table: "channel_member",
scope_col: "channel_id",
scope_val: SqlValue::Text(channel_id.as_str().to_string()),
key_col: "user_id",
key_val: SqlValue::Text(user_id.to_string()),
bump_col: "updated_at",
bump_delta: 0,
set_cols,
guard_col: "projection_revision",
guard_val: revision.min(i64::MAX as u64) as i64,
additional_guard: None,
}),
]
}
pub fn bump_channel_unread(upd: &crate::channel_write::PostChannelUpdate) -> Effect {
Effect::PersistFire {
ops: vec![bump_channel_unread_op(upd)],
}
}
pub fn bump_channel_unread_op(upd: &crate::channel_write::PostChannelUpdate) -> StorageOp {
use helix_core::effect::{GuardedBumpSpec, SqlValue};
let mut set_cols: Row = Vec::with_capacity(3);
if let Some(ref pid) = upd.unread_post_id {
set_cols.push(("unread_post_id".to_string(), SqlValue::Text(pid.clone())));
}
if !upd.last_post.is_empty() {
set_cols.push((
"last_post".to_string(),
SqlValue::Text(upd.last_post.clone()),
));
set_cols.push((
"last_post_at".to_string(),
SqlValue::Integer(upd.msg_create_at),
));
}
if upd.has_schedule_post {
set_cols.push(("has_schedule_post".to_string(), SqlValue::Integer(1)));
}
if upd.mention_hit && !upd.post_id.is_empty() {
set_cols.push((
"mention_list".to_string(),
SqlValue::Text(serde_json::json!([upd.post_id]).to_string()),
));
set_cols.push((
"mention_user".to_string(),
SqlValue::Text(serde_json::json!(upd.mentions).to_string()),
));
}
if upd.urgent_hit && !upd.post_id.is_empty() {
set_cols.push((
"urgent_post_list".to_string(),
SqlValue::Text(serde_json::json!([upd.post_id]).to_string()),
));
set_cols.push(("has_urgent_post".to_string(), SqlValue::Integer(1)));
}
set_cols.push((
"last_root_post_at".to_string(),
SqlValue::Integer(upd.msg_create_at),
));
let mut extra_bumps = Vec::new();
if upd.mention_hit {
extra_bumps.push(("mention_count", 1));
}
if upd.urgent_hit {
extra_bumps.push(("urgent_count", 1));
}
StorageOp::GuardedBump(GuardedBumpSpec {
table: "channel",
key_col: "id",
key_val: SqlValue::Text(upd.channel_id.as_str().to_string()),
bump_col: "unread_count",
bump_delta: upd.unread_delta,
extra_bumps,
set_cols,
guard_col: "last_root_post_at",
guard_val: upd.msg_create_at,
})
}
pub fn get_channel_row_op(channel_id: ChannelId) -> StorageOp {
use helix_core::effect::{GetSpec, SqlValue};
StorageOp::Get(GetSpec {
table: "channel",
key_col: "id",
key_val: SqlValue::Text(channel_id.as_str().to_string()),
})
}
pub fn advance_cursor(channel_id: ChannelId, target_seq: Seq) -> Effect {
Effect::PersistFire {
ops: vec![advance_cursor_op(channel_id, target_seq)],
}
}
pub fn advance_cursor_op(channel_id: ChannelId, target_seq: Seq) -> StorageOp {
StorageOp::MonotonicUpsert(MonotonicUpsertSpec {
table: "channel_event_cursor",
key_col: "channel_id",
value_col: "last_event_seq",
touch_col: Some("updated_at"),
scope_key: channel_id.as_str().to_string(),
value: target_seq.0 as i64,
})
}
pub fn terminal_tombstone_and_cursor_op(channel_id: ChannelId, target_seq: Seq) -> StorageOp {
use helix_core::effect::SqlValue;
StorageOp::BatchUpsert(UpsertSpec {
version_column: None,
update_guard: None,
table: "channel_event_cursor",
rows: vec![vec![
(
"channel_id".to_string(),
SqlValue::Text(channel_id.as_str().to_string()),
),
(
"last_event_seq".to_string(),
SqlValue::Integer(target_seq.0 as i64),
),
(
"terminal_event_seq".to_string(),
SqlValue::Integer(target_seq.0 as i64),
),
]],
conflict_key: Some("channel_id"),
exclude_from_update: Vec::new(),
})
}
pub fn closed_channel_op(channel_id: ChannelId) -> StorageOp {
crate::acl::to_effect_s1::channel_set_cols_op(
channel_id,
vec![("is_closed", helix_core::effect::SqlValue::Integer(1))],
)
}
pub fn reset_channel_dialog_op(channel_id: ChannelId) -> StorageOp {
use helix_core::effect::{BatchUpdateSpec, SqlValue};
StorageOp::BatchUpdate(BatchUpdateSpec {
table: "channel",
key_col: "id",
key_vals: vec![SqlValue::Text(channel_id.as_str().to_string())],
patch: vec![
("last_post".to_string(), SqlValue::Text(String::new())),
("unread_post_id".to_string(), SqlValue::Text(String::new())),
("unread_count".to_string(), SqlValue::Integer(0)),
("mention_count".to_string(), SqlValue::Integer(0)),
("mention_list".to_string(), SqlValue::Text(String::new())),
("mention_user".to_string(), SqlValue::Text(String::new())),
("urgent_count".to_string(), SqlValue::Integer(0)),
(
"urgent_post_list".to_string(),
SqlValue::Text(String::new()),
),
("has_urgent_post".to_string(), SqlValue::Integer(0)),
("has_more".to_string(), SqlValue::Integer(1)),
],
})
}