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;