Skip to main content

velesdb_mobile/
query.rs

1//! VelesQL query execution via UniFFI for mobile targets.
2//!
3//! Exposes `execute_query()` on [`VelesDatabase`] so iOS/Android apps can
4//! run arbitrary VelesQL statements (SELECT, INSERT, UPDATE, DELETE, MATCH,
5//! DDL, TRAIN, SHOW, FLUSH, etc.) through a single entry point.
6//!
7//! Results are returned as [`QueryResult`], a UniFFI-friendly struct that
8//! encodes rows as JSON strings (because UniFFI cannot represent
9//! `HashMap<String, serde_json::Value>` directly).
10
11use std::collections::HashMap;
12
13use crate::types::VelesError;
14
15// ============================================================================
16// UniFFI-exported types
17// ============================================================================
18
19/// Classifies the kind of VelesQL statement that was executed.
20#[derive(Debug, Clone, uniffi::Enum)]
21pub enum QueryResultKind {
22    /// Row-returning query (SELECT, MATCH, SHOW, DESCRIBE).
23    Rows,
24    /// Data manipulation that returns affected rows (INSERT, UPSERT, UPDATE).
25    Mutation,
26    /// Deletion that returns affected count.
27    Deletion,
28    /// DDL statement (CREATE, DROP, ALTER, TRUNCATE).
29    Ddl,
30    /// TRAIN QUANTIZER.
31    Train,
32    /// Admin command (FLUSH, ANALYZE).
33    Admin,
34}
35
36/// A single row in a query result, serialized as JSON for FFI safety.
37///
38/// UniFFI cannot represent `HashMap<String, serde_json::Value>` directly,
39/// so each row is a JSON object string that the mobile client deserializes
40/// with its native JSON parser (Swift `JSONSerialization`, Kotlin `Gson`).
41#[derive(Debug, Clone, uniffi::Record)]
42pub struct QueryResultRow {
43    /// Point ID (0 for non-point results like SHOW COLLECTIONS).
44    pub id: u64,
45    /// Similarity / relevance score (0.0 for non-search results).
46    pub score: f32,
47    /// Full row data as a JSON object string.
48    /// Contains `id`, `score`, and all payload fields merged at top level.
49    pub data_json: String,
50}
51
52/// Result of executing a VelesQL query via [`crate::VelesDatabase::execute_query`].
53#[derive(Debug, Clone, uniffi::Record)]
54pub struct QueryResult {
55    /// What kind of statement produced this result.
56    pub kind: QueryResultKind,
57    /// Result rows (empty for DDL/TRAIN/FLUSH that return no data).
58    pub rows: Vec<QueryResultRow>,
59    /// Number of rows in the result (convenience field for mobile).
60    pub row_count: u32,
61    /// Human-readable status message (e.g., "3 rows inserted").
62    pub message: String,
63}
64
65// ============================================================================
66// Conversion helpers
67// ============================================================================
68
69/// Classifies a parsed query into its [`QueryResultKind`].
70pub(crate) fn classify_query(query: &velesdb_core::velesql::Query) -> QueryResultKind {
71    if query.is_train() {
72        QueryResultKind::Train
73    } else if query.is_ddl_query() {
74        QueryResultKind::Ddl
75    } else if query.is_admin_query() {
76        QueryResultKind::Admin
77    } else if query.is_dml_query() {
78        classify_dml(query)
79    } else {
80        // SELECT, MATCH, introspection (SHOW/DESCRIBE/EXPLAIN)
81        QueryResultKind::Rows
82    }
83}
84
85/// Distinguishes DELETE from other DML (INSERT/UPSERT/UPDATE).
86fn classify_dml(query: &velesdb_core::velesql::Query) -> QueryResultKind {
87    use velesdb_core::velesql::DmlStatement;
88    match query.dml.as_ref() {
89        Some(DmlStatement::Delete(_) | DmlStatement::DeleteEdge(_)) => QueryResultKind::Deletion,
90        _ => QueryResultKind::Mutation,
91    }
92}
93
94/// Converts a core `SearchResult` into a [`QueryResultRow`].
95///
96/// Flattens the point payload into the top-level JSON object alongside
97/// `id` and `score` fields, matching the CLI REPL output format.
98pub(crate) fn to_result_row(
99    result: &velesdb_core::SearchResult,
100) -> Result<QueryResultRow, VelesError> {
101    let mut map = serde_json::Map::new();
102    map.insert("id".to_string(), serde_json::json!(result.point.id));
103    map.insert("score".to_string(), serde_json::json!(result.score));
104
105    if let Some(serde_json::Value::Object(payload)) = &result.point.payload {
106        for (k, v) in payload {
107            if k != "id" && k != "score" {
108                map.insert(k.clone(), v.clone());
109            }
110        }
111    }
112
113    let data_json = serde_json::to_string(&serde_json::Value::Object(map))
114        .map_err(|e| VelesError::database(format!("Failed to serialize row to JSON: {e}")))?;
115
116    Ok(QueryResultRow {
117        id: result.point.id,
118        score: result.score,
119        data_json,
120    })
121}
122
123/// Builds the human-readable message for the query result.
124pub(crate) fn build_message(kind: &QueryResultKind, row_count: u32) -> String {
125    match kind {
126        QueryResultKind::Rows => format!("{row_count} row(s) returned"),
127        QueryResultKind::Mutation => format!("{row_count} row(s) affected"),
128        QueryResultKind::Deletion => format!("{row_count} row(s) deleted"),
129        QueryResultKind::Ddl => "DDL statement executed successfully".to_string(),
130        QueryResultKind::Train => "Training completed successfully".to_string(),
131        QueryResultKind::Admin => "Admin command executed successfully".to_string(),
132    }
133}
134
135/// Parses a JSON string into query parameters.
136///
137/// VelesQL parameters use `$name` syntax. The params map keys should
138/// be the bare name (without the `$` prefix).
139pub(crate) fn parse_params(
140    params_json: Option<String>,
141) -> Result<HashMap<String, serde_json::Value>, VelesError> {
142    params_json
143        .map(|json| {
144            serde_json::from_str(&json)
145                .map_err(|e| VelesError::database(format!("Invalid params JSON: {e}")))
146        })
147        .transpose()
148        .map(Option::unwrap_or_default)
149}
150
151#[cfg(test)]
152#[path = "query_tests.rs"]
153mod integration_tests;
154
155#[cfg(test)]
156#[path = "query_unit_tests.rs"]
157mod tests;