Skip to main content

octra_sqlite/client/
results.rs

1use super::error::{Error, ErrorKind, Result};
2use serde_json::Value;
3
4/// Result of read SQL.
5#[derive(Debug, Clone, PartialEq)]
6pub struct QueryResult {
7    /// Column names returned by SQLite.
8    pub columns: Vec<String>,
9    /// Rows as JSON values in column order.
10    pub rows: Vec<Vec<Value>>,
11    /// Number of returned rows.
12    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/// Submitted Octra transaction returned by no-wait write paths.
80#[derive(Debug, Clone, PartialEq)]
81pub struct SubmittedTransaction {
82    /// Target Circle ID when known.
83    pub circle: Option<String>,
84    /// Submitting wallet address when known.
85    pub wallet: Option<String>,
86    /// Transaction hash when the RPC returned one.
87    pub tx_hash: Option<String>,
88    /// Raw submit result.
89    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/// Result of a write that has been submitted and confirmed.
106#[derive(Debug, Clone, PartialEq)]
107pub struct ExecuteResult {
108    /// Submitted transaction metadata.
109    pub submitted: SubmittedTransaction,
110    /// Confirmed transaction receipt.
111    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/// Deployed Circle program metadata.
127#[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/// Owner-write authorization metadata exposed by the Circle program.
153#[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}