octra_sqlite/client/
results.rs1use super::error::{Error, ErrorKind, Result};
2use serde_json::Value;
3
4#[derive(Debug, Clone, PartialEq)]
6pub struct QueryResult {
7 pub columns: Vec<String>,
9 pub rows: Vec<Vec<Value>>,
11 pub row_count: usize,
13 raw: Value,
14}
15
16impl QueryResult {
17 pub fn from_value(value: Value) -> Result<Self> {
18 let columns = value
19 .get("columns")
20 .and_then(Value::as_array)
21 .ok_or_else(|| Error::with_kind(ErrorKind::Decode, "query result missing columns"))?
22 .iter()
23 .map(|column| {
24 column.as_str().map(str::to_string).ok_or_else(|| {
25 Error::with_kind(ErrorKind::Decode, "query result column must be a string")
26 })
27 })
28 .collect::<Result<Vec<_>>>()?;
29 let rows = value
30 .get("rows")
31 .and_then(Value::as_array)
32 .ok_or_else(|| Error::with_kind(ErrorKind::Decode, "query result missing rows"))?
33 .iter()
34 .map(|row| {
35 row.as_array().cloned().ok_or_else(|| {
36 Error::with_kind(ErrorKind::Decode, "query result row must be an array")
37 })
38 })
39 .collect::<Result<Vec<_>>>()?;
40 let row_count = value
41 .get("row_count")
42 .and_then(Value::as_u64)
43 .map(|count| count as usize)
44 .unwrap_or(rows.len());
45 if row_count != rows.len() {
46 return Err(Error::with_kind(
47 ErrorKind::Decode,
48 format!(
49 "query result row_count {row_count} does not match {} rows",
50 rows.len()
51 ),
52 ));
53 }
54 for row in &rows {
55 if row.len() != columns.len() {
56 return Err(Error::with_kind(
57 ErrorKind::Decode,
58 format!(
59 "query result row has {} cells but {} columns",
60 row.len(),
61 columns.len()
62 ),
63 ));
64 }
65 }
66 Ok(Self {
67 columns,
68 rows,
69 row_count,
70 raw: value,
71 })
72 }
73
74 pub fn raw(&self) -> &Value {
75 &self.raw
76 }
77}
78
79#[derive(Debug, Clone, PartialEq)]
81pub struct SubmittedTransaction {
82 pub circle: Option<String>,
84 pub wallet: Option<String>,
86 pub tx_hash: Option<String>,
88 pub result: Value,
90}
91
92impl SubmittedTransaction {
93 pub fn from_value(value: Value) -> Result<Self> {
94 Ok(Self {
95 circle: string_field(&value, "circle"),
96 wallet: string_field(&value, "wallet"),
97 tx_hash: string_field(&value, "tx_hash"),
98 result: value.get("result").cloned().ok_or_else(|| {
99 Error::with_kind(ErrorKind::Rpc, "submitted transaction missing result")
100 })?,
101 })
102 }
103}
104
105#[derive(Debug, Clone, PartialEq)]
107pub struct ExecuteResult {
108 pub submitted: SubmittedTransaction,
110 pub receipt: Value,
112}
113
114impl ExecuteResult {
115 pub fn from_value(value: Value) -> Result<Self> {
116 let submitted = SubmittedTransaction::from_value(value.clone())?;
117 let receipt = value
118 .get("receipt")
119 .cloned()
120 .ok_or_else(|| Error::with_kind(ErrorKind::Receipt, "exec result missing receipt"))?;
121 ensure_receipt_success(&receipt)?;
122 Ok(Self { submitted, receipt })
123 }
124}
125
126#[derive(Debug, Clone, PartialEq)]
128pub struct ProgramInfo {
129 pub version: Option<String>,
130 pub code_hash: Option<String>,
131 pub code_bytes: Option<u64>,
132 raw: Value,
133}
134
135impl ProgramInfo {
136 pub fn from_value(value: Value) -> Result<Self> {
137 Ok(Self {
138 version: string_field(&value, "version"),
139 code_hash: string_field(&value, "code_hash"),
140 code_bytes: value
141 .get("code_bytes")
142 .and_then(|value| value.as_u64().or_else(|| value.as_str()?.parse().ok())),
143 raw: value,
144 })
145 }
146
147 pub fn raw(&self) -> &Value {
148 &self.raw
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct AuthInfo {
155 pub configured: bool,
156 pub db_id: String,
157 pub owner_pubkey: Option<String>,
158 pub owner_sequence: Option<u64>,
159}
160
161pub(super) fn ensure_receipt_success(receipt: &Value) -> Result<()> {
162 let sql_error = event_values(receipt, "octra.sqlite.error");
163 let failed = receipt.get("success").and_then(Value::as_bool) == Some(false)
164 || receipt.get("error").is_some_and(|error| !error.is_null())
165 || sql_error.is_some();
166 if failed {
167 return Err(Error::with_kind(
168 ErrorKind::Receipt,
169 format!(
170 "SQL execution failed: {}",
171 sql_error
172 .map(|error| format_sql_error_event(&error))
173 .unwrap_or_else(|| receipt_error_text(receipt))
174 ),
175 ));
176 }
177 Ok(())
178}
179
180fn event_values(receipt: &Value, topic: &str) -> Option<String> {
181 receipt
182 .get("events")?
183 .as_array()?
184 .iter()
185 .find(|event| event.get("event").and_then(Value::as_str) == Some(topic))
186 .and_then(|event| event.get("values"))
187 .and_then(Value::as_array)
188 .map(|values| {
189 values
190 .iter()
191 .map(value_to_event_text)
192 .collect::<Vec<_>>()
193 .join(", ")
194 })
195}
196
197fn receipt_error_text(receipt: &Value) -> String {
198 receipt
199 .get("error")
200 .filter(|error| !error.is_null())
201 .map(value_to_compact_text)
202 .unwrap_or_else(|| value_to_compact_text(receipt))
203}
204
205fn value_to_compact_text(value: &Value) -> String {
206 serde_json::to_string(value).unwrap_or_else(|_| value.to_string())
207}
208
209fn value_to_event_text(value: &Value) -> String {
210 value
211 .as_str()
212 .map(str::to_string)
213 .unwrap_or_else(|| value_to_compact_text(value))
214}
215
216fn format_sql_error_event(error: &str) -> String {
217 match error.split_once(':') {
218 Some((code, detail)) if !detail.is_empty() => {
219 format!("database error ({code}): {detail}")
220 }
221 _ => error.to_string(),
222 }
223}
224
225fn string_field(value: &Value, key: &str) -> Option<String> {
226 value.get(key).and_then(Value::as_str).map(str::to_string)
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use serde_json::json;
233
234 #[test]
235 fn query_result_validates_rectangular_rows() {
236 let error = QueryResult::from_value(json!({
237 "columns": ["a", "b"],
238 "rows": [[1]],
239 "row_count": 1,
240 }))
241 .unwrap_err();
242 assert_eq!(error.kind(), ErrorKind::Decode);
243 }
244
245 #[test]
246 fn receipt_success_with_sql_error_event_is_failed_execution() {
247 let receipt = json!({
248 "success": true,
249 "error": null,
250 "events": [{
251 "event": "octra.sqlite.error",
252 "values": ["sqlite_exec_failed:no such table: correction"]
253 }]
254 });
255 let error = ensure_receipt_success(&receipt).unwrap_err();
256 assert_eq!(error.kind(), ErrorKind::Receipt);
257 assert!(
258 error
259 .to_string()
260 .contains("database error (sqlite_exec_failed): no such table: correction")
261 );
262 }
263}