1use crate::FaucetError;
20use arrow::array::RecordBatch;
21use arrow::datatypes::{Schema, SchemaRef};
22use serde_json::Value;
23use std::sync::Arc;
24
25#[derive(Debug, Clone)]
33pub struct ColumnarPage {
34 pub batch: RecordBatch,
36 pub bookmark: Option<Value>,
38}
39
40impl ColumnarPage {
41 pub fn new(batch: RecordBatch, bookmark: Option<Value>) -> Self {
43 Self { batch, bookmark }
44 }
45
46 pub fn num_rows(&self) -> usize {
48 self.batch.num_rows()
49 }
50}
51
52fn te<E: std::fmt::Display>(ctx: &str, e: E) -> FaucetError {
54 FaucetError::Transform(format!("columnar shim: {ctx}: {e}"))
55}
56
57pub fn infer_arrow_schema(records: &[Value]) -> Result<SchemaRef, FaucetError> {
62 let iter = records
63 .iter()
64 .map(|v| Ok::<_, arrow::error::ArrowError>(v.clone()));
65 let schema = arrow_json::reader::infer_json_schema_from_iterator(iter)
66 .map_err(|e| te("schema inference", e))?;
67 refine_wide_integers(&schema, records).map(Arc::new)
68}
69
70fn refine_wide_integers(schema: &Schema, records: &[Value]) -> Result<Schema, FaucetError> {
85 use arrow::datatypes::{DataType, Field};
86
87 fn observed<'a>(records: &'a [Value], name: &str) -> Vec<&'a Value> {
89 records
90 .iter()
91 .filter_map(|r| r.get(name))
92 .filter(|v| !v.is_null())
93 .collect()
94 }
95
96 fn is_wide_integer(v: &Value) -> bool {
98 matches!(v, Value::Number(n) if !n.is_i64() && n.is_u64())
99 }
100
101 fn refine_field(field: &Field, values: Vec<&Value>) -> Result<Field, FaucetError> {
102 match field.data_type() {
103 DataType::Float64 if values.iter().any(|v| is_wide_integer(v)) => {
104 if values
106 .iter()
107 .all(|v| matches!(v, Value::Number(n) if n.is_u64()))
108 {
109 Ok(field.clone().with_data_type(DataType::UInt64))
110 } else {
111 Err(FaucetError::Transform(format!(
112 "columnar shim: field {:?} mixes an integer above i64::MAX with values \
113 no unsigned type can hold, so no exact Arrow type fits. Convert it to a \
114 string first (a `cast` transform) — it is not silently widened to a \
115 float because that loses the exact value",
116 field.name()
117 )))
118 }
119 }
120 DataType::Struct(children) => {
121 let refined = children
122 .iter()
123 .map(|child| {
124 let child_values = values
125 .iter()
126 .filter_map(|v| v.get(child.name()))
127 .filter(|v| !v.is_null())
128 .collect();
129 refine_field(child, child_values).map(Arc::new)
130 })
131 .collect::<Result<Vec<_>, _>>()?;
132 Ok(field
133 .clone()
134 .with_data_type(DataType::Struct(refined.into())))
135 }
136 _ if contains_wide_integer_in_container(&values) => {
139 Err(FaucetError::Transform(format!(
140 "columnar shim: field {:?} contains an integer above i64::MAX inside a list, \
141 which the columnar path cannot represent exactly. Convert those elements to \
142 strings first (a `cast` transform)",
143 field.name()
144 )))
145 }
146 _ => Ok(field.clone()),
147 }
148 }
149
150 fn contains_wide_integer_in_container(values: &[&Value]) -> bool {
152 fn walk(v: &Value, in_list: bool) -> bool {
153 match v {
154 Value::Array(items) => items.iter().any(|i| walk(i, true)),
155 Value::Object(map) => map.values().any(|i| walk(i, in_list)),
156 other => in_list && is_wide_integer(other),
157 }
158 }
159 values.iter().any(|v| walk(v, false))
160 }
161
162 let fields = schema
163 .fields()
164 .iter()
165 .map(|f| refine_field(f, observed(records, f.name())).map(Arc::new))
166 .collect::<Result<Vec<_>, _>>()?;
167 Ok(Schema::new(fields).with_metadata(schema.metadata().clone()))
168}
169
170pub fn values_to_record_batch(
174 records: &[Value],
175 schema: SchemaRef,
176) -> Result<RecordBatch, FaucetError> {
177 let mut decoder = arrow_json::ReaderBuilder::new(schema.clone())
178 .build_decoder()
179 .map_err(|e| te("decoder build", e))?;
180 decoder.serialize(records).map_err(|e| te("encode", e))?;
181 let mut batches = Vec::new();
182 while let Some(b) = decoder.flush().map_err(|e| te("flush", e))? {
183 batches.push(b);
184 }
185 if batches.is_empty() {
186 return Ok(RecordBatch::new_empty(schema));
187 }
188 if batches.len() == 1 {
189 return Ok(batches.pop().unwrap());
190 }
191 arrow::compute::concat_batches(&schema, &batches).map_err(|e| te("concat", e))
192}
193
194pub fn values_to_record_batch_inferred(records: &[Value]) -> Result<RecordBatch, FaucetError> {
196 let schema = infer_arrow_schema(records)?;
197 values_to_record_batch(records, schema)
198}
199
200pub fn record_batch_to_values(batch: &RecordBatch) -> Result<Vec<Value>, FaucetError> {
207 let mut buf = Vec::new();
208 {
209 let mut writer = arrow_json::writer::WriterBuilder::new()
210 .with_explicit_nulls(true)
211 .build::<_, arrow_json::writer::JsonArray>(&mut buf);
212 writer.write(batch).map_err(|e| te("json write", e))?;
213 writer.finish().map_err(|e| te("json finish", e))?;
214 }
215 serde_json::from_slice(&buf).map_err(|e| te("json parse", e))
216}
217
218pub fn schema_eq(a: &Schema, b: &Schema) -> bool {
220 a.fields() == b.fields()
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use serde_json::json;
227
228 #[test]
229 fn round_trip_scalars_nulls_nested() {
230 let recs = vec![
231 json!({"id": 1, "name": "a", "score": 1.5, "ok": true, "tags": ["x", "y"]}),
232 json!({"id": 2, "name": null, "score": null, "ok": false, "tags": []}),
233 ];
234 let batch = values_to_record_batch_inferred(&recs).unwrap();
235 assert_eq!(batch.num_rows(), 2);
236 let back = record_batch_to_values(&batch).unwrap();
237 assert_eq!(back[0]["id"], json!(1));
238 assert_eq!(back[0]["tags"], json!(["x", "y"]));
239 assert!(back[1].as_object().unwrap().contains_key("name"));
241 assert_eq!(back[1]["name"], json!(null));
242 }
243
244 #[test]
245 fn empty_records_yield_empty_batch_and_back() {
246 let schema = infer_arrow_schema(&[json!({"a": 1})]).unwrap();
247 let batch = values_to_record_batch(&[], schema).unwrap();
248 assert_eq!(batch.num_rows(), 0);
249 assert!(record_batch_to_values(&batch).unwrap().is_empty());
250 }
251
252 #[test]
253 fn columnar_page_reports_rows() {
254 let batch = values_to_record_batch_inferred(&[json!({"a": 1}), json!({"a": 2})]).unwrap();
255 let page = ColumnarPage::new(batch, Some(json!({"lsn": 42})));
256 assert_eq!(page.num_rows(), 2);
257 assert_eq!(page.bookmark, Some(json!({"lsn": 42})));
258 }
259}
260
261#[cfg(test)]
262mod wide_integer_tests {
263 use super::*;
264 use serde_json::json;
265
266 #[test]
270 fn u64_above_i64_max_round_trips_exactly() {
271 let recs = vec![json!({"id": u64::MAX})];
272 let back =
273 record_batch_to_values(&values_to_record_batch_inferred(&recs).unwrap()).unwrap();
274 assert_eq!(back[0]["id"], json!(u64::MAX), "exact value must survive");
275 assert!(back[0]["id"].is_u64(), "and stay an integer, not a float");
276 }
277
278 #[test]
279 fn mixed_small_and_wide_integers_all_survive() {
280 let recs = vec![
281 json!({"id": 1}),
282 json!({"id": u64::MAX}),
283 json!({"id": null}),
284 ];
285 let back =
286 record_batch_to_values(&values_to_record_batch_inferred(&recs).unwrap()).unwrap();
287 assert_eq!(back[0]["id"], json!(1));
288 assert_eq!(back[1]["id"], json!(u64::MAX));
289 assert_eq!(back[2]["id"], json!(null));
290 }
291
292 #[test]
293 fn wide_integer_nested_in_a_struct_survives() {
294 let recs = vec![json!({"outer": {"id": u64::MAX, "n": 3}})];
295 let back =
296 record_batch_to_values(&values_to_record_batch_inferred(&recs).unwrap()).unwrap();
297 assert_eq!(back[0]["outer"]["id"], json!(u64::MAX));
298 assert_eq!(back[0]["outer"]["n"], json!(3));
299 }
300
301 #[test]
305 fn genuine_floats_are_untouched() {
306 let recs = vec![json!({"a": 1.5}), json!({"a": 2})];
307 let back =
308 record_batch_to_values(&values_to_record_batch_inferred(&recs).unwrap()).unwrap();
309 assert_eq!(back[0]["a"], json!(1.5));
310 assert_eq!(back[1]["a"], json!(2.0));
311
312 let recs = vec![json!({"n": 9007199254740993i64, "m": i64::MIN})];
314 let back =
315 record_batch_to_values(&values_to_record_batch_inferred(&recs).unwrap()).unwrap();
316 assert_eq!(back[0]["n"], json!(9007199254740993i64));
317 assert_eq!(back[0]["m"], json!(i64::MIN));
318 }
319
320 #[test]
323 fn unrepresentable_mix_is_refused() {
324 let recs = vec![json!({"v": -1}), json!({"v": u64::MAX})];
325 let err = match values_to_record_batch_inferred(&recs) {
326 Err(e) => e.to_string(),
327 Ok(b) => panic!("must refuse; got {:?}", record_batch_to_values(&b).unwrap()),
328 };
329 assert!(err.contains("\"v\""), "{err}");
330 assert!(err.contains("cast"), "points at the workaround: {err}");
331 }
332
333 #[test]
336 fn wide_integer_inside_a_list_is_refused() {
337 let recs = vec![json!({"ids": [1, u64::MAX]})];
338 let err = match values_to_record_batch_inferred(&recs) {
339 Err(e) => e.to_string(),
340 Ok(_) => panic!("must refuse a wide integer inside a list"),
341 };
342 assert!(err.contains("\"ids\""), "{err}");
343 assert!(err.contains("list"), "{err}");
344 }
345}