use crate::{
ConstraintValidationFindingOutput, Error, ErrorKind, ErrorOrigin, RuntimeErrorKind,
db::{
EntityCatalogDescription, EntityConstraintDescription, MemoryCatalogDescription,
RowProjectionOutput, SqlDescribeOutput, SqlShowColumnsOutput, SqlShowRelationsOutput,
StoreCatalogDescription,
},
};
use serde::Deserialize;
const MAX_PUBLIC_SQL_QUERY_REPLY_BYTES: usize = 3 * 1024 * 1024;
#[cfg_attr(doc, doc = "SqlGroupedRowsOutput\n\nStructured grouped SQL payload.")]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct SqlGroupedRowsOutput {
pub entity: String,
pub columns: Vec<String>,
pub rows: Vec<Vec<String>>,
pub row_count: u32,
pub next_cursor: Option<String>,
}
#[cfg_attr(
doc,
doc = "SqlConstraintValidationOutput\n\nTyped progress from one bounded constraint-validation step."
)]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct SqlConstraintValidationOutput {
pub constraint_id: u32,
pub activation_epoch: Option<u64>,
pub page_sequence: Option<u64>,
pub state: String,
pub revision_status: String,
pub rows_scanned: u64,
pub findings: Vec<ConstraintValidationFindingOutput>,
pub complete: bool,
}
#[cfg_attr(doc, doc = "SqlQueryResult\n\nUnified SQL endpoint result.")]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
pub enum SqlQueryResult {
Count {
entity: String,
row_count: u32,
},
Projection(RowProjectionOutput),
Grouped(SqlGroupedRowsOutput),
Explain {
entity: String,
explain: String,
},
Describe(SqlDescribeOutput),
ShowConstraints {
entity: String,
constraints: Vec<EntityConstraintDescription>,
},
ShowIndexes {
entity: String,
indexes: Vec<String>,
},
ShowColumns(SqlShowColumnsOutput),
ShowRelations(SqlShowRelationsOutput),
ShowEntities {
entities: Vec<EntityCatalogDescription>,
verbose: bool,
},
ShowStores {
stores: Vec<StoreCatalogDescription>,
verbose: bool,
},
ShowMemory {
memory: Vec<MemoryCatalogDescription>,
},
Ddl {
entity: String,
mutation_kind: String,
target_index: String,
target_store: String,
field_path: Vec<String>,
status: String,
rows_scanned: u64,
index_keys_written: u64,
constraint_validation: Option<SqlConstraintValidationOutput>,
},
}
impl SqlQueryResult {
pub(in crate::db) fn encoded_returning_response_len(
projection: RowProjectionOutput,
) -> Result<usize, icydb_schema::wire::Error> {
let response: Result<Self, Error> = Ok(Self::Projection(projection));
ic_wire::native::encoded_frame_len(&response, icydb_schema::wire::sizing_limits())
}
#[doc(hidden)]
pub fn into_deliverable_query_reply(self) -> Result<Self, Error> {
let response: Result<&Self, Error> = Ok(&self);
let encoded =
ic_wire::native::encoded_frame_len(&response, icydb_schema::wire::sizing_limits())
.map_err(|_| {
Error::from_kind(
ErrorKind::Runtime(RuntimeErrorKind::Internal),
ErrorOrigin::Response,
)
})?;
if encoded > MAX_PUBLIC_SQL_QUERY_REPLY_BYTES {
return Err(Error::from_runtime_boundary(
crate::diagnostic::RuntimeBoundaryCode::SqlQueryReplyBytesExceeded,
ErrorOrigin::Response,
));
}
Ok(self)
}
}
#[cfg(test)]
mod tests {
use super::{MAX_PUBLIC_SQL_QUERY_REPLY_BYTES, SqlQueryResult};
use crate::{Error, ErrorOrigin};
#[test]
fn returning_frame_counts_match_the_actual_public_response() {
use crate::{
db::RowProjectionOutput,
value::{OutputValue, PublicValue},
};
for (count, size) in [
(0, 0),
(1, 127),
(1, 128),
(127, 128),
(128, 128),
(1, 1_048_000),
(100, 10_300),
] {
let row = vec![
OutputValue::text("x".repeat(size)),
OutputValue::list(vec![PublicValue::Null, PublicValue::Bool(true)]),
];
let output = RowProjectionOutput {
entity: "Row".into(),
columns: vec!["text".into(), "nested".into()],
rows: vec![row; count],
row_count: u32::try_from(count).unwrap(),
};
let mut empty = output.clone();
empty.rows.clear();
let empty_frame = SqlQueryResult::encoded_returning_response_len(empty).unwrap();
let values_len = icydb_schema::wire::encoded_len(&output.rows).unwrap();
let candidate: Result<SqlQueryResult, Error> = Ok(SqlQueryResult::Projection(output));
let actual = icydb_schema::wire::encode_one(candidate).unwrap().len() + 11;
assert_eq!(empty_frame - 1 + values_len, actual);
}
}
#[test]
fn generated_sql_query_reply_guard_measures_the_exact_complete_envelope() {
let small = SqlQueryResult::ShowIndexes {
entity: "Entry".to_string(),
indexes: vec!["by_owner".to_string()],
};
let owned: Result<SqlQueryResult, Error> = Ok(small.clone());
let borrowed: Result<&SqlQueryResult, Error> = Ok(&small);
assert_eq!(
icydb_schema::wire::encode_one(owned).expect("owned endpoint envelope should encode"),
icydb_schema::wire::encode_one(borrowed)
.expect("borrowed endpoint envelope should encode"),
"the zero-copy guard must measure the exact generated endpoint envelope",
);
assert_eq!(
small.into_deliverable_query_reply(),
Ok(SqlQueryResult::ShowIndexes {
entity: "Entry".to_string(),
indexes: vec!["by_owner".to_string()],
})
);
}
#[test]
fn generated_sql_query_reply_guard_returns_a_deliverable_typed_error() {
let oversized = SqlQueryResult::ShowIndexes {
entity: "Entry".to_string(),
indexes: vec!["x".repeat(MAX_PUBLIC_SQL_QUERY_REPLY_BYTES)],
};
let candidate: Result<&SqlQueryResult, Error> = Ok(&oversized);
assert!(
icydb_schema::wire::encoded_len(&candidate)
.expect("oversized endpoint candidate should remain fallibly sizeable")
> MAX_PUBLIC_SQL_QUERY_REPLY_BYTES,
);
let error = oversized
.into_deliverable_query_reply()
.expect_err("oversized success must become a typed response error");
assert_eq!(
error.code(),
crate::ErrorCode::RUNTIME_BOUNDARY_SQL_QUERY_REPLY_BYTES_EXCEEDED,
);
assert_eq!(error.origin(), ErrorOrigin::Response);
assert!(
icydb_schema::wire::encode_one(Err::<SqlQueryResult, Error>(error))
.expect("typed oversize error should encode")
.len()
<= MAX_PUBLIC_SQL_QUERY_REPLY_BYTES,
);
}
}