use std::fmt;
use std::time::Duration;
pub mod evidence;
pub use evidence::{
ATTESTATION_SIGNATURE_BYTES, ATTESTATION_SIGNER_BYTES, COMMIT_ROOT_BYTES, Classification,
DIGEST_BYTES, ExactObjectRef, INCARNATION_BYTES, JournalAnchor, JournalAttestation,
JournalSource, MAX_SOURCE_PINS, ObjectDescriptor, ProjectionKey, ProjectionManifest,
PublisherFence, Retention, SourceCheckpoint, SourceEvidence, SourcePin,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QueryOutcome {
Succeeded,
Failed(ErrorClass),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorClass {
Denied,
Deadline,
Cancelled,
Bounds,
Unavailable,
Malformed,
Internal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Truncation {
Complete,
TruncatedAt(u64),
}
pub const PROTOCOL_VERSION: u32 = 1;
pub const MAX_SQL_BYTES: usize = 64 * 1024;
pub const MAX_PARAMETERS: usize = 256;
pub const MAX_PARAMETER_BYTES: usize = 64 * 1024;
pub const MAX_TIMEOUT: Duration = Duration::from_mins(5);
pub const MAX_ROWS: u64 = 1_000_000;
pub const MAX_RESULT_BYTES: u64 = 64 * 1024 * 1024;
pub const MAX_FRAME_BYTES: u64 = 4 * 1024 * 1024;
#[derive(Clone, PartialEq, Eq)]
pub enum Parameter {
Utf8(String),
UInt64(u64),
Boolean(bool),
Null,
}
impl fmt::Debug for Parameter {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let kind = match self {
Self::Utf8(_) => "utf8",
Self::UInt64(_) => "uint64",
Self::Boolean(_) => "boolean",
Self::Null => "null",
};
formatter.write_str(kind)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Consistency {
Projected,
RequireProjectedThrough(u64),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RequestedBounds {
timeout: Duration,
rows: u64,
result_bytes: u64,
frame_bytes: u64,
}
impl RequestedBounds {
pub fn try_new(
timeout: Duration,
rows: u64,
result_bytes: u64,
frame_bytes: u64,
) -> Result<Self, ModelError> {
if timeout.is_zero() || timeout > MAX_TIMEOUT {
return Err(ModelError::Bounds("timeout_nanos"));
}
if rows == 0 || rows > MAX_ROWS {
return Err(ModelError::Bounds("rows"));
}
if result_bytes == 0 || result_bytes > MAX_RESULT_BYTES {
return Err(ModelError::Bounds("result_bytes"));
}
if frame_bytes == 0 || frame_bytes > MAX_FRAME_BYTES {
return Err(ModelError::Bounds("frame_bytes"));
}
Ok(Self {
timeout,
rows,
result_bytes,
frame_bytes,
})
}
#[must_use]
pub const fn timeout(self) -> Duration {
self.timeout
}
#[must_use]
pub const fn rows(self) -> u64 {
self.rows
}
#[must_use]
pub const fn result_bytes(self) -> u64 {
self.result_bytes
}
#[must_use]
pub const fn frame_bytes(self) -> u64 {
self.frame_bytes
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct QueryRequest {
sql: String,
parameters: Vec<Parameter>,
consistency: Consistency,
bounds: RequestedBounds,
}
impl QueryRequest {
pub fn try_new(
sql: String,
parameters: Vec<Parameter>,
consistency: Consistency,
bounds: RequestedBounds,
) -> Result<Self, ModelError> {
if sql.is_empty() || sql.len() > MAX_SQL_BYTES {
return Err(ModelError::Bounds("sql"));
}
if parameters.len() > MAX_PARAMETERS {
return Err(ModelError::Bounds("parameters"));
}
let parameter_bytes = parameters.iter().try_fold(0_usize, |total, parameter| {
let bytes = match parameter {
Parameter::Utf8(value) => value.len(),
Parameter::UInt64(_) | Parameter::Boolean(_) | Parameter::Null => 0,
};
total
.checked_add(bytes)
.ok_or(ModelError::Bounds("parameters"))
})?;
if parameter_bytes > MAX_PARAMETER_BYTES {
return Err(ModelError::Bounds("parameters"));
}
Ok(Self {
sql,
parameters,
consistency,
bounds,
})
}
#[must_use]
pub fn sql(&self) -> &str {
&self.sql
}
#[must_use]
pub fn parameters(&self) -> &[Parameter] {
&self.parameters
}
#[must_use]
pub const fn consistency(&self) -> Consistency {
self.consistency
}
#[must_use]
pub const fn bounds(&self) -> RequestedBounds {
self.bounds
}
}
impl fmt::Debug for QueryRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("QueryRequest")
.field("sql_bytes", &self.sql.len())
.field("parameters", &self.parameters.len())
.field("consistency", &self.consistency)
.field("bounds", &self.bounds)
.finish()
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct SchemaFrame {
arrow_ipc: Vec<u8>,
}
impl SchemaFrame {
pub fn try_new(arrow_ipc: Vec<u8>) -> Result<Self, ModelError> {
validate_frame_bytes(&arrow_ipc)?;
Ok(Self { arrow_ipc })
}
#[must_use]
pub fn arrow_ipc(&self) -> &[u8] {
&self.arrow_ipc
}
}
impl fmt::Debug for SchemaFrame {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SchemaFrame")
.field("arrow_ipc_bytes", &self.arrow_ipc.len())
.finish()
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct DataFrame {
sequence: u64,
rows: u64,
arrow_ipc: Vec<u8>,
}
impl DataFrame {
pub fn try_new(sequence: u64, rows: u64, arrow_ipc: Vec<u8>) -> Result<Self, ModelError> {
validate_frame_bytes(&arrow_ipc)?;
Ok(Self {
sequence,
rows,
arrow_ipc,
})
}
#[must_use]
pub const fn sequence(&self) -> u64 {
self.sequence
}
#[must_use]
pub const fn rows(&self) -> u64 {
self.rows
}
#[must_use]
pub fn arrow_ipc(&self) -> &[u8] {
&self.arrow_ipc
}
}
impl fmt::Debug for DataFrame {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("DataFrame")
.field("sequence", &self.sequence)
.field("rows", &self.rows)
.field("arrow_ipc_bytes", &self.arrow_ipc.len())
.finish()
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct TerminalFrame {
outcome: QueryOutcome,
duration: Duration,
rows: u64,
result_bytes: u64,
truncation: Truncation,
source: SourceEvidence,
}
impl TerminalFrame {
#[must_use]
pub const fn new(
outcome: QueryOutcome,
duration: Duration,
rows: u64,
result_bytes: u64,
truncation: Truncation,
source: SourceEvidence,
) -> Self {
Self {
outcome,
duration,
rows,
result_bytes,
truncation,
source,
}
}
#[must_use]
pub const fn outcome(&self) -> QueryOutcome {
self.outcome
}
#[must_use]
pub const fn duration(&self) -> Duration {
self.duration
}
#[must_use]
pub const fn rows(&self) -> u64 {
self.rows
}
#[must_use]
pub const fn result_bytes(&self) -> u64 {
self.result_bytes
}
#[must_use]
pub const fn truncation(&self) -> Truncation {
self.truncation
}
#[must_use]
pub const fn source(&self) -> &SourceEvidence {
&self.source
}
}
impl fmt::Debug for TerminalFrame {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TerminalFrame")
.field("outcome", &self.outcome)
.field("duration", &self.duration)
.field("rows", &self.rows)
.field("result_bytes", &self.result_bytes)
.field("truncation", &self.truncation)
.field("source_pins", &self.source.pins().len())
.finish()
}
}
fn validate_frame_bytes(bytes: &[u8]) -> Result<(), ModelError> {
if bytes.is_empty() || u64::try_from(bytes.len()).unwrap_or(u64::MAX) > MAX_FRAME_BYTES {
return Err(ModelError::Bounds("arrow_ipc"));
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResultFrame {
Schema(SchemaFrame),
Data(DataFrame),
Terminal(TerminalFrame),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum ModelError {
#[error("query field `{0}` is outside its protocol bound")]
Bounds(&'static str),
#[error("query field `{0}` is not in strict canonical order")]
Order(&'static str),
}
#[cfg(test)]
mod tests {
use super::*;
fn bounds() -> RequestedBounds {
RequestedBounds::try_new(Duration::from_secs(1), 10, 1024, 512).unwrap()
}
#[test]
fn request_refuses_empty_and_overbound_content() {
assert_eq!(
QueryRequest::try_new(String::new(), vec![], Consistency::Projected, bounds()),
Err(ModelError::Bounds("sql"))
);
assert_eq!(
QueryRequest::try_new(
"select ?".into(),
vec![Parameter::Utf8("x".repeat(MAX_PARAMETER_BYTES + 1))],
Consistency::Projected,
bounds(),
),
Err(ModelError::Bounds("parameters"))
);
}
#[test]
fn bounds_refuse_zero_and_crossed_frame_limits() {
assert_eq!(
RequestedBounds::try_new(Duration::ZERO, 1, 1, 1),
Err(ModelError::Bounds("timeout_nanos"))
);
assert_eq!(
RequestedBounds::try_new(Duration::from_secs(1), 1, 8, MAX_FRAME_BYTES + 1),
Err(ModelError::Bounds("frame_bytes"))
);
}
#[test]
fn debug_output_carries_no_caller_content() {
let request = QueryRequest::try_new(
"select secret_column from messages".into(),
vec![Parameter::Utf8("tenant-secret".into())],
Consistency::Projected,
bounds(),
)
.unwrap();
let rendered = format!("{request:?}");
assert!(!rendered.contains("secret_column"), "{rendered}");
assert!(!rendered.contains("tenant-secret"), "{rendered}");
assert!(rendered.contains("sql_bytes"), "{rendered}");
let data = DataFrame::try_new(0, 2, vec![7, 8, 9]).unwrap();
let rendered = format!("{data:?}");
assert!(!rendered.contains('7'), "{rendered}");
assert!(rendered.contains("arrow_ipc_bytes: 3"), "{rendered}");
}
}