Skip to main content

mongreldb_kit/
arrow_util.rs

1//! Arrow IPC <-> JSON row helpers, shared by the embedded SQL surface
2//! ([`Database::sql`] / `sql_arrow` / `sql_rows`) and the optional `remote`
3//! HTTP client. Kept here (rather than inside `remote.rs`) so the default
4//! embedded build — which has no HTTP dependency — can still decode the Arrow
5//! IPC bytes that `MongrelSession::run` produces.
6
7use arrow::array::{
8    Array, BooleanArray, Float32Array, Float64Array, Int16Array, Int32Array, Int64Array, Int8Array,
9    NullArray, StringArray, UInt16Array, UInt32Array, UInt64Array, UInt8Array,
10};
11use arrow::ipc::reader::FileReader;
12use arrow::ipc::writer::FileWriter;
13use arrow::record_batch::RecordBatch;
14use serde_json::{json, Map, Value};
15
16use crate::error::{boxed_query_metadata, KitError, QueryExecutionOutcome, Result};
17use crate::SqlOutputLimits;
18
19/// Decode Arrow IPC *file* bytes (the format `MongrelSession` and the daemon
20/// both emit) into [`RecordBatch`]es. An empty input yields an empty vec.
21pub fn read_arrow_ipc(bytes: &[u8]) -> Result<Vec<RecordBatch>> {
22    if bytes.is_empty() {
23        return Ok(Vec::new());
24    }
25    let cursor = std::io::Cursor::new(bytes);
26    let reader = FileReader::try_new(cursor, None)
27        .map_err(|e| KitError::Storage(format!("arrow ipc decode: {e}")))?;
28    reader
29        .into_iter()
30        .collect::<std::result::Result<Vec<_>, _>>()
31        .map_err(|e| KitError::Storage(format!("arrow ipc decode: {e}")))
32}
33
34/// Encode `RecordBatch`es as Arrow IPC *file* bytes — the wire format the
35/// daemon and the NAPI addon both produce. Mirrors the node addon's
36/// `native_cols_to_ipc_from_batches`.
37pub fn batches_to_ipc(batches: &[RecordBatch]) -> Result<Vec<u8>> {
38    batches_to_ipc_with_checkpoint(batches, || Ok(()))
39}
40
41#[doc(hidden)]
42pub fn batches_to_ipc_controlled(
43    batches: &[RecordBatch],
44    query: &mongreldb_query::RegisteredSqlQuery,
45) -> Result<Vec<u8>> {
46    batches_to_ipc_controlled_with_limits(batches, query, SqlOutputLimits::default())
47}
48
49#[doc(hidden)]
50pub fn batches_to_ipc_controlled_with_limits(
51    batches: &[RecordBatch],
52    query: &mongreldb_query::RegisteredSqlQuery,
53    limits: SqlOutputLimits,
54) -> Result<Vec<u8>> {
55    let schema = batches
56        .first()
57        .map(|batch| batch.schema())
58        .unwrap_or_else(|| std::sync::Arc::new(arrow::datatypes::Schema::empty()));
59    let exceeded = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
60    let mut output = LimitedIpcOutput {
61        bytes: Vec::new(),
62        max_bytes: limits.max_bytes,
63        exceeded: std::sync::Arc::clone(&exceeded),
64    };
65    let mut rows = 0usize;
66    {
67        let mut writer = match FileWriter::try_new(&mut output, &schema) {
68            Ok(writer) => writer,
69            Err(error) => {
70                return serialization_or_limit_error(query, limits, &exceeded, error.to_string())
71            }
72        };
73        for batch in batches {
74            for offset in (0..batch.num_rows()).step_by(256) {
75                query.checkpoint().map_err(KitError::from)?;
76                let length = 256.min(batch.num_rows() - offset);
77                rows = rows.saturating_add(length);
78                if rows > limits.max_rows {
79                    return result_limit_error(query, limits);
80                }
81                if let Err(error) = writer.write(&batch.slice(offset, length)) {
82                    return serialization_or_limit_error(
83                        query,
84                        limits,
85                        &exceeded,
86                        error.to_string(),
87                    );
88                }
89            }
90        }
91        if let Err(error) = writer.finish() {
92            return serialization_or_limit_error(query, limits, &exceeded, error.to_string());
93        }
94    }
95    Ok(output.bytes)
96}
97
98fn batches_to_ipc_with_checkpoint(
99    batches: &[RecordBatch],
100    mut checkpoint: impl FnMut() -> Result<()>,
101) -> Result<Vec<u8>> {
102    let schema = batches
103        .first()
104        .map(|b| b.schema())
105        .unwrap_or_else(|| std::sync::Arc::new(arrow::datatypes::Schema::empty()));
106    let mut out = Vec::new();
107    let mut writer =
108        FileWriter::try_new(&mut out, &schema).map_err(|e| KitError::Storage(e.to_string()))?;
109    for batch in batches {
110        for offset in (0..batch.num_rows()).step_by(256) {
111            checkpoint()?;
112            let length = 256.min(batch.num_rows() - offset);
113            writer
114                .write(&batch.slice(offset, length))
115                .map_err(|e| KitError::Storage(format!("arrow ipc encode: {e}")))?;
116        }
117    }
118    writer
119        .finish()
120        .map_err(|e| KitError::Storage(format!("arrow ipc encode: {e}")))?;
121    Ok(out)
122}
123
124/// Materialize one `RecordBatch` into JSON-row maps (column name → value).
125pub fn batch_to_rows(b: &RecordBatch) -> Result<Vec<Map<String, Value>>> {
126    let schema = b.schema();
127    let mut rows = Vec::with_capacity(b.num_rows());
128    for r in 0..b.num_rows() {
129        let mut row = Map::new();
130        for (c, field) in schema.fields().iter().enumerate() {
131            let name = field.name();
132            let arr = b.column(c);
133            row.insert(name.clone(), cell_value(arr.as_ref(), r));
134        }
135        rows.push(row);
136    }
137    Ok(rows)
138}
139
140/// Flatten a slice of batches into a single list of JSON-row maps.
141pub fn batches_to_rows(batches: &[RecordBatch]) -> Result<Vec<Map<String, Value>>> {
142    batches_to_rows_with_checkpoint(batches, || Ok(()))
143}
144
145#[doc(hidden)]
146pub fn batches_to_rows_controlled(
147    batches: &[RecordBatch],
148    query: &mongreldb_query::RegisteredSqlQuery,
149) -> Result<Vec<Map<String, Value>>> {
150    batches_to_rows_controlled_with_limits(batches, query, SqlOutputLimits::default())
151}
152
153#[doc(hidden)]
154pub fn batches_to_rows_controlled_with_limits(
155    batches: &[RecordBatch],
156    query: &mongreldb_query::RegisteredSqlQuery,
157    limits: SqlOutputLimits,
158) -> Result<Vec<Map<String, Value>>> {
159    let mut rows = Vec::new();
160    let mut bytes = 2usize;
161    for batch in batches {
162        let schema = batch.schema();
163        for row_index in 0..batch.num_rows() {
164            if row_index % 256 == 0 {
165                query.checkpoint().map_err(KitError::from)?;
166            }
167            if rows.len() >= limits.max_rows {
168                return result_limit_error(query, limits);
169            }
170            let mut row = Map::new();
171            for (column_index, field) in schema.fields().iter().enumerate() {
172                row.insert(
173                    field.name().clone(),
174                    cell_value(batch.column(column_index).as_ref(), row_index),
175                );
176            }
177            let row_bytes = serde_json::to_vec(&row)
178                .map_err(|error| controlled_serialization_error(query, error.to_string()))?
179                .len();
180            bytes = bytes
181                .saturating_add(row_bytes)
182                .saturating_add(usize::from(!rows.is_empty()));
183            if bytes > limits.max_bytes {
184                return result_limit_error(query, limits);
185            }
186            rows.push(row);
187        }
188    }
189    Ok(rows)
190}
191
192fn result_limit_error<T>(
193    query: &mongreldb_query::RegisteredSqlQuery,
194    limits: SqlOutputLimits,
195) -> Result<T> {
196    Err(controlled_result_limit_error(query, limits))
197}
198
199#[doc(hidden)]
200pub fn controlled_result_limit_error(
201    query: &mongreldb_query::RegisteredSqlQuery,
202    limits: SqlOutputLimits,
203) -> KitError {
204    let status = query.status();
205    KitError::ResultLimitExceeded {
206        query_id: Some(query.id().to_string().into_boxed_str()),
207        max_rows: Some(Box::new(limits.max_rows)),
208        max_bytes: Some(Box::new(limits.max_bytes)),
209        outcome: Box::new(QueryExecutionOutcome {
210            committed: status.durable_outcome.committed,
211            committed_statements: Some(status.durable_outcome.committed_statements),
212            last_commit_epoch: status.durable_outcome.last_commit_epoch,
213            first_commit_statement_index: status.durable_outcome.first_commit_statement_index,
214            last_commit_statement_index: status.durable_outcome.last_commit_statement_index,
215            completed_statements: status.completed_statements,
216            statement_index: status.statement_index,
217        }),
218        message: format!(
219            "SQL result exceeds {} rows or {} bytes",
220            limits.max_rows, limits.max_bytes
221        )
222        .into_boxed_str(),
223        metadata: boxed_query_metadata(None, None, Some(false), Some("serializing")),
224    }
225}
226
227fn serialization_or_limit_error<T>(
228    query: &mongreldb_query::RegisteredSqlQuery,
229    limits: SqlOutputLimits,
230    exceeded: &std::sync::atomic::AtomicBool,
231    message: String,
232) -> Result<T> {
233    if exceeded.load(std::sync::atomic::Ordering::Acquire) {
234        return result_limit_error(query, limits);
235    }
236    Err(controlled_serialization_error(query, message))
237}
238
239#[doc(hidden)]
240pub fn controlled_serialization_error(
241    query: &mongreldb_query::RegisteredSqlQuery,
242    message: String,
243) -> KitError {
244    let status = query.status();
245    KitError::SerializationFailed {
246        query_id: Some(query.id().to_string()),
247        outcome: Box::new(QueryExecutionOutcome {
248            committed: status.durable_outcome.committed,
249            committed_statements: Some(status.durable_outcome.committed_statements),
250            last_commit_epoch: status.durable_outcome.last_commit_epoch,
251            first_commit_statement_index: status.durable_outcome.first_commit_statement_index,
252            last_commit_statement_index: status.durable_outcome.last_commit_statement_index,
253            completed_statements: status.completed_statements,
254            statement_index: status.statement_index,
255        }),
256        message: message.into_boxed_str(),
257        metadata: boxed_query_metadata(None, None, Some(false), Some("serializing")),
258    }
259}
260
261struct LimitedIpcOutput {
262    bytes: Vec<u8>,
263    max_bytes: usize,
264    exceeded: std::sync::Arc<std::sync::atomic::AtomicBool>,
265}
266
267impl std::io::Write for LimitedIpcOutput {
268    fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
269        if self.bytes.len().saturating_add(bytes.len()) > self.max_bytes {
270            self.exceeded
271                .store(true, std::sync::atomic::Ordering::Release);
272            return Err(std::io::Error::other("SQL result byte limit exceeded"));
273        }
274        self.bytes.extend_from_slice(bytes);
275        Ok(bytes.len())
276    }
277
278    fn flush(&mut self) -> std::io::Result<()> {
279        Ok(())
280    }
281}
282
283fn batches_to_rows_with_checkpoint(
284    batches: &[RecordBatch],
285    mut checkpoint: impl FnMut() -> Result<()>,
286) -> Result<Vec<Map<String, Value>>> {
287    let mut rows = Vec::new();
288    for batch in batches {
289        let schema = batch.schema();
290        for row_index in 0..batch.num_rows() {
291            if row_index % 256 == 0 {
292                checkpoint()?;
293            }
294            let mut row = Map::new();
295            for (column_index, field) in schema.fields().iter().enumerate() {
296                row.insert(
297                    field.name().clone(),
298                    cell_value(batch.column(column_index).as_ref(), row_index),
299                );
300            }
301            rows.push(row);
302        }
303    }
304    Ok(rows)
305}
306
307fn cell_value(arr: &dyn Array, r: usize) -> Value {
308    if arr.is_null(r) {
309        return Value::Null;
310    }
311    // Signed integers → i64
312    if let Some(a) = arr.as_any().downcast_ref::<Int64Array>() {
313        return json!(a.value(r));
314    }
315    if let Some(a) = arr.as_any().downcast_ref::<Int32Array>() {
316        return json!(a.value(r));
317    }
318    if let Some(a) = arr.as_any().downcast_ref::<Int16Array>() {
319        return json!(a.value(r));
320    }
321    if let Some(a) = arr.as_any().downcast_ref::<Int8Array>() {
322        return json!(a.value(r));
323    }
324    // Unsigned integers → i64 (JSON has no unsigned; cast is lossless for normal values)
325    if let Some(a) = arr.as_any().downcast_ref::<UInt64Array>() {
326        return json!(a.value(r) as i64);
327    }
328    if let Some(a) = arr.as_any().downcast_ref::<UInt32Array>() {
329        return json!(a.value(r) as i64);
330    }
331    if let Some(a) = arr.as_any().downcast_ref::<UInt16Array>() {
332        return json!(a.value(r) as i64);
333    }
334    if let Some(a) = arr.as_any().downcast_ref::<UInt8Array>() {
335        return json!(a.value(r) as i64);
336    }
337    // Floats
338    if let Some(a) = arr.as_any().downcast_ref::<Float64Array>() {
339        return serde_json::Number::from_f64(a.value(r))
340            .map(Value::Number)
341            .unwrap_or(Value::Null);
342    }
343    if let Some(a) = arr.as_any().downcast_ref::<Float32Array>() {
344        return serde_json::Number::from_f64(a.value(r) as f64)
345            .map(Value::Number)
346            .unwrap_or(Value::Null);
347    }
348    // Boolean
349    if let Some(a) = arr.as_any().downcast_ref::<BooleanArray>() {
350        return Value::Bool(a.value(r));
351    }
352    // Strings
353    if let Some(a) = arr.as_any().downcast_ref::<StringArray>() {
354        return Value::String(a.value(r).to_string());
355    }
356    if arr.as_any().downcast_ref::<NullArray>().is_some() {
357        return Value::Null;
358    }
359    // Fallback: stringify unknown types.
360    Value::String(format!("{arr:?}"))
361}
362
363#[cfg(test)]
364mod tests {
365    use super::{batches_to_ipc_controlled_with_limits, batches_to_rows_controlled_with_limits};
366    use crate::{KitError, SqlOutputLimits};
367    use arrow::array::Int64Array;
368    use arrow::record_batch::RecordBatch;
369    use mongreldb_query::{SqlQueryOptions, SqlQueryRegistry};
370    use std::sync::Arc;
371
372    fn batch() -> RecordBatch {
373        RecordBatch::try_from_iter([(
374            "value",
375            Arc::new(Int64Array::from(vec![1, 2])) as arrow::array::ArrayRef,
376        )])
377        .unwrap()
378    }
379
380    #[test]
381    fn controlled_converters_enforce_row_and_byte_limits() {
382        for convert in ["arrow", "rows"] {
383            for limits in [
384                SqlOutputLimits {
385                    max_rows: 1,
386                    max_bytes: 1_024,
387                },
388                SqlOutputLimits {
389                    max_rows: 10,
390                    max_bytes: 1,
391                },
392            ] {
393                let registry = Arc::new(SqlQueryRegistry::default());
394                let query = registry.register(SqlQueryOptions::default()).unwrap();
395                query.record_commit(2, 17);
396                let batch = batch();
397                let error = if convert == "arrow" {
398                    batches_to_ipc_controlled_with_limits(
399                        std::slice::from_ref(&batch),
400                        &query,
401                        limits,
402                    )
403                    .map(|_| ())
404                    .unwrap_err()
405                } else {
406                    batches_to_rows_controlled_with_limits(
407                        std::slice::from_ref(&batch),
408                        &query,
409                        limits,
410                    )
411                    .map(|_| ())
412                    .unwrap_err()
413                };
414                assert!(matches!(
415                    error,
416                    KitError::ResultLimitExceeded {
417                        outcome,
418                        ..
419                    } if outcome.committed
420                        && outcome.committed_statements == Some(1)
421                        && outcome.last_commit_epoch == Some(17)
422                        && outcome.first_commit_statement_index == Some(2)
423                        && outcome.last_commit_statement_index == Some(2)
424                ));
425                query.fail_result_limit();
426                assert_eq!(
427                    query.status().terminal_error.unwrap().code,
428                    "RESULT_LIMIT_EXCEEDED"
429                );
430            }
431        }
432    }
433}