use arrow::record_batch::RecordBatch;
use polyc_crypto::signing_role::{HandoffRole, RoleTrustSet};
use polyc_eventlog::Event;
use crate::decode;
use super::QueryEngineError;
use super::tables::{
APPROVALS_RAW_TABLE, ATTRIBUTION_RAW_TABLE, EVENTS_RAW_TABLE, FIRES_RAW_TABLE,
GRANT_REPLAYS_RAW_TABLE, HANDOFFS_RAW_TABLE, MESSAGES_RAW_TABLE, MODEL_CALL_RAW_TABLE,
PAYMENTS_RAW_TABLE, REFUSALS_RAW_TABLE, ROUTINE_LIFECYCLE_RAW_TABLE, ROUTINE_SETUP_RAW_TABLE,
SUMMARY_RAW_TABLE, TOOL_CALLS_RAW_TABLE, TURN_DISPATCH_RAW_TABLE, TURN_FAILED_RAW_TABLE,
USAGE_RAW_TABLE, WALLET_LINK_LIFECYCLE_RAW_TABLE,
};
#[derive(Debug, Clone)]
#[allow(
clippy::struct_field_names,
reason = "every field's `_raw` suffix deliberately mirrors its own `*_RAW_TABLE` catalog \
constant (e.g. `events_raw` <-> EVENTS_RAW_TABLE) — dropping it would break that \
parallel, not just rename a field"
)]
pub(crate) struct PartitionTables {
pub events_raw: RecordBatch,
pub usage_raw: RecordBatch,
pub model_call_raw: RecordBatch,
pub attribution_raw: RecordBatch,
pub turn_failed_raw: RecordBatch,
pub turn_dispatch_raw: RecordBatch,
pub payments_raw: RecordBatch,
pub refusals_raw: RecordBatch,
pub wallet_link_lifecycle_raw: RecordBatch,
pub approvals_raw: RecordBatch,
pub handoffs_raw: RecordBatch,
pub grant_replays_raw: RecordBatch,
pub summary_raw: RecordBatch,
pub fires_raw: RecordBatch,
pub routine_lifecycle_raw: RecordBatch,
pub routine_setup_raw: RecordBatch,
pub messages_raw: RecordBatch,
pub tool_calls_raw: RecordBatch,
}
impl PartitionTables {
const fn fields(&self) -> [(&'static str, &RecordBatch); 18] {
[
(EVENTS_RAW_TABLE, &self.events_raw),
(USAGE_RAW_TABLE, &self.usage_raw),
(MODEL_CALL_RAW_TABLE, &self.model_call_raw),
(ATTRIBUTION_RAW_TABLE, &self.attribution_raw),
(TURN_FAILED_RAW_TABLE, &self.turn_failed_raw),
(TURN_DISPATCH_RAW_TABLE, &self.turn_dispatch_raw),
(PAYMENTS_RAW_TABLE, &self.payments_raw),
(REFUSALS_RAW_TABLE, &self.refusals_raw),
(
WALLET_LINK_LIFECYCLE_RAW_TABLE,
&self.wallet_link_lifecycle_raw,
),
(APPROVALS_RAW_TABLE, &self.approvals_raw),
(HANDOFFS_RAW_TABLE, &self.handoffs_raw),
(GRANT_REPLAYS_RAW_TABLE, &self.grant_replays_raw),
(SUMMARY_RAW_TABLE, &self.summary_raw),
(FIRES_RAW_TABLE, &self.fires_raw),
(ROUTINE_LIFECYCLE_RAW_TABLE, &self.routine_lifecycle_raw),
(ROUTINE_SETUP_RAW_TABLE, &self.routine_setup_raw),
(MESSAGES_RAW_TABLE, &self.messages_raw),
(TOOL_CALLS_RAW_TABLE, &self.tool_calls_raw),
]
}
pub(crate) fn memory_bytes(&self) -> usize {
self.fields()
.iter()
.map(|(_, batch)| batch.get_array_memory_size())
.sum()
}
pub(crate) fn concat(&self, tail: &Self) -> Result<Self, QueryEngineError> {
Ok(Self {
events_raw: concat(EVENTS_RAW_TABLE, &self.events_raw, &tail.events_raw)?,
usage_raw: concat(USAGE_RAW_TABLE, &self.usage_raw, &tail.usage_raw)?,
model_call_raw: concat(
MODEL_CALL_RAW_TABLE,
&self.model_call_raw,
&tail.model_call_raw,
)?,
attribution_raw: concat(
ATTRIBUTION_RAW_TABLE,
&self.attribution_raw,
&tail.attribution_raw,
)?,
turn_failed_raw: concat(
TURN_FAILED_RAW_TABLE,
&self.turn_failed_raw,
&tail.turn_failed_raw,
)?,
turn_dispatch_raw: concat(
TURN_DISPATCH_RAW_TABLE,
&self.turn_dispatch_raw,
&tail.turn_dispatch_raw,
)?,
payments_raw: concat(PAYMENTS_RAW_TABLE, &self.payments_raw, &tail.payments_raw)?,
refusals_raw: concat(REFUSALS_RAW_TABLE, &self.refusals_raw, &tail.refusals_raw)?,
wallet_link_lifecycle_raw: concat(
WALLET_LINK_LIFECYCLE_RAW_TABLE,
&self.wallet_link_lifecycle_raw,
&tail.wallet_link_lifecycle_raw,
)?,
approvals_raw: concat(
APPROVALS_RAW_TABLE,
&self.approvals_raw,
&tail.approvals_raw,
)?,
handoffs_raw: concat(HANDOFFS_RAW_TABLE, &self.handoffs_raw, &tail.handoffs_raw)?,
grant_replays_raw: concat(
GRANT_REPLAYS_RAW_TABLE,
&self.grant_replays_raw,
&tail.grant_replays_raw,
)?,
summary_raw: concat(SUMMARY_RAW_TABLE, &self.summary_raw, &tail.summary_raw)?,
fires_raw: concat(FIRES_RAW_TABLE, &self.fires_raw, &tail.fires_raw)?,
routine_lifecycle_raw: concat(
ROUTINE_LIFECYCLE_RAW_TABLE,
&self.routine_lifecycle_raw,
&tail.routine_lifecycle_raw,
)?,
routine_setup_raw: concat(
ROUTINE_SETUP_RAW_TABLE,
&self.routine_setup_raw,
&tail.routine_setup_raw,
)?,
messages_raw: concat(MESSAGES_RAW_TABLE, &self.messages_raw, &tail.messages_raw)?,
tool_calls_raw: concat(
TOOL_CALLS_RAW_TABLE,
&self.tool_calls_raw,
&tail.tool_calls_raw,
)?,
})
}
}
fn concat(
table: &'static str,
first: &RecordBatch,
second: &RecordBatch,
) -> Result<RecordBatch, QueryEngineError> {
arrow::compute::concat_batches(&first.schema(), [first, second])
.map_err(|source| QueryEngineError::Decode { table, source })
}
#[cfg(test)]
pub(crate) static DECODE_CALL_COUNT: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
#[allow(
clippy::too_many_lines,
reason = "eighteen mechanical per-table decode-then-map-err blocks, one per typed table \
this partition builds; unlike register_typed_journal_tables (which iterates one \
uniform (name, schema_fn, accessor) recipe over JOURNAL_TABLE_REGISTRATIONS, \
crate::engine::registration), each table's own decode fn here has a distinct \
per-kind signature (trusted_signers threaded through payments/approvals/\
grant_replays/routine_lifecycle/wallet_link_lifecycle, message_content split \
into two outputs, ...), so this cannot fold into the same table-driven loop \
without losing that per-kind variation"
)]
pub(crate) fn decode_partition_tables(
partition: &str,
events: &[(u64, Event)],
trusted_signers: &[Vec<u8>],
handoff_trust: &RoleTrustSet<HandoffRole>,
) -> Result<PartitionTables, QueryEngineError> {
#[cfg(test)]
DECODE_CALL_COUNT.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let withheld = polyc_facts::withheld_turn_ids_positioned(events);
let owned;
let events = if withheld.is_empty() {
events
} else {
owned = {
let mut copy = events.to_vec();
polyc_facts::withhold_paused_turn_text_positioned(&mut copy, &withheld);
copy
};
&owned
};
let events_raw =
decode::events_batch(partition, events).map_err(|source| QueryEngineError::Decode {
table: EVENTS_RAW_TABLE,
source,
})?;
let usage_raw =
decode::usage::decode_usage_batch(&decode::usage::decode_usage_events(partition, events))
.map_err(|source| QueryEngineError::Decode {
table: USAGE_RAW_TABLE,
source,
})?;
let model_call_raw = decode::model_call::decode_model_call_batch(
&decode::model_call::decode_model_call_events(partition, events),
)
.map_err(|source| QueryEngineError::Decode {
table: MODEL_CALL_RAW_TABLE,
source,
})?;
let attribution_raw = decode::attribution::decode_attribution_batch(
&decode::attribution::decode_attribution_events(partition, events),
)
.map_err(|source| QueryEngineError::Decode {
table: ATTRIBUTION_RAW_TABLE,
source,
})?;
let turn_failed_raw = decode::turn_failed::decode_turn_failed_batch(
&decode::turn_failed::decode_turn_failed_events(partition, events),
)
.map_err(|source| QueryEngineError::Decode {
table: TURN_FAILED_RAW_TABLE,
source,
})?;
let turn_dispatch_raw = decode::turn_dispatch::decode_turn_dispatch_batch(
&decode::turn_dispatch::decode_turn_dispatch_events(partition, events),
)
.map_err(|source| QueryEngineError::Decode {
table: TURN_DISPATCH_RAW_TABLE,
source,
})?;
let payments_raw = decode::payments::decode_payments_batch(
&decode::payments::decode_payments_events(partition, events, trusted_signers),
)
.map_err(|source| QueryEngineError::Decode {
table: PAYMENTS_RAW_TABLE,
source,
})?;
let refusals_raw = decode::refusals::decode_refusals_batch(
&decode::refusals::decode_refusals_events(partition, events, trusted_signers),
)
.map_err(|source| QueryEngineError::Decode {
table: REFUSALS_RAW_TABLE,
source,
})?;
let wallet_link_lifecycle_raw =
decode::wallet_link_lifecycle::decode_wallet_link_lifecycle_batch(
&decode::wallet_link_lifecycle::decode_wallet_link_lifecycle_events(
partition,
events,
trusted_signers,
),
)
.map_err(|source| QueryEngineError::Decode {
table: WALLET_LINK_LIFECYCLE_RAW_TABLE,
source,
})?;
let approvals_raw = decode::approvals::decode_approvals_batch(
&decode::approvals::decode_approvals_events(partition, events, trusted_signers),
)
.map_err(|source| QueryEngineError::Decode {
table: APPROVALS_RAW_TABLE,
source,
})?;
let handoffs_raw = decode::handoffs::decode_handoffs_batch(
&decode::handoffs::decode_handoffs_events(partition, events, handoff_trust),
)
.map_err(|source| QueryEngineError::Decode {
table: HANDOFFS_RAW_TABLE,
source,
})?;
let grant_replays_raw = decode::grant_replays::decode_grant_replays_batch(
&decode::grant_replays::decode_grant_replays_events(partition, events, trusted_signers),
)
.map_err(|source| QueryEngineError::Decode {
table: GRANT_REPLAYS_RAW_TABLE,
source,
})?;
let summary_raw = decode::summary::decode_summary_batch(
&decode::summary::decode_summary_events(partition, events),
)
.map_err(|source| QueryEngineError::Decode {
table: SUMMARY_RAW_TABLE,
source,
})?;
let fires_raw =
decode::fires::decode_fires_batch(&decode::fires::decode_fires_events(partition, events))
.map_err(|source| QueryEngineError::Decode {
table: FIRES_RAW_TABLE,
source,
})?;
let routine_lifecycle_raw = decode::routine_lifecycle::decode_routine_lifecycle_batch(
&decode::routine_lifecycle::decode_routine_lifecycle_events(
partition,
events,
trusted_signers,
),
)
.map_err(|source| QueryEngineError::Decode {
table: ROUTINE_LIFECYCLE_RAW_TABLE,
source,
})?;
let routine_setup_raw = decode::routine_setup::decode_routine_setup_batch(
&decode::routine_setup::decode_routine_setup_events(partition, events),
)
.map_err(|source| QueryEngineError::Decode {
table: ROUTINE_SETUP_RAW_TABLE,
source,
})?;
let mut message_rows = decode::message_content::messages::MessageRows::default();
let mut tool_call_rows = decode::message_content::tool_calls::ToolCallRows::default();
decode::message_content::decode_message_content_events(
partition,
events,
&mut message_rows,
&mut tool_call_rows,
);
let messages_raw = decode::message_content::messages::decode_messages_batch(&message_rows)
.map_err(|source| QueryEngineError::Decode {
table: MESSAGES_RAW_TABLE,
source,
})?;
let tool_calls_raw = decode::message_content::tool_calls::decode_tool_calls_batch(
&tool_call_rows,
)
.map_err(|source| QueryEngineError::Decode {
table: TOOL_CALLS_RAW_TABLE,
source,
})?;
Ok(PartitionTables {
events_raw,
usage_raw,
model_call_raw,
attribution_raw,
turn_failed_raw,
turn_dispatch_raw,
payments_raw,
refusals_raw,
wallet_link_lifecycle_raw,
approvals_raw,
handoffs_raw,
grant_replays_raw,
summary_raw,
fires_raw,
routine_lifecycle_raw,
routine_setup_raw,
messages_raw,
tool_calls_raw,
})
}
#[cfg(test)]
mod tests {
use polyc_eventlog::Event;
use polyc_proto::kinds;
use uuid::Uuid;
use super::*;
fn handoff_trust() -> RoleTrustSet<HandoffRole> {
RoleTrustSet::current(&polyc_crypto::signing_role::HandoffSigner::from_seed(700))
}
#[test]
fn empty_partition_decodes_to_empty_tables() {
let tables =
decode_partition_tables("conv-empty", &[], &[], &handoff_trust()).expect("decode");
assert_eq!(tables.events_raw.num_rows(), 0);
assert_eq!(tables.usage_raw.num_rows(), 0);
assert_eq!(tables.messages_raw.num_rows(), 0);
assert_eq!(tables.tool_calls_raw.num_rows(), 0);
}
#[test]
fn concat_appends_tail_rows_onto_base_rows() {
let turn = Uuid::now_v7();
let base = decode_partition_tables(
"conv-tail",
&[(
0,
Event::new(kinds::tagged(kinds::TURN_START, &turn), Vec::new()),
)],
&[],
&handoff_trust(),
)
.expect("decode base");
let tail = decode_partition_tables(
"conv-tail",
&[(
1,
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn), Vec::new()),
)],
&[],
&handoff_trust(),
)
.expect("decode tail");
let merged = base.concat(&tail).expect("concat");
assert_eq!(merged.events_raw.num_rows(), 2);
}
#[test]
fn memory_bytes_grows_after_concat() {
let turn = Uuid::now_v7();
let base = decode_partition_tables(
"conv-mem",
&[(
0,
Event::new(kinds::tagged(kinds::TURN_START, &turn), Vec::new()),
)],
&[],
&handoff_trust(),
)
.expect("decode base");
let tail = decode_partition_tables(
"conv-mem",
&[(
1,
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn), Vec::new()),
)],
&[],
&handoff_trust(),
)
.expect("decode tail");
let merged = base.concat(&tail).expect("concat");
assert!(merged.memory_bytes() >= base.memory_bytes());
}
}