use std::ops::Bound;
use crate::catalog::SYSTEM_PROJECT_ID;
use crate::catalog::types::Value;
use crate::error::AedbError;
use crate::permission::{CallerContext, Permission};
use crate::query::plan::ConsistencyMode;
use crate::query_authorization::ensure_external_caller_allowed;
use crate::storage::encoded_key::EncodedKey;
use crate::{
AedbInstance, EVENT_OUTBOX_TABLE, EventFieldFilter, EventOrder, EventOutboxRecord, EventQuery,
EventStreamPage, SYSTEM_SCOPE_ID,
};
impl EventQuery {
pub fn new() -> Self {
Self::default()
}
pub fn topic(mut self, topic: impl Into<String>) -> Self {
self.topic = Some(topic.into());
self
}
pub fn namespace(mut self, project_id: impl Into<String>, scope_id: impl Into<String>) -> Self {
self.project_id = Some(project_id.into());
self.scope_id = Some(scope_id.into());
self
}
pub fn after_commit_seq(mut self, seq: u64) -> Self {
self.from_commit_seq_exclusive = Some(seq);
self
}
pub fn before_commit_seq(mut self, seq: u64) -> Self {
self.to_commit_seq_exclusive = Some(seq);
self
}
pub fn time_range(mut self, from_micros: Option<u64>, to_micros: Option<u64>) -> Self {
self.from_ts_micros = from_micros;
self.to_ts_micros = to_micros;
self
}
pub fn where_field(mut self, field: impl Into<String>, equals: impl Into<String>) -> Self {
self.field_filters.push(EventFieldFilter {
field: field.into(),
equals: equals.into(),
});
self
}
pub fn descending(mut self) -> Self {
self.order = EventOrder::Descending;
self
}
pub fn limit(mut self, limit: usize) -> Self {
self.limit = limit;
self
}
}
impl AedbInstance {
pub async fn query_events(
&self,
query: &EventQuery,
consistency: ConsistencyMode,
) -> Result<EventStreamPage, AedbError> {
if self.require_authenticated_calls {
return Err(AedbError::PermissionDenied(
"authenticated caller required in secure mode".into(),
));
}
self.query_events_internal(query, consistency, None).await
}
pub async fn query_events_as(
&self,
caller: &CallerContext,
query: &EventQuery,
consistency: ConsistencyMode,
) -> Result<EventStreamPage, AedbError> {
ensure_external_caller_allowed(caller)?;
self.query_events_internal(query, consistency, Some(caller))
.await
}
pub async fn latest_events_by_topic(
&self,
topic: &str,
n: usize,
consistency: ConsistencyMode,
) -> Result<Vec<EventOutboxRecord>, AedbError> {
let query = EventQuery::new().topic(topic).descending().limit(n);
Ok(self.query_events(&query, consistency).await?.events)
}
async fn query_events_internal(
&self,
query: &EventQuery,
consistency: ConsistencyMode,
caller: Option<&CallerContext>,
) -> Result<EventStreamPage, AedbError> {
let lease = self.acquire_snapshot(consistency).await?;
let snapshot_seq = lease.view.seq;
if query.limit == 0 {
return Ok(EventStreamPage {
events: Vec::new(),
next_commit_seq: None,
snapshot_seq,
});
}
if let Some(caller) = caller {
let required = Permission::TableRead {
project_id: SYSTEM_PROJECT_ID.to_string(),
scope_id: SYSTEM_SCOPE_ID.to_string(),
table_name: EVENT_OUTBOX_TABLE.to_string(),
};
if !lease
.view
.catalog
.has_permission(&caller.caller_id, &required)
{
return Err(AedbError::PermissionDenied("permission denied".into()));
}
}
let Some(table) =
lease
.view
.keyspace
.table(SYSTEM_PROJECT_ID, SYSTEM_SCOPE_ID, EVENT_OUTBOX_TABLE)
else {
return Ok(EventStreamPage {
events: Vec::new(),
next_commit_seq: None,
snapshot_seq,
});
};
let start_bound = match query.from_commit_seq_exclusive {
Some(from) => match from.checked_add(1) {
Some(start_seq) => match i64::try_from(start_seq) {
Ok(start_seq_i64) => Bound::Included(seq_key(start_seq_i64)),
Err(_) => {
return Ok(EventStreamPage {
events: Vec::new(),
next_commit_seq: None,
snapshot_seq,
});
}
},
None => {
return Ok(EventStreamPage {
events: Vec::new(),
next_commit_seq: None,
snapshot_seq,
});
}
},
None => Bound::Unbounded,
};
let end_bound = match query.to_commit_seq_exclusive {
Some(to) => match i64::try_from(to) {
Ok(to_i64) => Bound::Excluded(seq_key(to_i64)),
Err(_) => Bound::Unbounded,
},
None => Bound::Unbounded,
};
let max_scan_rows = self._config.max_scan_rows;
let limit = query.limit;
let mut events: Vec<EventOutboxRecord> = Vec::new();
let mut examined: usize = 0;
let mut boundary_seq: Option<u64> = None;
let mut stopped_early = false;
let range = table.rows.range((start_bound, end_bound));
match query.order {
EventOrder::Ascending => {
for (_, stored) in range {
let row = lease.view.keyspace.materialize_row(stored)?;
if !Self::consume_event_row(
&row,
query,
limit,
max_scan_rows,
&mut events,
&mut examined,
&mut boundary_seq,
&mut stopped_early,
) {
break;
}
}
}
EventOrder::Descending => {
for (_, stored) in range.rev() {
let row = lease.view.keyspace.materialize_row(stored)?;
if !Self::consume_event_row(
&row,
query,
limit,
max_scan_rows,
&mut events,
&mut examined,
&mut boundary_seq,
&mut stopped_early,
) {
break;
}
}
}
}
let next_commit_seq = if stopped_early { boundary_seq } else { None };
Ok(EventStreamPage {
events,
next_commit_seq,
snapshot_seq,
})
}
#[allow(clippy::too_many_arguments)]
fn consume_event_row(
row: &crate::catalog::types::Row,
query: &EventQuery,
limit: usize,
max_scan_rows: usize,
events: &mut Vec<EventOutboxRecord>,
examined: &mut usize,
boundary_seq: &mut Option<u64>,
stopped_early: &mut bool,
) -> bool {
let Some(record) = decode_event_row(row) else {
return true;
};
if let Some(b) = *boundary_seq
&& record.commit_seq != b
{
*stopped_early = true;
return false;
}
*examined += 1;
if event_matches(&record, query) {
events.push(record.clone());
}
if boundary_seq.is_none() && (events.len() >= limit || *examined >= max_scan_rows) {
*boundary_seq = Some(record.commit_seq);
}
true
}
}
fn seq_key(commit_seq_i64: i64) -> EncodedKey {
EncodedKey::from_values(&[
Value::Integer(commit_seq_i64),
Value::Text("".into()),
Value::Text("".into()),
])
}
fn decode_event_row(row: &crate::catalog::types::Row) -> Option<EventOutboxRecord> {
let (
Some(Value::Integer(commit_seq_i64)),
Some(Value::Timestamp(ts_i64)),
Some(Value::Text(project_id)),
Some(Value::Text(scope_id)),
Some(Value::Text(topic)),
Some(Value::Text(event_key)),
Some(Value::Json(payload)),
) = (
row.values.first(),
row.values.get(1),
row.values.get(2),
row.values.get(3),
row.values.get(4),
row.values.get(5),
row.values.get(6),
)
else {
return None;
};
let commit_seq = u64::try_from(*commit_seq_i64).ok()?;
let ts_micros = u64::try_from(*ts_i64).ok()?;
Some(EventOutboxRecord {
commit_seq,
ts_micros,
project_id: project_id.to_string(),
scope_id: scope_id.to_string(),
topic: topic.to_string(),
event_key: event_key.to_string(),
payload_json: payload.to_string(),
})
}
fn event_matches(record: &EventOutboxRecord, query: &EventQuery) -> bool {
if let Some(topic) = &query.topic
&& &record.topic != topic
{
return false;
}
if let Some(project_id) = &query.project_id
&& &record.project_id != project_id
{
return false;
}
if let Some(scope_id) = &query.scope_id
&& &record.scope_id != scope_id
{
return false;
}
if let Some(from) = query.from_ts_micros
&& record.ts_micros < from
{
return false;
}
if let Some(to) = query.to_ts_micros
&& record.ts_micros > to
{
return false;
}
payload_fields_match(&record.payload_json, &query.field_filters)
}
fn payload_fields_match(payload_json: &str, filters: &[EventFieldFilter]) -> bool {
if filters.is_empty() {
return true;
}
let Ok(parsed) = serde_json::from_str::<serde_json::Value>(payload_json) else {
return false;
};
let Some(obj) = parsed.as_object() else {
return false;
};
for filter in filters {
let Some(value) = obj.get(&filter.field) else {
return false;
};
let matches = match value {
serde_json::Value::String(s) => s == &filter.equals,
serde_json::Value::Number(n) => n.to_string() == filter.equals,
serde_json::Value::Bool(b) => b.to_string() == filter.equals,
_ => false,
};
if !matches {
return false;
}
}
true
}
#[cfg(test)]
mod tests;