1use anyhow::{anyhow, Result};
4use arrow::array::{
5 ArrayRef, BooleanBuilder, Float64Builder, Int64Builder, StringBuilder,
6};
7use arrow::datatypes::{DataType, SchemaRef};
8use arrow::record_batch::RecordBatch;
9
10pub struct JsonExtractSpec {
11 pub paths: Vec<String>,
12}
13
14pub struct JShiftExtractor {
15 pub spec: JsonExtractSpec,
16}
17
18impl JShiftExtractor {
19 pub fn new(paths: Vec<String>) -> Self {
20 Self {
21 spec: JsonExtractSpec { paths },
22 }
23 }
24
25 pub fn extract_jsonl(&self, jsonl_bytes: &[u8], schema: SchemaRef) -> Result<RecordBatch> {
28 let lines: Vec<&[u8]> = jsonl_bytes
29 .split(|&b| b == b'\n')
30 .map(|line| trim_bytes(line)) .filter(|line| !line.is_empty())
32 .collect();
33
34 let num_rows = lines.len();
35
36 let parsed_paths: Vec<Vec<jshift::PathSegment<'_>>> = self
38 .spec
39 .paths
40 .iter()
41 .map(|p| {
42 jshift::try_parse_path(p)
43 .map_err(|e| anyhow!("Failed to parse JSON path '{}': {:?}", p, e))
44 })
45 .collect::<Result<_>>()?;
46
47 let mut builders: Vec<Box<dyn arrow::array::ArrayBuilder>> = Vec::new();
49 for field in schema.fields() {
50 match field.data_type() {
51 DataType::Int64 => builders.push(Box::new(Int64Builder::with_capacity(num_rows))),
52 DataType::Float64 => builders.push(Box::new(Float64Builder::with_capacity(num_rows))),
53 DataType::Boolean => builders.push(Box::new(BooleanBuilder::with_capacity(num_rows))),
54 DataType::Utf8 | DataType::LargeUtf8 => {
55 builders.push(Box::new(StringBuilder::with_capacity(num_rows, num_rows * 16)))
56 }
57 other => anyhow::bail!("Unsupported target Arrow data type for JSON extraction: {:?}", other),
58 }
59 }
60
61 let path_indices: Vec<Option<usize>> = schema
63 .fields()
64 .iter()
65 .map(|field| self.spec.paths.iter().position(|p| p == field.name()))
66 .collect();
67
68 for line in lines {
70 for (col_idx, &path_idx) in path_indices.iter().enumerate() {
71 let builder = &mut builders[col_idx];
72 let field = schema.field(col_idx);
73
74 if let Some(p_idx) = path_idx {
75 let path = &parsed_paths[p_idx];
76 match jshift::find_value(line, path) {
77 Ok(val_bytes) => {
78 append_value(builder, field.data_type(), val_bytes)?;
79 }
80 Err(_) => {
81 append_null(builder, field.data_type())?;
82 }
83 }
84 } else {
85 append_null(builder, field.data_type())?;
86 }
87 }
88 }
89
90 let arrays: Vec<ArrayRef> = builders.into_iter().map(|mut b| b.finish()).collect();
92 let batch = RecordBatch::try_new(schema, arrays)?;
93 Ok(batch)
94 }
95}
96
97fn trim_bytes(mut s: &[u8]) -> &[u8] {
98 while !s.is_empty() && (s[0] == b' ' || s[0] == b'\t' || s[0] == b'\r' || s[0] == b'\n') {
99 s = &s[1..];
100 }
101 while !s.is_empty() && (s[s.len() - 1] == b' ' || s[s.len() - 1] == b'\t' || s[s.len() - 1] == b'\r' || s[s.len() - 1] == b'\n') {
102 s = &s[..s.len() - 1];
103 }
104 s
105}
106
107fn unescape_json_string(val_bytes: &[u8]) -> Result<String> {
108 if val_bytes.len() >= 2 && val_bytes[0] == b'"' && val_bytes[val_bytes.len() - 1] == b'"' {
109 let inner = &val_bytes[1..val_bytes.len() - 1];
110 let mut s = String::with_capacity(inner.len());
111 let mut chars = std::str::from_utf8(inner)?.chars();
112 while let Some(c) = chars.next() {
113 if c == '\\' {
114 if let Some(next_c) = chars.next() {
115 match next_c {
116 '"' => s.push('"'),
117 '\\' => s.push('\\'),
118 '/' => s.push('/'),
119 'b' => s.push('\x08'),
120 'f' => s.push('\x0c'),
121 'n' => s.push('\n'),
122 'r' => s.push('\r'),
123 't' => s.push('\t'),
124 other => {
125 s.push('\\');
126 s.push(other);
127 }
128 }
129 } else {
130 s.push('\\');
131 }
132 } else {
133 s.push(c);
134 }
135 }
136 Ok(s)
137 } else {
138 Ok(std::str::from_utf8(val_bytes)?.to_string())
139 }
140}
141
142fn append_value(
143 builder: &mut Box<dyn arrow::array::ArrayBuilder>,
144 data_type: &DataType,
145 val_bytes: &[u8],
146) -> Result<()> {
147 match data_type {
148 DataType::Int64 => {
149 let s = std::str::from_utf8(val_bytes)?;
150 let val = s.trim().parse::<i64>()?;
151 builder
152 .as_any_mut()
153 .downcast_mut::<Int64Builder>()
154 .ok_or_else(|| anyhow!("Failed to downcast builder to Int64Builder"))?
155 .append_value(val);
156 }
157 DataType::Float64 => {
158 let s = std::str::from_utf8(val_bytes)?;
159 let val = s.trim().parse::<f64>()?;
160 builder
161 .as_any_mut()
162 .downcast_mut::<Float64Builder>()
163 .ok_or_else(|| anyhow!("Failed to downcast builder to Float64Builder"))?
164 .append_value(val);
165 }
166 DataType::Boolean => {
167 let val = match val_bytes {
168 b"true" => true,
169 b"false" => false,
170 other => {
171 let s = std::str::from_utf8(other)?;
172 s.trim().parse::<bool>()?
173 }
174 };
175 builder
176 .as_any_mut()
177 .downcast_mut::<BooleanBuilder>()
178 .ok_or_else(|| anyhow!("Failed to downcast builder to BooleanBuilder"))?
179 .append_value(val);
180 }
181 DataType::Utf8 | DataType::LargeUtf8 => {
182 let s = unescape_json_string(val_bytes)?;
183 builder
184 .as_any_mut()
185 .downcast_mut::<StringBuilder>()
186 .ok_or_else(|| anyhow!("Failed to downcast builder to StringBuilder"))?
187 .append_value(&s);
188 }
189 other => anyhow::bail!("Unsupported target Arrow data type: {:?}", other),
190 }
191 Ok(())
192}
193
194fn append_null(
195 builder: &mut Box<dyn arrow::array::ArrayBuilder>,
196 data_type: &DataType,
197) -> Result<()> {
198 match data_type {
199 DataType::Int64 => {
200 builder
201 .as_any_mut()
202 .downcast_mut::<Int64Builder>()
203 .ok_or_else(|| anyhow!("Failed to downcast builder to Int64Builder"))?
204 .append_null();
205 }
206 DataType::Float64 => {
207 builder
208 .as_any_mut()
209 .downcast_mut::<Float64Builder>()
210 .ok_or_else(|| anyhow!("Failed to downcast builder to Float64Builder"))?
211 .append_null();
212 }
213 DataType::Boolean => {
214 builder
215 .as_any_mut()
216 .downcast_mut::<BooleanBuilder>()
217 .ok_or_else(|| anyhow!("Failed to downcast builder to BooleanBuilder"))?
218 .append_null();
219 }
220 DataType::Utf8 | DataType::LargeUtf8 => {
221 builder
222 .as_any_mut()
223 .downcast_mut::<StringBuilder>()
224 .ok_or_else(|| anyhow!("Failed to downcast builder to StringBuilder"))?
225 .append_null();
226 }
227 other => anyhow::bail!("Unsupported target Arrow data type: {:?}", other),
228 }
229 Ok(())
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use arrow::array::Array;
236 use arrow::datatypes::{Field, Schema};
237 use std::sync::Arc;
238
239 #[test]
240 fn test_extract_jsonl() -> Result<()> {
241 let jsonl = b"
242 {\"id\": 1, \"name\": \"Alice\", \"active\": true, \"score\": 98.5}
243 {\"id\": 2, \"name\": \"Bob\", \"active\": false, \"score\": 85.0}
244 {\"id\": 3, \"name\": \"Charlie\", \"active\": true}
245 ";
246
247 let paths = vec![
248 "id".to_string(),
249 "name".to_string(),
250 "active".to_string(),
251 "score".to_string(),
252 ];
253 let extractor = JShiftExtractor::new(paths);
254
255 let schema = Arc::new(Schema::new(vec![
256 Field::new("id", DataType::Int64, true),
257 Field::new("name", DataType::Utf8, true),
258 Field::new("active", DataType::Boolean, true),
259 Field::new("score", DataType::Float64, true),
260 ]));
261
262 let batch = extractor.extract_jsonl(jsonl, schema)?;
263
264 assert_eq!(batch.num_rows(), 3);
265 assert_eq!(batch.num_columns(), 4);
266
267 let id_array = batch.column(0)
269 .as_any()
270 .downcast_ref::<arrow::array::Int64Array>()
271 .unwrap();
272 assert_eq!(id_array.value(0), 1);
273 assert_eq!(id_array.value(1), 2);
274 assert_eq!(id_array.value(2), 3);
275
276 let name_array = batch.column(1)
277 .as_any()
278 .downcast_ref::<arrow::array::StringArray>()
279 .unwrap();
280 assert_eq!(name_array.value(0), "Alice");
281 assert_eq!(name_array.value(1), "Bob");
282 assert_eq!(name_array.value(2), "Charlie");
283
284 let active_array = batch.column(2)
285 .as_any()
286 .downcast_ref::<arrow::array::BooleanArray>()
287 .unwrap();
288 assert_eq!(active_array.value(0), true);
289 assert_eq!(active_array.value(1), false);
290 assert_eq!(active_array.value(2), true);
291
292 let score_array = batch.column(3)
293 .as_any()
294 .downcast_ref::<arrow::array::Float64Array>()
295 .unwrap();
296 assert_eq!(score_array.value(0), 98.5);
297 assert_eq!(score_array.value(1), 85.0);
298 assert!(score_array.is_null(2)); Ok(())
301 }
302}